作者:閃客sun | 博客園
https://www.cnblogs.com/flashsun
一直不知道性能優(yōu)化都要做些什么重挑,從哪方面思考,直到最近接手了一個(gè)公司的小項(xiàng)目撩鹿,可謂麻雀雖小五臟俱全。讓我這個(gè)編程小白學(xué)到了很多性能優(yōu)化的知識(shí)悦屏,或者說(shuō)一些思考方式节沦。真的感受到任何一點(diǎn)效率的損失放大一定倍數(shù)時(shí),將會(huì)是天文數(shù)字础爬。最初我的程序計(jì)算下來(lái)需要跑2個(gè)月才能跑完甫贯,經(jīng)過(guò)2周不斷地調(diào)整架構(gòu)和細(xì)節(jié),將性能提升到了4小時(shí)完成看蚜。
很多心得體會(huì)叫搁,希望和大家分享,也希望多多批評(píng)指正供炎,共同進(jìn)步渴逻。
一、項(xiàng)目描述
我將公司的項(xiàng)目?jī)?nèi)容抽象音诫,大概是要做這樣一件事情:
1惨奕、數(shù)據(jù)庫(kù)A中有2000萬(wàn)條用戶數(shù)據(jù);
2纽竣、將數(shù)據(jù)庫(kù)A中的用戶讀出墓贿,為每條用戶生成guid茧泪,并保存到數(shù)據(jù)庫(kù)B中;
3聋袋、同時(shí)在數(shù)據(jù)庫(kù)A中生成關(guān)聯(lián)表队伟;
項(xiàng)目要求為:
1、將用戶存入數(shù)據(jù)庫(kù)B的過(guò)程需要調(diào)用sdk的注冊(cè)接口幽勒,不允許直接操作jdbc進(jìn)行插入嗜侮;
2、數(shù)據(jù)要求可恢復(fù):再次運(yùn)行要跳過(guò)已成功的數(shù)據(jù)啥容;出錯(cuò)的數(shù)據(jù)要進(jìn)行持久化以便下次可以選擇恢復(fù)該部分?jǐn)?shù)據(jù)锈颗;
3、數(shù)據(jù)要保證一致性:在不出錯(cuò)的情況下咪惠,數(shù)據(jù)庫(kù)B的用戶必然一一對(duì)應(yīng)數(shù)據(jù)庫(kù)A的關(guān)聯(lián)表击吱。如果出錯(cuò),那么正確的數(shù)據(jù)加上記錄下來(lái)的出錯(cuò)數(shù)據(jù)后要保證一致性遥昧;
4覆醇、速度要盡可能塊:共2000萬(wàn)條數(shù)據(jù),在保證正確性的前提下炭臭,至多一天內(nèi)完成永脓;
二、第一版:面向過(guò)程——2個(gè)月
特征:面向過(guò)程鞋仍、單一線程常摧、不可拓展、極度耦合威创、逐條插入落午、數(shù)據(jù)不可恢復(fù)
最初的一版簡(jiǎn)直是匯聚了一個(gè)項(xiàng)目的所有缺點(diǎn)。整個(gè)流程就是從A庫(kù)讀出一條數(shù)據(jù)那婉,立刻做處理板甘,然后調(diào)用接口插入B庫(kù),然后在拼一個(gè)關(guān)聯(lián)表的sql語(yǔ)句详炬,插入A庫(kù)盐类。沒(méi)有計(jì)數(shù)器,沒(méi)有錯(cuò)誤信息處理呛谜。這樣下來(lái)的代碼最終預(yù)測(cè)2000萬(wàn)條數(shù)據(jù)要處理2個(gè)月在跳。如果中間哪怕一條數(shù)據(jù)出錯(cuò),又要重新再來(lái)2個(gè)月隐岛。簡(jiǎn)直可怕猫妙。
這個(gè)流程圖就等同于廢話,是完全基于面向過(guò)程的思想聚凹,整個(gè)代碼就是在一個(gè)大main方法里寫(xiě)的割坠,實(shí)際業(yè)務(wù)流程完全等同于代碼的流程齐帚。思考起來(lái)簡(jiǎn)單,但實(shí)現(xiàn)和維護(hù)起來(lái)極為困難彼哼,代碼結(jié)構(gòu)冗長(zhǎng)混亂对妄。而且?guī)缀跏遣豢蓴U(kuò)展的。暫且不談代碼的設(shè)計(jì)美觀敢朱,它的效率如此低下主要有一下幾點(diǎn):
1剪菱、每一條數(shù)據(jù)的速度受制于整個(gè)鏈條中最慢的一環(huán)。試想假如有一條A庫(kù)插入關(guān)聯(lián)表的數(shù)據(jù)卡住了拴签,等待將近1分鐘(夸張了點(diǎn))孝常,那這一分鐘jvm完全就在傻等,它完全可以繼續(xù)進(jìn)行之前的兩步蚓哩。正如你等待雞蛋煮熟的過(guò)程中可以同時(shí)去做其他的事一樣构灸。
2、向B庫(kù)插入用戶需要調(diào)用sdk(HTTP請(qǐng)求)接口杖剪,那每一次調(diào)用都需要建立連接冻押,等待響應(yīng)驰贷,再釋放鏈接盛嘿。正如你要給朋友送一箱蘋(píng)果,你分成100次每次只送一個(gè)括袒,時(shí)間全搭載路上了次兆。
三、第二版:面向?qū)ο蟆?1天
特征:面向?qū)ο笄旅獭我痪€程芥炭、可拓展、略微耦合恃慧、批量插入园蝠、數(shù)據(jù)可恢復(fù)
3.1、架構(gòu)設(shè)計(jì)
根據(jù)第一版設(shè)計(jì)的問(wèn)題痢士,第二版有了一些改進(jìn)彪薛。當(dāng)然最明顯的就是從面向過(guò)程的思想轉(zhuǎn)變?yōu)槊嫦驅(qū)ο蟆?/p>
我將整個(gè)過(guò)程抽離出來(lái),分配給不同的對(duì)象去處理怠蹂。這樣善延,我所分配的對(duì)象時(shí)這樣的:
1、一個(gè)配置對(duì)象:BatchStrategy说订。負(fù)責(zé)從配置文件中讀取本次任務(wù)的策略并傳遞給執(zhí)行者家厌,配置包括基礎(chǔ)配置如總條數(shù)胚嘲,每次批量查詢的數(shù)量,每次批量插入的數(shù)量豆茫。還有一些數(shù)據(jù)源方面的侨歉,如來(lái)源表的表名、列名揩魂、等为肮,這樣如果換成其他數(shù)據(jù)庫(kù)的類(lèi)似導(dǎo)入,就能供通過(guò)配置進(jìn)行拓展了肤京。
2颊艳、三個(gè)執(zhí)行者:整個(gè)執(zhí)行過(guò)程可以分成三個(gè)部分:讀數(shù)據(jù)--處理數(shù)據(jù)--寫(xiě)數(shù)據(jù),可以分別交給三個(gè)對(duì)象Reader忘分,Processor棋枕,Writer進(jìn)行。這樣如果某一處邏輯變了妒峦,可以單獨(dú)進(jìn)行改變而不影響其他環(huán)節(jié)重斑。
3、一個(gè)失敗數(shù)據(jù)處理類(lèi):ErrorHandler肯骇。這樣每當(dāng)有數(shù)據(jù)出現(xiàn)異常時(shí)窥浪,便把改數(shù)據(jù)扔給這個(gè)類(lèi),在這給類(lèi)中進(jìn)行寫(xiě)入日志笛丙,或者其他的處理辦法漾脂。在一定程度上將失敗數(shù)據(jù)的處理解耦。
這種設(shè)計(jì)很大程度上解除了耦合胚鸯,尤其是失敗數(shù)據(jù)的處理基本上完全解耦骨稿。但由于整個(gè)執(zhí)行過(guò)程仍然是需要有一個(gè)main來(lái)分別調(diào)用三個(gè)對(duì)象處理任務(wù),因此三者之間還是沒(méi)有完全解耦姜钳,main部分的邏輯依然是面向過(guò)程的思想坦冠,比較復(fù)雜。即使把main中執(zhí)行的邏輯抽出一個(gè)service哥桥,這個(gè)問(wèn)題依然沒(méi)有解決辙浑。
3.2、效率問(wèn)題
由于將第一版的逐條插入改為批量插入拟糕。其中sdk接口部分是批量傳入一組數(shù)據(jù)判呕,減少了http請(qǐng)求的次數(shù)。生成關(guān)聯(lián)表的部分是用了jdbc batch操作已卸,將之前逐條插入的excute改為excuteBatch佛玄,效率提升很明顯。這兩部分批量帶來(lái)的效率提升累澡,將原本需要兩個(gè)月時(shí)間的代碼梦抢,提升到了21天,但依然是天文數(shù)字愧哟。
可以看出奥吩,本次效率提升僅僅是在減少http請(qǐng)求次數(shù)哼蛆,優(yōu)化sql的插入邏輯方面做出來(lái)努力,但依然沒(méi)有解決第一版的一個(gè)致命問(wèn)題霞赫,就是一次循環(huán)的速度依然受制于整個(gè)鏈條中最慢的一環(huán)腮介,三者沒(méi)有解耦也可以從這一點(diǎn)看出,在其他兩者沒(méi)有將工作做完時(shí)端衰,就只能傻等叠洗,這是效率損失最嚴(yán)重的地方了。
** 四旅东、第三版:完全解耦(隊(duì)列+多線程)——3天**
特征:面向?qū)ο竺鹨帧⒍嗑€程、可拓展抵代、完全解耦腾节、批量插入、數(shù)據(jù)可恢復(fù)荤牍。
4.1案腺、架構(gòu)設(shè)計(jì)
該版并沒(méi)有代碼實(shí)現(xiàn),但確是過(guò)度到下一版的重要思考過(guò)程康吵,故記錄在次劈榨。這一版本較上一版的重大改進(jìn)之處有兩點(diǎn):隊(duì)列和多線程。
隊(duì)列:其中隊(duì)列的使用使上一版未完全解耦的執(zhí)行類(lèi)之間涎才,實(shí)現(xiàn)了完全解耦鞋既,將同步過(guò)程變?yōu)楫惒剑瑫r(shí)也是多線程能夠使用的前提耍铜。Reader做的事就是讀取數(shù)據(jù),并放入隊(duì)列跌前,至于它的下一個(gè)環(huán)節(jié)Processor如何處理隊(duì)列的數(shù)據(jù)棕兼,它完全不用理會(huì),這時(shí)便可以繼續(xù)讀取數(shù)據(jù)抵乓。這便做到了完全解耦伴挚,處理隊(duì)列的數(shù)據(jù)也能夠使用多線程了。
多線程:Processor和Writer所做的事情灾炭,就是讀取自身隊(duì)列中的數(shù)據(jù)茎芋,然后處理。只不過(guò)Processor比Writer還承擔(dān)了一個(gè)往下一環(huán)隊(duì)列里放數(shù)據(jù)的過(guò)程蜈出。此處的隊(duì)列用的是多線程安全隊(duì)列ConcurrentLinkedQueue田弥。因此可以肆無(wú)忌憚地使用多線程來(lái)執(zhí)行這兩者的任務(wù)。由于各個(gè)環(huán)節(jié)之間的完全解耦铡原,某一環(huán)上的偶爾卡主并不再影響整個(gè)過(guò)程的進(jìn)度偷厦,所以效率提升不知一兩點(diǎn)商叹。
還有一點(diǎn)就是數(shù)據(jù)的可恢復(fù)性在這個(gè)設(shè)計(jì)中有了保障,成功過(guò)的用戶被保存起來(lái)以便再次運(yùn)行不會(huì)沖突只泼,失敗的關(guān)聯(lián)表數(shù)據(jù)也被記錄下來(lái)剖笙,在下次運(yùn)行時(shí)Writer會(huì)先將這一部分加入到自己的隊(duì)列里,整個(gè)數(shù)據(jù)的正確性就有了一個(gè)不是特別完善的方案请唱,效率也有了可觀的提升弥咪。
4.2、效率問(wèn)題
雖然效率從21天提升到了3天十绑,但我們還要思考一些問(wèn)題酪夷。實(shí)際在執(zhí)行的過(guò)程中發(fā)現(xiàn),Writer所完成的數(shù)據(jù)總是緊跟在Processor之后孽惰。這就說(shuō)明Processor的處理速度要慢于Writer晚岭,因?yàn)镻rocessor插入數(shù)據(jù)庫(kù)之前還要走一段注冊(cè)用戶的業(yè)務(wù)邏輯。這就有個(gè)問(wèn)題勋功,當(dāng)上一環(huán)的速度慢過(guò)下一環(huán)時(shí)坦报,還有必要進(jìn)行批量的操作么?答案是不需要的狂鞋。試想一下片择,如果你在生產(chǎn)線上,你的上一環(huán)2秒鐘處理一個(gè)零件骚揍,而你的速度是1秒鐘一個(gè)字管。這時(shí)即使你的批量處理速度更快,從系統(tǒng)最優(yōu)的角度考慮信不,你也應(yīng)該來(lái)一個(gè)零件就馬上處理嘲叔,而不是等積攢到100個(gè)再批量處理。
還有一個(gè)問(wèn)題是抽活,我們從未考慮過(guò)Reader的性能硫戈。實(shí)際上我用的是limit操作來(lái)批量讀取數(shù)據(jù)庫(kù),而mysql的limit是先全表查再截取下硕,當(dāng)起始位置很大時(shí)丁逝,就會(huì)越來(lái)越慢。0-1000萬(wàn)還算輕松梭姓,但1000萬(wàn)到2000萬(wàn)簡(jiǎn)直是“寸步難行”霜幼。所以最終效率的瓶頸反而落到了讀庫(kù)操作上。
五誉尖、第四版:高度抽象(一鍵啟動(dòng))——4小時(shí)
特征:面向接口罪既、多線程、可拓展、完全解耦萝衩、批量或逐條插入回挽、數(shù)據(jù)可恢復(fù)、優(yōu)化查詢的limit操作
5.1猩谊、架構(gòu)的思考
優(yōu)雅的代碼應(yīng)該是整潔而美妙千劈,不應(yīng)是冗長(zhǎng)而復(fù)雜的。這一版將會(huì)設(shè)計(jì)出簡(jiǎn)潔度如第一版牌捷,而性能和拓展性超越所有版本的架構(gòu)墙牌。
通過(guò)總結(jié)前三版特征,我發(fā)現(xiàn)不論是Reader暗甥,Processor喜滨,Writer,都有共同的特征:?jiǎn)?dòng)任務(wù)撤防、處理任務(wù)虽风、結(jié)束任務(wù)。而Reader和Processor又有一個(gè)共同的可以向下一道工序傳遞數(shù)據(jù)寄月,通知下一道工序數(shù)據(jù)傳遞結(jié)束的功能辜膝。他們就像生產(chǎn)線上的一個(gè)個(gè)工序,相互關(guān)聯(lián)而又各自獨(dú)立地運(yùn)行著漾肮。每一道工序都可以啟動(dòng)厂抖,瘋狂地處理任務(wù),直到上一道工序通知結(jié)束為止克懊。而第一個(gè)發(fā)起通知結(jié)束的便是Reader忱辅,之后便一個(gè)通知下一個(gè),直到整個(gè)工序停止谭溉,這個(gè)過(guò)程就是美妙的墙懂。
因此我們可以將這三者都看做是Job,除了Reader外又都有與上一道工序交互的能力(其實(shí)Reader的上一道工序就是數(shù)據(jù)庫(kù))夜只,因此便有了如下的接口設(shè)計(jì)垒在。
有了這樣的接口設(shè)計(jì),不論實(shí)現(xiàn)類(lèi)具體怎么寫(xiě)扔亥,主方法已經(jīng)可以寫(xiě)出了,變得異常整潔有序谈为。
只提煉主干部分旅挤,去掉了一些細(xì)枝末節(jié),如日志輸出伞鲫、時(shí)間記錄等粘茄。
接下來(lái)就是具體實(shí)現(xiàn)類(lèi)的問(wèn)題了,這里實(shí)現(xiàn)類(lèi)主要實(shí)現(xiàn)的是三個(gè)功能:
1、接收上一環(huán)的數(shù)據(jù):屬于Interactive接口的receive方法的實(shí)現(xiàn)柒瓣,基于之前的設(shè)計(jì)儒搭,即是對(duì)象中有一個(gè)ConcurrentLinkedQueue類(lèi)型的屬性,用來(lái)接收上一環(huán)傳來(lái)的數(shù)據(jù)芙贫。
2搂鲫、處理數(shù)據(jù)并傳遞給下一環(huán):在每一個(gè)(有下一環(huán)的)對(duì)象屬性中,放入下一環(huán)的對(duì)象磺平。如Reader中要有Processor對(duì)象魂仍,Processor要有Writer,一旦有數(shù)據(jù)需要加入下一環(huán)的隊(duì)列拣挪,調(diào)用其receiive方法即可擦酌。
3、告訴下一環(huán)我結(jié)束了:本任務(wù)結(jié)束時(shí)菠劝,調(diào)用下一環(huán)對(duì)象的closeInteractive方法赊舶。而每個(gè)對(duì)象判斷自身結(jié)束的方法視情況而定,比如Reader結(jié)束的條件是批量讀取的數(shù)據(jù)超過(guò)了一開(kāi)始設(shè)置的total赶诊,說(shuō)明數(shù)據(jù)讀取完畢笼平,可以結(jié)束。而Processor結(jié)束的條件是甫何,它被上一環(huán)通知了結(jié)束出吹,并且從自己的隊(duì)列中poll不出東西了,證明應(yīng)該結(jié)束辙喂,結(jié)束后再通知下一環(huán)節(jié)捶牢。這樣整個(gè)工序就安全有序地退出了。不過(guò)由于是多線程巍耗,所以Processor不能貿(mào)然通知Writer結(jié)束信號(hào)秋麸,需要在Processor內(nèi)部弄一個(gè)計(jì)數(shù)器,只有計(jì)數(shù)器達(dá)到預(yù)期的數(shù)量的那個(gè)線程的Processor炬太,才能發(fā)起結(jié)束通知灸蟆。
5.2、效率問(wèn)題:
正如上一版提出的亲族,Processor的處理速度要慢于Writer炒考,所以Writer并不需要用batch去處理數(shù)據(jù)的插入,該成逐條插入反而是提高性能的一種方式霎迫。
大數(shù)據(jù)量limit操作十分耗時(shí)斋枢,由于測(cè)試部分只是在前幾百萬(wàn)條測(cè)試,所以還是大大低估了效率的損失知给。在后幾百萬(wàn)條可以說(shuō)每一次limit的讀取都寸步難行瓤帚∶枰Γ考慮到這個(gè)問(wèn)題,我選去了唯一一個(gè)有索引并且稍稍易于排序的字段“用戶的手機(jī)號(hào)”戈次,(不想吐槽它們?cè)O(shè)計(jì)表的時(shí)候居然沒(méi)有自增id轩勘。。怯邪。)绊寻,每次全表將手機(jī)號(hào)排序,再limit查詢擎颖。查詢之后將最后一條的手機(jī)號(hào)保存起來(lái)榛斯,成為當(dāng)前讀取的最后一條數(shù)據(jù)的一個(gè)標(biāo)識(shí)。下次再limit操作就可以從這個(gè)手機(jī)號(hào)之后開(kāi)始查詢了搂捧。這樣每次查詢不論從哪里開(kāi)始驮俗,速度都是一樣的。雖然前面部分的數(shù)據(jù)速度與之前的方案相比慢了不少允跑,但卻完美解決了大數(shù)據(jù)量limit操作的超長(zhǎng)等待時(shí)間王凑,預(yù)防了危險(xiǎn)的發(fā)生。
至此聋丝,項(xiàng)目架構(gòu)再次簡(jiǎn)潔起來(lái)索烹,但同第一版相比,已經(jīng)不是同一級(jí)別的簡(jiǎn)潔了弱睦。
六百姓、關(guān)于繼續(xù)優(yōu)化的思考
1、Reader部分是單線程在處理况木,由于讀取是從數(shù)據(jù)庫(kù)中垒拢,并不是隊(duì)列中,因此設(shè)計(jì)成多線程有些麻煩火惊,但并不是不可求类,這里是優(yōu)化點(diǎn)
2、日志部分占有很大一部分比例屹耐,2000萬(wàn)條讀尸疆、處理、寫(xiě)就要有至少6000萬(wàn)次日志輸出惶岭。如果設(shè)計(jì)成異步處理寿弱,效率會(huì)提升不少。
這就是我本次項(xiàng)目?jī)?yōu)化的心得體會(huì)按灶,還望各位大神予以指點(diǎn)脖捻。因?yàn)榇a是公司為了避嫌,就不發(fā)到github了兆衅,感興趣的大神可以私聊。