性能优化那些事
寫在之前的話,最近一年多來幾乎沒更新博客,更多的原因是自知資歷尚潛,要學習的東西太多,要接觸的東西也太多,沒有足夠的精力投入到博客上,或許有一天時機成熟會再提高更新頻率吧,但有一點不會變的是,學習的路上數十年如一日,我會一直堅持,爭取有更多的機會可以走出來,但前提是我有了足夠的深度和廣度。
謝謝大家的支持。
——————————————————————————————————————————————————————————-
從今年一月份開始,我們團隊陸續完成了郵件服務的架構升級。新平臺上線運行的過程中也發生了一系列的性能問題,即使很多看起來來微不足道的點也會讓整個系統運行得不是那么平穩,今天就將這段時間的問題以及解決方案統一整理下,希望能起到拋磚的作用,讓讀者在遇到類似問題的時候能多一個解決方案。
新平臺上線后第一版架構如下:
整個平臺的數據流程是:
這版架構上線后,我們遇到的第一個問題:數據庫讀寫壓力過大后影響整體服務穩定。
表現為:
經過分析后,我們做了如下優化:
這么做還有一個原因是經過測試,對于Redis的lpush命令來說每次Push1K大小的元素和每次Push20K的元素耗時沒有明顯增加。
因此,我們使用了EventDrieven模型將Push操作改成了定時+批量+異步的方式往Redis Push郵件任務,這版優化上線后數據庫主庫CPU利用率基本在5%以下。
總結:這次優化的經驗可以總結為:用異步縮短住業務流程 +用批量提高執行效率+數據庫讀寫分離分散讀寫壓力。
優化后的架構圖:
優化上線后,我們又遇到了第二個問題:JVM假死。
表現為:
堆信息大致如下(注意紅色標注的點):
如上兩圖,可以看到RecommendGoodsService 類占用了60%以上的內存空間,持有了34W個 “郵件任務對象”,非??梢?。
分析后發現制造平臺在生成“郵件任務對象”后使用了異步隊列的方式處理對象中的推薦商品業務,因為某個低級的BUG導致處理隊列的線程數只有5個,遠低于預期數量,?因此隊列長度劇增導致的堆內存不夠用,觸發JVM的頻繁GC,導致整個JVM大量時間停留在”stop the world ” 狀態,JVM響應變得非常慢,最終表現為JVM假死,接口處理延遲劇增。
總結:
相信很多團隊在使用線程池異步處理的時候都是使用的無界隊列存放Runnable任務的,此時一定要非常小心,無界意味著一旦生產線程快于消費線程,隊列將快速變長,這會帶來兩個非常不好的問題:
經過一段時間的運行,我們將JVM內存從2G調到了3G,于是我們又遇到了第三個問題:內存變大的煩惱
JVM內存調大后,我們的JVM的GC次數減少了非常多,運行一段時間后加上了很多新功能,為了提高處理效率和減少業務之間的耦合,我們做了很多異步化的處理。更多的異步化意味著更多的線程和隊列,如上述經驗,很多元素被移到了年老代去,內存越用越小,如果正好在業務量不是特別大時,整個堆會呈現一個“穩步上升”的態勢,下一步就是內存閥值的持續報警了。
所以,無界隊列的使用是需要非常小心的。
我們把郵件服務分為生產郵件和促銷郵件兩部分,代碼90%是復用的,但獨立部署,獨立的數據庫,促銷郵件上線后,我們又遇到了老問題:數據庫主庫壓力再次CPU100%
在經過生產郵件3個月的運行及優化后,我們對代碼做了少許的改動用于支持促銷郵件的發送,促銷的業務可以概括為:瞬間大量數據寫入,Checker每次需要掃描上百萬的數據,整個系統需要在大量待發送數據中維持一個較穩定的發送速率。上線后,數據庫又再次報出異常:
死鎖的問題,原因是這樣的:
條件1.如果有Transaction1需要對ABC記錄加鎖,已經對A,B記錄加了X鎖,此刻在嘗試對C記錄枷鎖。
條件2.如果此前Transaction2已經對C記錄加了獨占鎖,此刻需要對B記錄加X鎖。
就會產生dead lock。實際情況是:如果兩條update語句同一時刻既需要掃描ABC又需要掃描DCB,那么死鎖就出現了。
盡管Mysql做了優化,比如增加超時時間:innodb_lock_wait_timeout,超時后會自動釋放,釋放的結果是Transaction1和Transaction2全部Rollback(死鎖問題并沒有解決,如果不幸,下次執行還會重現)。再如果每個Transaction都是update數萬,數十萬的記錄(我們的業務就是),那事務的回滾代價就非常高了。
解決辦法很多,比如先select出來再做逐條做update,或者update加上一個limit限制每次的更新次數,同時避免兩個Transaction并發執行等等。我們選擇了第一種,因為我們的業務對于時間上要求并不高,可以“慢慢做”。
全表掃表的問題發生在Checker上,我們封裝了很多操作郵件任務的邏輯在不同的Checker中,比如:過期Checker,重置Checker,Redis Push Checker等等。他們負責將郵件任務更新為業務需要的各種狀態,大部分時候他們是并行執行的,會產生很多select請求。嚴重時,讀庫壓力基本維持在95%上長達數小時。
全表掃描99%的原因是因為select沒有使用索引,所以往往開發同學的第一反應是加索引,然后讓數據庫“死扛”讀壓力 ,但索引是有成本的,占用硬盤空間不說,insert/delete操作都需要維護索引,
其實我們還有另外好幾種方案可以選擇,比如:是不是需要這么頻繁的執行select? 是不是每次都要select這么多數據?是不是需要同一時間并發執行?
我們的解決辦法是:合理利用索引+降低掃描頻率+掃描適量記錄。
首先,將Checker里的SQL統一化,每個Checker產生的SQL只有條件不同,使用的字段基本一樣,這樣可以很好的使用索引。
其次,我們發現發送端的消費能力是整個郵件發送流程的制約點,消費能力決定了某個時間內需要多少郵件發送任務,Checker掃描的量只要剛剛夠發送端滿負荷發送就可以了,因此,Checker不再每個幾分鐘掃表一次,只在隊列長度低于某個下限值時才掃描,
并且一次掃描到隊列的上限值,多一個都不掃。
經過以上優化后,促銷的庫也沒有再報警了。
直到兩周以前,我們又遇到了一個新問題:發送節點CPU100%.
這個問題的表象為:CPU正常執行業務時保持在80%以上,高峰時超過95%數小時。監控圖標如下:
在說這個問題前,先看下發送節點的線程模型:
Redis中根據目標郵箱的域名有一到多個Redis隊列,每個發送節點有一個跟目標郵箱相對應的FetchThread用于從Redis Pull郵件發送任務到發送節點本地,然后通過一個BlockingQueue將任務傳遞給DeliveryThread,DeliveryThread連接具體郵件服務商的服務器發送郵件??紤]到每次連接郵件服務商的服務器是一個相對耗時的過程,因此同一個域名的DeliveryThread有多個,是多線程并發執行的。
既然表象是CPU100%,根據這個線程模型,第一步懷疑是不是線程數太多,同一時間并發導致的。查看配置后發現線程數只有幾百個,同時一時間執行的只有十多個,是相對合理的,不應該是引起CPU100%的根因。
但是在檢查代碼時發現有這么一個業務場景:
那就意味著發送節點上的很多FetchThread執行的是不必要的喚醒->檢查->sleep的流程,白白的浪費CPU資源。
于是我們利用事件驅動的思想將模型稍稍改變一下:
每次FetchThread對應的Redis隊列為空時,將該線程阻塞到Checker上,由Checker統一對多個Redis隊列的Pull條件做判斷,符合Pull條件后再喚醒FetchThread。
Pull條件為:
1.FetchThread的本地隊列長度小于初始長度的一半。
2.Redis隊列不為空。
同時滿足以上兩個條件,視為可以喚醒對應的FetchThread。
以上的改造本質上還是在降低線程上下文切換的次數,將簡單工作歸一化,并將多路并發改為阻塞+事件驅動和降低拉取頻率,進一步減少線程占用CPU的時間片的機會。
上線后,發送節點的CPU占用率有了20%左右的下降,但是并沒有直接將CPU的利用率優化為非常理想的情況(20%以下),我們懷疑并沒有找到真正的原因。
于是我們接著對郵件發送流程做了進一步的梳理,發現了一個非常奇怪的地方,代碼如下:
我們在發送節點上使用了Handlebars做郵件內容的渲染,在初始化時使用了Concurrent相關的Map做模板的緩存,但是每次渲染前卻要重新new一個HandlebarUtil,那每個HandlebarUtil豈不是用的都是不同的TemplateCache對象?既然如此,為什么要用Concurrent(意味著線程安全)的Map?
進一步閱讀源碼后發現無論是Velocity還是Handlebars在渲染先都需要對模板做語法解析,構建抽象語法樹(AST),直至生成Template對象。構建的整個過程是相對消耗計算資源的,因此猜想Velocity或者Handlebars會對Template做緩存,只對同一個模板解析一次。
為了驗證猜想,可以把渲染的過程單獨運行下:
可以看到Handlebars的確可以對Template做了緩存,并且每次渲染前會優先去緩存中查找Template。而除了同樣執行5次,耗時開銷特別大以外,CPU的開銷也同樣特別大,上圖為使用了緩存CPU利用率,下圖為沒有使用到緩存的CPU利用率:
找到了原因,修改就比較簡單了保證handlebars對象是單例的,能夠盡量使用緩存即可。
上線后結果如下:
至此,整個性能優化工作已經基本完成了,從每個案例的優化方案來看,有以下幾點經驗想和大家分享:
from:?http://www.importnew.com/22118.html
總結
- 上一篇: MyBatis(1):MyBatis入门
- 下一篇: Java性能优化指南,及唯品会的实战