性能優化那些事兒(一)
作者 | 張錦程
性能優化是個恒久的話題,它伴隨著業務的一次次迭代,產品的一步步演進,它陪伴企業一步步走向壯大再走向衰敗,是我們面臨的不可回避的問題。就如同宇宙的遞增定律,一切都走向混亂走向無序,性能的劣化邊隨著企業的發展壯大,業務的膨脹,人員的流動,復雜度的提升,一定也最終走向不可收拾的一步。
我們沒法像消除吸血鬼一樣,性能的優化沒有銀彈可用,但不代表性能優化沒有共性可言,本文針對筆者做過的一些性能優化案例,嘗試總結下解決性能問題的常用手段,以及如何持續性地避免過快的遞增。
首先我們把性能優化分為兩種情況,第一種是在企業發展階段的平穩期產生的性能瓶頸,第二種是企業發展的臨界點產生的性能瓶頸,知道第二曲線原理的同學們可以嘗試對應到第二曲線上去,一種是在曲線內的性能優化,一種是跨越曲線的性能優化。
理論源自查爾斯·漢迪《第二曲線:跨越“S型曲線”的二次增長》
比如著名的C10K問題,和10年淘寶架構的演進基本都屬于第二種情況,這種情況很難通過業務代碼的優化或者簡單的架構調整就能解決性能問題,這種情況的性能優化一般在算法&理論的突破或者是架構哲學&語言的調整層面了。我們沒法在一條曲線上完成性能的突破,可以看到曲線后期的收益越來越小,我們必須跳躍到一個新的曲線上去,這就是為什么很多大企業會注重架構的演進,第一曲線和第二曲線重合的部分就是企業高層進行重要決策的時機,我們再看淘寶的架構演進很明顯是符合第二曲線原理的。
針對這種如同換血般的性能優化,評估的時候需要結合現有流量和指標的分析給出強有理的數學模型,來證實在當前架構模型上是否能承載未來一段時間的業務高速發展,這種預判需要有前瞻性,和對市場有準確的估計。一旦發現數學模型證實架構模型無法承載更多的業務增長,那就需要果斷的遷移到第二曲線上去,公司的前瞻性和戰略性在這個階段表現無遺。
我們很多的性能優化接觸更多的其實是第一種情況,我們需要在不打破現有架構的情況下,進行性能調優。我們繼續在這個場景下進行總結:
環境優化
所謂環境優化就是代碼執行環境的優化,就如同你的工作環境影響你工作效率一樣,程序的運行環境對性能影響也很大。舉個栗子,網卡中斷與CPU親和性,在Linux的網絡調優方面,如果你發現網絡流量上不去,那么有一個方面需要去查一下:網卡處理網絡請求的中斷是否被綁定到單個CPU(或者說跟處理其它中斷的是同一個CPU)。
這就是個典型的運行環境對性能的影響,你的服務會應為網卡中斷的原因導致性能下降的很厲害。當然環境的優化比較吃經驗,如果沒有經驗會比較難定位問題,但一些基礎的Linux優化常識還是得必備的,需要學會看各項指標,有足夠的敏銳力發現異常的指標,有足夠的經驗識別異常指標的誘因是什么。
輪子的優化和選擇
很多庫提供了非常便利的功能,但有些情況下這些便利的功能對性能不是很友好。準確來說很多輪子對開發而言是個黑盒,即使有源碼也鮮有人去一行行研究,往往很多性能問題就暴露在簡單的一句調用中。先說說簡單的,大家都知道的,HashTable和ConcurrentHashMap,都是并發安全的組件,但是性能上差別就大了,明顯用ConcurrentHashMap性能就會比HashTable好。
一樣的道理,ArrayBlockingQueue是JDK提供的同步堵塞隊列,很多場景下會用到這個組件,但是追求極致性能的情況下Disruptor是個更好的選擇。對于大量定時任務的調用,Netty的時間輪算法就是更為優秀的選擇。
對于輪子的性能選擇可以遵循下面的原則:無鎖設計普遍優于有鎖的設計,細粒度鎖優于粗粒度鎖,環形隊列的設計普遍優于無邊界隊列的設計。
萬惡的循環
一般爛代碼都出現在循環里,比如幾百次的REST請求,幾千次的SQL請求,手動找起來海底撈針,特別是很深的調用堆棧很難發現,這里需要利用工具,我們單獨去說,無論是基于語法樹還是字節碼的靜態檢測還是持續性能集成都能一定程度的預防這種情況的發生,經驗告訴我這里是優化成果的大頭,越復雜的項目這種問題越嚴重,解決方案是做批處理和小心的使用緩存。這里性能可視化和調用鏈分析可以幫助你快速定位問題。
鎖
鎖可以說性能優化的難點,一類鎖會牽扯到業務,優化的重心是如何合理的使用鎖,有沒有行成鎖的使用規范,鎖的粒度足夠細么?有可能的集中管理鎖,限制開發人員直接使用鎖。那另一類鎖一般在中間件那塊,屬于通用組件,和業務關系不大,但如果瓶頸在中間件那就得著手去優化了,最好的是實現無鎖模型。
緩存
緩存是能夠解決一些性能問題的,在某些場合是殺手锏的存在,但緩存需要注意的是時效性和生效范圍,控制好這2點一般緩存會帶來很大的收益。
線程池
線程池過大對性能也是有一定影響的,畢竟JAVA的線程是1:1的內核線程,解決方法是設置合適的線程池大小不要過于龐大,線程上下文切換的開銷可是不小的,或者干脆使用阿里的JDK開啟全局虛擬線程模式(黑科技)。
同步
同步一般會堵塞線程導致需要大量線程池,異步太難寫了,協程JAVA不支持,有條件的用阿里的JDK吧。
慎用Hibernate
為啥單獨說Hibernate,可能筆者有條件反射了,一般使用Hibernate的項目多多少少都對其用法有誤解,或者完全沉迷于它帶來的便利性而忽略了這些便利性帶來的性能問題。簡單的N+1問題經常在項目上遇到,復雜的級聯更新問題導致的性能問題也屢見不鮮,總之大家小心使用Hibernate。
GC
由于頻繁FullGC導致的性能問題也是很常見的,這塊有點大,可以說個幾天幾夜了,這里不細說。
還有些奇葩的優化點,比如緩存行失效,一般來說業務涉及不到,都是中間件基礎組件才有可能碰到的優化策略。
業務優化,業務優化往往會取得很喜人的成績,但這是一個取舍的問題,而且涉及到業務,小心謹慎,一般來說在性能優化的專項工作中盡量不去修改業務。
上面這些僅僅是一些性能優化心得,有不少是經驗很難總結全面,但有不少效果顯著的優化項可以通過模式去解決,那么如何發現代碼中的性能問題,快速識別出那些性能不友好的代碼呢?請聽下回分解。
下一篇文章:????《????性能優化那些事兒(二)》??