Author : 岑文初(淘寶花名:放翁)
Email: fangweng@taobao.com
Blog: http://blog.csdn.net/cenwenchu79
這部分內容是我前個禮拜作內部分享的一部分,是挑了大家在日常中經常使用的生產者消費者模式作了一個細節問題的分析來講述關于系統設計中的一些問題,其實在我前面的救火經驗分享里面也有部分的介紹,不過那些比較抽象一點,需要有實際的工作經歷的同學才會體會的到,而這里就具體的針對某一個特定場景作了分析.

圖 1 生產者消費者模式
生產者和消費者模式在我們日常生產的代碼里面應該出現的很頻繁,但是有一些細節足以導致這種模式漏洞百出,同時也會使得系統不穩定。在早期Java來實現這種生產者和消費者模式,通常就采用線程池,資源池加上消息通知(wait,notify)的方式來實現,現在的Jdk有原生線程池(ExecutorService)和阻塞式隊列,那我就主要說說后者這種實現需要注意的一些問題。
消費者的依賴帶來的系統不穩定性
在我們現有系統中消費者往往會依賴于外部系統(文件系統,DB等等),或者內部處理會有比較長時間的消耗,那么對于整個模式來說就會出現在特定情況下隊列暴漲(消費者被Hold住,同時控制了總消費者的數量),此時對于隊列的壓力直接會導致應用系統的不穩定,這時候通常就兩種解決方式,一種就是加大消費者線程數(治標不治本),另一種就是將業務處理再細分,同時考慮優化。
業務處理再細分,其實就是考慮對于消費者的角色是否應該還有分層。參看nio等設計思想,其實可以看到對于工作者角色和消息監聽者角色的分割,可以提高對于消息的處理,增加吞吐量。簡單來說就是消費者作的事情更少,功能更加單薄,目的就是將消費這個動作加速,而將具體的業務操作分配給后端的工作線程來做,同時考慮在分配的過程中作合并和其他的優化處理,批量處理消息提高效率。
這么做的優點就在于避免“單點”資源池或者隊列的不穩定性,任務在低并發下按常規即時處理,在高并發下批量優化處理。

圖2 生產者消費者模式
三個維度解決消費慢于生產的情況
當消費無論如何優化都慢于生產的情況,那么需要考慮在三個維度上去防止異常情況發生。
1. 控制生產者生產頻度。
2. 對列或者資源池空間大小限制,同時制定滿載的消息處理策略(出錯丟棄,磁盤固化等等)
3. 工作者處理時長控制,超時丟棄任務。
資源是寶貴的
這個在以前反復說過,但是實際操作過程中往往被很多同學忽視。
1.使用線程池一定要設置邊界,不然連接池不斷膨脹會立刻導致內存溢出。
2.隊列需要設置大小,特別是在選擇時用阻塞隊列的時候需要仔細考慮,同時存儲在隊列的內容盡量是對象的標示,在性能允許的范疇下,由工作線程去獲得具體的龐大的處理數據集。
3.超時時間無論如何需要設置,不然依賴的不穩定性隨時可以擊垮系統。
并行和串行相輔相成
很怕很多同學動不動就起一個線程池,說是多線程效率高。其實是否選擇多線程首先就是要考慮這個任務并行執行是否好于串行執行。
1. 是不是關鍵路徑。有時候優化了半天其實到后續還會堵塞在流程的某一階段,那么多線程的意義就不大了。
2. 會不會有資源競爭。有資源競爭問題不大,但是發現競爭帶來的性能損失要遠多于多線程帶來的性能節省,那么就絕對不選擇多線程。并行化計算的最大問題就是一個共享資源訪問控制問題,解決這個問題就兩種方式:a.共享資源,鎖機制保證數據一致性。b.不共享資源,操作結果可合并。(Share nothing,也是MapReduce, Erlang等分布式計算的核心設計理念)
3. 簡單的工作串行,復雜的工作多線程并行執行。這個其實回到上面將消費者在分成消息監聽者和任務執行者兩個角色。(消息監聽如果處理得夠快,那么采用單線程串行處理也可以接受,只要保證任何異常不會中止監聽工作)
多線程,并行處理不是包治百病的良藥,串行并行結合起來根據實際場景來合理使用,才會設計出簡單高效的系統架構。(設計作復雜容易,作簡單難,因此不要在意簡單的設計圖拿不出手,因為用戶只在乎如何得到穩定,高效的服務)
容錯策略的抉擇
上面有提到如果隊列滿了應該做一定的策略去保證業務的正常流轉。但是對于容錯策略的選擇上,其實要考慮自己系統地特性。原則如下:
1. 業務需求優先。(任務是否可以丟,任務執行順序是否有要求,任務的及時性)
2. 架構簡單。
3. 不引入新的性能瓶頸和系統不穩定因素。
根據上面的幾點,首先業務是否可以丟棄,如果可以丟棄,那么很簡單,直接丟棄過載的任務請求(考慮異步記錄一些日志備作查詢和告警)。如果不可以丟棄,那么就考慮執行的順序是否有要求,執行的即時性是否有要求,這將直接決定你數據恢復處理的策略。此時就會結合2,3兩點來考量方案,有可能會引入持久化操作,同時還有恢復處理的順序等等。但是一定要仔細判斷是否因此會帶來其他的性能瓶頸。總結起來一個結論,在業務容許的范圍內,結構越簡單越好。
后話:
我記得我剛工作那陣子也和現在一些剛畢業不久的同學一樣,如果覺得自己的設計很簡單,發現出去講講都很沒面子,一定要想一個很完備的方案,面面俱到,但其實就像我前面所說的,客戶在乎的不是你如何實現,而是是否能夠滿足他的需求(業務上,穩定性,容錯性)。
會做出很復雜的設計但是從來不關心客戶的想法的程序員僅僅只能算是一個學生。
會考慮如何滿足客戶需求但是不會走在客戶前面多為將來考慮的程序員是一個新手。
會考慮如何滿足客戶,同時會為客戶更進一步思考的程序員是一個合格的程序員。
工作3年內還是一個新手不可怕,可怕的是工作了幾年還是處于一個學生狀態,如何把設計從簡單做復雜,然后再從復雜作到簡單,其實才考驗一個人實際的工作能力,在合時的環境采用合適的方法,得出最簡化方案是程序員應該追求的。引用我小時候讀書老師常常灌輸我的一句話:“書是先要讀厚來,然后再讀薄的。“