但如果你想了解Java大數(shù)據(jù)平臺(tái)開發(fā)瞧哟、項(xiàng)目系統(tǒng)的優(yōu)化實(shí)戰(zhàn)混巧。請(qǐng)繼續(xù)向下閱讀。
項(xiàng)目背景
該項(xiàng)目是銀行自用項(xiàng)目勤揩,是多租戶的數(shù)據(jù)查詢平臺(tái)咧党。可能很多人對(duì)這個(gè)概念不是很清楚陨亡,別急傍衡,容我做個(gè)簡(jiǎn)單的介紹深员,就明白這個(gè)系統(tǒng)是干嘛的了。
項(xiàng)目簡(jiǎn)介
首先蛙埂,整個(gè)系統(tǒng)是基于Dubbo的分布式系統(tǒng)架構(gòu)倦畅,數(shù)據(jù)存儲(chǔ)統(tǒng)一存儲(chǔ)在數(shù)據(jù)倉(cāng)庫(kù)。數(shù)據(jù)倉(cāng)庫(kù)提供多種存儲(chǔ)方式绣的,包括MySQL叠赐、HDFS、HBSE被辑、Hive燎悍、Impala、Spark盼理、ElasticSearch等等谈山。而如果讓業(yè)務(wù)方去做數(shù)據(jù)存取操作,顯然是非常麻煩的宏怔。所以在業(yè)務(wù)系統(tǒng)與數(shù)據(jù)倉(cāng)庫(kù)之間再搭建了一個(gè)數(shù)據(jù)查詢系統(tǒng)——這就是本篇文章的主角奏路。
系統(tǒng)架構(gòu)
項(xiàng)目源碼
這里會(huì)給大家展示項(xiàng)目的部分源碼,當(dāng)然臊诊,所展示的源碼都是功能性的而非項(xiàng)目業(yè)務(wù)相關(guān)(即任何項(xiàng)目都可以有這些代碼)鸽粉,大家可以先找找茬。
通過(guò)4張圖抓艳,大家應(yīng)該對(duì)該系統(tǒng)之前的編碼水平有了大致的了解触机。下面我將一一解鎖每張?jiān)创a圖的故事。
源碼1:
源碼1是我在做功能調(diào)試的時(shí)候發(fā)現(xiàn)的一個(gè)BUG玷或,邏輯非常簡(jiǎn)單儡首,就是比對(duì)兩個(gè)id是否相等。但為什么這就產(chǎn)生BUG了呢偏友?
很簡(jiǎn)單蔬胯,就是包裝類的緩存!
Integer和Long類型會(huì)有1個(gè)byte的緩存位他,即 -128 ~ 127氛濒,當(dāng)比較數(shù)的返回在此之間時(shí),因?yàn)槎际鞘褂玫木彺娑焖琛r?yàn)證代碼如下:
package demo;
public class IntegerCacheDemo {
public static void main(String[] args) {
compare(1,1);
compare(127,127);
compare(128,128);
compareWithEquals(1,1);
compareWithEquals(127,127);
compareWithEquals(128,128);
}
/**
* 錯(cuò)誤的包裝類比較
* @param a
* @param b
*/
public static void compare(Integer a, Integer b){
System.out.println(a == b ? a + " == " + b:a + " != " + b);
}
/**
* 正確的的包裝類比較
* @param a
* @param b
*/
public static void compareWithEquals(Integer a, Integer b){
System.out.println(a.equals(b) ? a + " == " + b:a + " != " + b);
}
}
測(cè)試結(jié)果:
測(cè)試的結(jié)果印證了前面的說(shuō)法舞竿。
眾所周知, == 比較是直接比較的地址窿冯,而由于緩存的原因骗奖,包裝類緩存所指向的都是同一個(gè)對(duì)象,所有 == 判斷返回true,而當(dāng)超出了緩存的返回重归,包裝類的對(duì)象都是新創(chuàng)建的地址,使用 == 判斷會(huì)返回false厦凤,而equals判斷使用的是重寫的equals方法鼻吮,Integer的equals方法如下:
public boolean equals(Object obj) {
//判斷類型是否相同
if (obj instanceof Integer) {
//如果相同則判斷值是否相同,this.value存儲(chǔ)的是int類型值较鼓, == 與
//Integer比較椎木,會(huì)觸發(fā)自動(dòng)拆箱, 即等價(jià)于 int == int 判斷
return this.value == (Integer)obj;
} else {
return false;
}
}
再來(lái)看IntegerCache的源碼吧博烂。
注意看全紅部分香椎,相信大家都明白了吧。 其他的包裝類如Long禽篱、Short畜伐、Byte等都有對(duì)應(yīng)的緩存,而且都是一個(gè)byte的取值范圍躺率。
源碼2:
請(qǐng)注意玛界,源碼2是在上線第二天就引起了線上事故。
業(yè)務(wù)描述
業(yè)務(wù)方通過(guò)查詢接口調(diào)用查詢平臺(tái)悼吱,查詢平臺(tái)通過(guò)Zookepper訪問(wèn)到Hbase獲取數(shù)據(jù)并返回慎框。
問(wèn)題排查
通過(guò)錯(cuò)誤日志,可以查到當(dāng)時(shí)有很多請(qǐng)求查詢失敗后添,并且偶爾會(huì)有一個(gè)查詢成功笨枯,且失敗數(shù)量是成線性增長(zhǎng)的趨勢(shì)。當(dāng)時(shí)我就根據(jù)經(jīng)驗(yàn)判斷是連接出了問(wèn)題遇西。
果然馅精,通過(guò)查看zookeeper日志,發(fā)現(xiàn)確實(shí)報(bào)連接數(shù)超過(guò)最大限制努溃,但業(yè)務(wù)方反饋業(yè)務(wù)才上線硫嘶,使用人數(shù)也就10來(lái)人。那么可以判斷梧税,代碼存在BUG沦疾。
問(wèn)題解決
首先,修改代碼上線是需要經(jīng)過(guò)一個(gè)流程的第队,不適合短時(shí)間解決哮塞。 而我們zookepper的最大連接數(shù)配置的是100,我們先將最大連接數(shù)調(diào)整到600凳谦,然后查找代碼BUG修復(fù)忆畅。
通過(guò)走查代碼,發(fā)現(xiàn)代碼中有一個(gè)非常低級(jí)且致命的低級(jí)錯(cuò)誤(大家有沒(méi)有發(fā)現(xiàn)呢尸执?)家凯,就是圖2的try-cache中的代碼缓醋,調(diào)用了createConnection(conf)方法兩次,其中一個(gè)連接返回給調(diào)用者绊诲,而另外一個(gè)連接創(chuàng)建后則沒(méi)有返回送粱。返回的連接會(huì)在使用后正確關(guān)閉,而沒(méi)有返回的連接由于永遠(yuǎn)不可能會(huì)有調(diào)用者,也就不可能手動(dòng)釋放掂之,而只能等待超時(shí)自動(dòng)釋放抗俄,超時(shí)時(shí)間在代碼中也看到了-30000ms。這就解釋了為什么并發(fā)不高的情況下世舰,連接首先掛掉了动雹。去掉此段代碼即可。
4.優(yōu)化升級(jí)
請(qǐng)注意看上一段加粗的文字跟压,我為什么加粗呢胰蝠? 肯定是另有乾坤啊,哈哈哈哈~~~裆馒!
圖2中有3個(gè)代碼片段姊氓, 可以看到,整個(gè)操作流程是 : 獲取配置 -> 創(chuàng)建Util對(duì)象 -> 創(chuàng)建連接 -> 查詢 -> 關(guān)閉連接喷好。
OMG翔横!OMG!OMG梗搅! 不得不驚嘆在21世紀(jì)的20年代禾唁, 居然還能看到這樣的代碼。OK无切,兩個(gè)問(wèn)題荡短,其一,整個(gè)流程少了個(gè)連接池吧哆键? 其二掘托,util對(duì)象居然是要new出來(lái)使用。 不使用連接池的弊端無(wú)需多說(shuō)籍嘹,太浪費(fèi)資源了闪盔。我們可以看看在單機(jī)并發(fā)下TCP連接數(shù)。
看到那個(gè)頂上去的尖了嗎辱士。 并發(fā)也不是特別高泪掀, 20線程 * 200 次循環(huán)。 可以想象颂碘,如果在生產(chǎn)環(huán)境异赫,并發(fā)量如果稍微上去一點(diǎn),這機(jī)器是最先扛不住的。
ok塔拳,繼續(xù)整鼠证。優(yōu)化思路:
- 配置和工具類分離,創(chuàng)建配置對(duì)象靠抑,而不創(chuàng)建工具對(duì)象
- 使用連接池管理連接名惩,這一點(diǎn)比較好辦,Hbase的Java客戶端提供了連接池孕荠。
經(jīng)過(guò)優(yōu)化,TCP連接基本比較穩(wěn)定攻谁,優(yōu)化代碼我這里就不貼了稚伍,代碼還是不少,重要的是思路而不是代碼戚宦。
源碼3 + 源碼4:
源碼3比較簡(jiǎn)單个曙,就是最基本的JDBC獲取連接操作,同樣的問(wèn)題受楼,整個(gè)操作都是創(chuàng)建連接垦搬、查詢、關(guān)閉連接艳汽,而沒(méi)有使用到連接池猴贰。但這一點(diǎn)和Hbase操作又有所不同,Hbase的數(shù)據(jù)源在系統(tǒng)中只有一個(gè)河狐,而JDBC的數(shù)據(jù)源就非常多了米绕,包括MySQL、Hive馋艺、Impala都是使用JDBC來(lái)連接的栅干,而每一個(gè)數(shù)據(jù)庫(kù)就是一個(gè)數(shù)據(jù)源。這樣我們系統(tǒng)中就會(huì)有非常多的數(shù)據(jù)源捐祠,而不是單一的數(shù)據(jù)資源管理碱鳞。
而源碼4我認(rèn)為是比較好的代碼,通過(guò)令牌池的機(jī)制限制了單臺(tái)服務(wù)器的最大數(shù)據(jù)庫(kù)連接數(shù)量踱蛀,這種思想在高并發(fā)中也可以使用窿给。 相對(duì)于限流的一種機(jī)制,他最大限度的保證了服務(wù)器的穩(wěn)定性星岗,不像源碼2那樣直接導(dǎo)致服務(wù)不可用填大。
這里的優(yōu)化思路就是在一個(gè)數(shù)據(jù)源對(duì)應(yīng)一個(gè)連接池,而多個(gè)數(shù)據(jù)源則對(duì)應(yīng)多個(gè)連接池俏橘,然后對(duì)多個(gè)連接池進(jìn)行緩存允华,當(dāng)請(qǐng)求訪問(wèn)的時(shí)候,首先根據(jù)請(qǐng)求查找到對(duì)應(yīng)的連接池,然后再?gòu)倪B接池獲取一個(gè)連接返回靴寂。這樣就解決了頻繁創(chuàng)建連接的問(wèn)題磷蜀。這種方式暫時(shí)提升了系統(tǒng)的并發(fā)度,但這種方式對(duì)服務(wù)器的本地資源占用比較多百炬,還有其他的解決方案褐隆,比如開源的中間件MyCat等。
如果你看到了這里剖踊,證明你太有耐心了 哈哈~J!