Author : 岑文初(淘寶花名:放翁)
Email: fangweng@taobao.com
Blog: http://blog.csdn.net/cenwenchu79
這部分內(nèi)容是我前個(gè)禮拜作內(nèi)部分享的一部分,是挑了大家在日常中經(jīng)常使用的生產(chǎn)者消費(fèi)者模式作了一個(gè)細(xì)節(jié)問題的分析來講述關(guān)于系統(tǒng)設(shè)計(jì)中的一些問題,其實(shí)在我前面的救火經(jīng)驗(yàn)分享里面也有部分的介紹,不過那些比較抽象一點(diǎn),需要有實(shí)際的工作經(jīng)歷的同學(xué)才會(huì)體會(huì)的到,而這里就具體的針對某一個(gè)特定場景作了分析.
圖 1 生產(chǎn)者消費(fèi)者模式
生產(chǎn)者和消費(fèi)者模式在我們?nèi)粘Ia(chǎn)的代碼里面應(yīng)該出現(xiàn)的很頻繁,但是有一些細(xì)節(jié)足以導(dǎo)致這種模式漏洞百出,同時(shí)也會(huì)使得系統(tǒng)不穩(wěn)定。在早期Java來實(shí)現(xiàn)這種生產(chǎn)者和消費(fèi)者模式,通常就采用線程池,資源池加上消息通知(wait,notify)的方式來實(shí)現(xiàn),現(xiàn)在的Jdk有原生線程池(ExecutorService)和阻塞式隊(duì)列,那我就主要說說后者這種實(shí)現(xiàn)需要注意的一些問題。
消費(fèi)者的依賴帶來的系統(tǒng)不穩(wěn)定性
在我們現(xiàn)有系統(tǒng)中消費(fèi)者往往會(huì)依賴于外部系統(tǒng)(文件系統(tǒng),DB等等),或者內(nèi)部處理會(huì)有比較長時(shí)間的消耗,那么對于整個(gè)模式來說就會(huì)出現(xiàn)在特定情況下隊(duì)列暴漲(消費(fèi)者被Hold住,同時(shí)控制了總消費(fèi)者的數(shù)量),此時(shí)對于隊(duì)列的壓力直接會(huì)導(dǎo)致應(yīng)用系統(tǒng)的不穩(wěn)定,這時(shí)候通常就兩種解決方式,一種就是加大消費(fèi)者線程數(shù)(治標(biāo)不治本),另一種就是將業(yè)務(wù)處理再細(xì)分,同時(shí)考慮優(yōu)化。
業(yè)務(wù)處理再細(xì)分,其實(shí)就是考慮對于消費(fèi)者的角色是否應(yīng)該還有分層。參看nio等設(shè)計(jì)思想,其實(shí)可以看到對于工作者角色和消息監(jiān)聽者角色的分割,可以提高對于消息的處理,增加吞吐量。簡單來說就是消費(fèi)者作的事情更少,功能更加單薄,目的就是將消費(fèi)這個(gè)動(dòng)作加速,而將具體的業(yè)務(wù)操作分配給后端的工作線程來做,同時(shí)考慮在分配的過程中作合并和其他的優(yōu)化處理,批量處理消息提高效率。
這么做的優(yōu)點(diǎn)就在于避免“單點(diǎn)”資源池或者隊(duì)列的不穩(wěn)定性,任務(wù)在低并發(fā)下按常規(guī)即時(shí)處理,在高并發(fā)下批量優(yōu)化處理。
圖2 生產(chǎn)者消費(fèi)者模式
三個(gè)維度解決消費(fèi)慢于生產(chǎn)的情況
當(dāng)消費(fèi)無論如何優(yōu)化都慢于生產(chǎn)的情況,那么需要考慮在三個(gè)維度上去防止異常情況發(fā)生。
1. 控制生產(chǎn)者生產(chǎn)頻度。
2. 對列或者資源池空間大小限制,同時(shí)制定滿載的消息處理策略(出錯(cuò)丟棄,磁盤固化等等)
3. 工作者處理時(shí)長控制,超時(shí)丟棄任務(wù)。
資源是寶貴的
這個(gè)在以前反復(fù)說過,但是實(shí)際操作過程中往往被很多同學(xué)忽視。
1.使用線程池一定要設(shè)置邊界,不然連接池不斷膨脹會(huì)立刻導(dǎo)致內(nèi)存溢出。
2.隊(duì)列需要設(shè)置大小,特別是在選擇時(shí)用阻塞隊(duì)列的時(shí)候需要仔細(xì)考慮,同時(shí)存儲(chǔ)在隊(duì)列的內(nèi)容盡量是對象的標(biāo)示,在性能允許的范疇下,由工作線程去獲得具體的龐大的處理數(shù)據(jù)集。
3.超時(shí)時(shí)間無論如何需要設(shè)置,不然依賴的不穩(wěn)定性隨時(shí)可以擊垮系統(tǒng)。
并行和串行相輔相成
很怕很多同學(xué)動(dòng)不動(dòng)就起一個(gè)線程池,說是多線程效率高。其實(shí)是否選擇多線程首先就是要考慮這個(gè)任務(wù)并行執(zhí)行是否好于串行執(zhí)行。
1. 是不是關(guān)鍵路徑。有時(shí)候優(yōu)化了半天其實(shí)到后續(xù)還會(huì)堵塞在流程的某一階段,那么多線程的意義就不大了。
2. 會(huì)不會(huì)有資源競爭。有資源競爭問題不大,但是發(fā)現(xiàn)競爭帶來的性能損失要遠(yuǎn)多于多線程帶來的性能節(jié)省,那么就絕對不選擇多線程。并行化計(jì)算的最大問題就是一個(gè)共享資源訪問控制問題,解決這個(gè)問題就兩種方式:a.共享資源,鎖機(jī)制保證數(shù)據(jù)一致性。b.不共享資源,操作結(jié)果可合并。(Share nothing,也是MapReduce, Erlang等分布式計(jì)算的核心設(shè)計(jì)理念)
3. 簡單的工作串行,復(fù)雜的工作多線程并行執(zhí)行。這個(gè)其實(shí)回到上面將消費(fèi)者在分成消息監(jiān)聽者和任務(wù)執(zhí)行者兩個(gè)角色。(消息監(jiān)聽如果處理得夠快,那么采用單線程串行處理也可以接受,只要保證任何異常不會(huì)中止監(jiān)聽工作)
多線程,并行處理不是包治百病的良藥,串行并行結(jié)合起來根據(jù)實(shí)際場景來合理使用,才會(huì)設(shè)計(jì)出簡單高效的系統(tǒng)架構(gòu)。(設(shè)計(jì)作復(fù)雜容易,作簡單難,因此不要在意簡單的設(shè)計(jì)圖拿不出手,因?yàn)橛脩糁辉诤跞绾蔚玫椒€(wěn)定,高效的服務(wù))
容錯(cuò)策略的抉擇
上面有提到如果隊(duì)列滿了應(yīng)該做一定的策略去保證業(yè)務(wù)的正常流轉(zhuǎn)。但是對于容錯(cuò)策略的選擇上,其實(shí)要考慮自己系統(tǒng)地特性。原則如下:
1. 業(yè)務(wù)需求優(yōu)先。(任務(wù)是否可以丟,任務(wù)執(zhí)行順序是否有要求,任務(wù)的及時(shí)性)
2. 架構(gòu)簡單。
3. 不引入新的性能瓶頸和系統(tǒng)不穩(wěn)定因素。
根據(jù)上面的幾點(diǎn),首先業(yè)務(wù)是否可以丟棄,如果可以丟棄,那么很簡單,直接丟棄過載的任務(wù)請求(考慮異步記錄一些日志備作查詢和告警)。如果不可以丟棄,那么就考慮執(zhí)行的順序是否有要求,執(zhí)行的即時(shí)性是否有要求,這將直接決定你數(shù)據(jù)恢復(fù)處理的策略。此時(shí)就會(huì)結(jié)合2,3兩點(diǎn)來考量方案,有可能會(huì)引入持久化操作,同時(shí)還有恢復(fù)處理的順序等等。但是一定要仔細(xì)判斷是否因此會(huì)帶來其他的性能瓶頸。總結(jié)起來一個(gè)結(jié)論,在業(yè)務(wù)容許的范圍內(nèi),結(jié)構(gòu)越簡單越好。
后話:
我記得我剛工作那陣子也和現(xiàn)在一些剛畢業(yè)不久的同學(xué)一樣,如果覺得自己的設(shè)計(jì)很簡單,發(fā)現(xiàn)出去講講都很沒面子,一定要想一個(gè)很完備的方案,面面俱到,但其實(shí)就像我前面所說的,客戶在乎的不是你如何實(shí)現(xiàn),而是是否能夠滿足他的需求(業(yè)務(wù)上,穩(wěn)定性,容錯(cuò)性)。
會(huì)做出很復(fù)雜的設(shè)計(jì)但是從來不關(guān)心客戶的想法的程序員僅僅只能算是一個(gè)學(xué)生。
會(huì)考慮如何滿足客戶需求但是不會(huì)走在客戶前面多為將來考慮的程序員是一個(gè)新手。
會(huì)考慮如何滿足客戶,同時(shí)會(huì)為客戶更進(jìn)一步思考的程序員是一個(gè)合格的程序員。
工作3年內(nèi)還是一個(gè)新手不可怕,可怕的是工作了幾年還是處于一個(gè)學(xué)生狀態(tài),如何把設(shè)計(jì)從簡單做復(fù)雜,然后再從復(fù)雜作到簡單,其實(shí)才考驗(yàn)一個(gè)人實(shí)際的工作能力,在合時(shí)的環(huán)境采用合適的方法,得出最簡化方案是程序員應(yīng)該追求的。引用我小時(shí)候讀書老師常常灌輸我的一句話:“書是先要讀厚來,然后再讀薄的。“