後臺性能優化實踐實戰

近年來公司的業務數據量和用戶流量都呈現出了非常迅猛的增長趨勢,爲了解決歷史架構設計中的不足,應對諸多因素引發的風險並保證通天塔平臺的穩定運行,通天塔後端組專項成立了一個“通天塔後端技術優化組”,號召並鼓勵團隊每個人積極參與進來,全盤分析和梳理、技術設計和技術Review、形成技術優化需求、排期、自測和驗證效果、Code Review、制定灰度和上線計劃、上線、總結經驗並形成未來可借鑑的方法。

本文主要聚焦在總結公司性能優化的經驗,爲聚合服務的性能優化提供些參考。

優化效果

本次公司性能優化對比了優化前後的數據,約有10%~30%的提升:

優化原則

性能優化主要按下圖所示 “三步走” 。

通過測試收集性能指標數據,具體分析產生性能問題的原因,對症下藥解決性能問題。

從長期看,這樣的優化過程是持續進行、反覆迭代的。但是短期看,我們同樣需要一些階段性的里程碑來提升信心。

因此,在做公司性能優化時,我們制定了一些 優化標準 。例如:接口響應指標AVG、TP999提升10~20%,吞吐率提升(當時沒有定明確的量化值,主要考慮到響應提升間接也能帶來一定的吞吐率提升)。

並且,我們始終要堅持一些基本的 系統優化原則:

1.以不影響業務功能爲前提。保證優化前後邏輯的一致性,從理論到實踐,通過嚴格的論證和全面的測試。

2.注重優先級。優先考慮一些性價比高的優化,性價比太低的放到最後,甚至乾脆就不考慮了,節省研發成本。

3.奧卡姆剃刀原則。即簡單性原則。新產生的性能問題的原因很可能是最近的迭代造成的;非必要的功能、代碼,能不加就不加。

性能測試

測試是爲了收集性能數據,爲後續分析調優提供基礎。 公司在優化過程中,主要採取了兩類性能測試方法:

1.微基準測試:針對方法級別,主要使用JMH等工具檢測方法的吞吐和延遲。

2.宏基準測試:針對系統級別,主要使用forcebot(公司內部工具)壓測系統整體的性能(CPU、內存、IO、JVM、延遲、吞吐等等)。開源的ab,jmeter等性能測試工具也能達到類似的效果。

對於性能指標,目前我們主要採集瞭如下的一些指標:

  • CPU、內存、IO等系統指標

  • 接口延遲、QPS等

  • JVM堆、棧等指標採樣

  • 調用鏈延遲分析工具等

性能指標分析

收集到系統資源、JVM、應用等性能指標後,對各類性能指標的問題表現進行分析,簡單總結了以下一些點:

CPU高:

  • 計算任務重

  • 正則表達式回溯

  • GC頻繁

  • 上下文切換多

  • 大量異常填充棧信息

堆內存高:

  • 創建對象多

  • 內存泄漏

JVM吞吐低:

  • STW過多

磁盤IO高:

  • 日誌記錄影響

網絡IO高:

  • 傳輸數據量大

應用延遲高:

  • 串行IO設計影響

  • 上游尾延遲影響

在公司優化過程中,最有效的還是通過調用鏈分析工具,幫助我們理清複雜業務邏輯中的調用關係和方法級別的延遲,更針對性地專注於優化更高優先級、最顯著的性能問題。

調優策略

這裏主要介紹對問題表現的分析後進行的一些系統優化。

1

優化設計

設計層面的優化,需要我們對自身的應用架構足夠熟悉,縮小可能存在瓶頸的範圍。然後再借助壓測、調用鏈分析等工具,進一步確診問題點。

6.1.1 多任務RPC查詢調度編排優化

對於公司(存在大規模扇出依賴查詢的聚合服務也是類似)來說,有大量的上游依賴需要進行RPC調用來獲取外部數據,再結合系統內部數據進行聚合。爲了只關注我們的優化點,我們把這個過程簡化爲如下圖所示:

圖6.1.1.a:公司數據聚合過程

對於外部數據的獲取,我們可以使用線程池去做一次並行查詢。對於簡單的頁面來說這是一種非常理想的情況,但是實際情況往往復雜得多。當配置的頁面複雜度提高後,外部數據之間也會存在着依賴關係,依賴之間必須串行執行,一次並行查詢無法解決問題。如下示意圖展示了外部數據的依賴關係及我們優化前的設計:

圖6.1.1.b:優化前外部數據查詢示意圖

如上圖所示,優化前的設計,我們人爲根據外部數據的依賴關係,將RPC調用拆分爲多個並行階段,也是權衡了公司早期的活動複雜度、流量以及系統實現複雜度等因素的設計。這種設計的問題就是,第二階段的整體開始查詢時間依賴於第一階段最慢的那個查詢節點的結束時間,即使第二階段的查詢節點完全不依賴於第一階段最慢的那個查詢節點。通過調用鏈分析工具分析RPC調用的延遲和時間軸上的關係,也證實了我們這一觀點。因此我們進行了RPC調度的設計優化,基於Sirector並行任務編排組件(公司內部組件,可以理解爲類似於JDK的Fork/Join組件),根據頁面的樓層配置,動態構建出RPC查詢任務依賴的DAG圖,RPC調用自然地沿着DAG圖的方向執行,消除了人爲的階段劃分。

圖6.1.1.c:優化後外部數據查詢示意圖

優化編排後對接口響應的均值和TP999都有很大的貢獻。

6.1.2 單任務RPC查詢並行優化

通過調用鏈分析工具,我們也發現部分RPC查詢任務內部存在串行查詢的情況。以商品促銷信息查詢爲例,入參是一籮筐商品sku,分批串行查詢促銷信息。優化過程如下圖所示,在上游接口提供方可接受的並行度範圍內進行並行查詢。

圖6.1.2.a:RPC查詢並行優化示意圖

6.1.3 虛擬機減壓,冷數據優化

公司是一個高併發高流量的系統,使用緩存技術來緩存活動配置等內部數據。對於大流量的系統,使用緩存技術就無需多言了。但是,隨着業務的不斷增長,緩存的量也越來越大,這給JVM堆內存造成了很大的壓力。那麼是否真的所有數據都要緩存?是否可以進行冷熱數據的拆分呢?

其實,爲了滿足用戶個性化的需求,對於特定樓層(如小院feed流),公司預定義了一部分活動作爲個性化樓層的數據源。這類活動不會單獨對外投放,因此可以作爲冷數據從在線活動池中拆分出去,通過RPC查詢接口按需查詢這部分數據。

圖6.1.3.a:冷數據優化示意圖

如上圖所示,優化後減輕了虛擬機的內存壓力。

6.1.4 尾延遲優化

尾延遲優化主要參考了Jeff Dean的論文《The Tail At Scale》,採用發送hedged-request備份請求的方式,來解決扇出依賴較多,因集羣規模擴大後,上游小概率的長尾傳導至下游後放大產生長尾概率的問題,平滑尾延遲。只適用於上游長尾特徵明顯的情況。爲了對上游不造成太大影響,我們也啓用了限流策略,控制備份請求的發送量。

由於我們的RPC框架不支持請求取消,而且實現要注意的細節問題較多,目前處於小流量實驗階段。

圖6.1.4.a: 尾延遲優化示意圖

優化組件

組件層面的優化,主要做了一些組件的替換升級、參數調優,變更代價小,有較 高的性價比。

6.2.1 JVM優化

JVM優化主要是進行了從JDK1.6到JDK1.8的升級,默認的Parallel垃圾回收器吞吐優先,不太適合我們的應用場景,因此改成了響應優先的G1;

另外由於JVM的sychronized(即使業務代碼沒用到,很多框架也有用到)鎖優化在偏向鎖升級過程中需要進入全局安全點,會有很短的STW。考慮到公司應用的高併發場景,我們也關閉了偏向鎖來提高吞吐;

 

從部署環境的角度考慮,當前我們使用的JVM實現無法感知到自己處於Docker容器環境,獲取的CPU核數是物理機的核數,遠超分配給我們的容器核數。這導致了一些依賴於CPU資源限制推導的虛擬機默認參數會非常不合理。比如,JVM默認的GC線程數會偏大,在GC純本地計算階段會有大量無意義的線程切換影響吞吐,因此我們手動設置了GC線程數,向容器核數對齊。

6.2.2 日誌框架優化

結合線程棧和磁盤IO分析,發現之前的日誌框架(log4j/logback)都使用了同步模式。併發量大時鎖競爭激烈,上下文切換造成額外開銷;另外,同步模式下,磁盤較低的IOPS影響應用的響應時間。因此我們切換到了log4j2框架上來,並結合LMAX Disruptor,開啓了全異步的日誌模式,同時log4j2的對象複用機制減少了框架本身產生的內存垃圾,降低日誌框架造成尾延遲的概率,將日誌對應用的影響降到最低。

圖6.2.2.a:日誌框架優化示意圖

6.2.3 線程池優化

原先線程池設置有一些不合理的地方。比如:線程數設置過大,導致CPU切換開銷大,佔用內存資源;核心線程數與最大線程數之間差值較大、線程池沒有預熱,造成突發流量增大時,同時申請線程資源時響應時長產生明顯的抖動。

因此我們通過壓測去合理設定線程池大小,減小核心線程數與最大線程數之間的差值,有些直接設置爲相等,設置線程池預熱核心線程,改善上述問題。

3

優化代碼

代碼層面的優化,帶來的提升表現在更微觀的層面,我們可以通過JMH等工具做一些微基準測試來對比優化前後的效果。 對於高流量的應用,微觀層面的優化也可以帶來一定的吞吐提升。

6.3.1 深拷貝優化

爲了避免對共享資源造成污染,公司代碼中使用了深拷貝方式來保護共享資源。原先通過序列化-反序列化的方式實現,這種方式拷貝流程長、中間對象多,GC有額外的壓力。優化後我們使用Cloning深拷貝工具,直接進行對象到對象的拷貝,縮短了拷貝流程,同時也不要求POJO實現Cloneable接口,避免了使用原型模式過多的限制。

圖6.3.1.a:深拷貝優化示意圖

6.3.2 對象重用&無競爭

公司灰度功能採用了隨機灰度的策略,代碼中使用Random對象,這裏存在一些代碼重用和競爭的問題,優化過程如下圖所示

圖6.3.2.a 對象重用&無競爭優化示意圖 

6.3.3 其他代碼優化

雖然“不提倡過早優化”這句話老生常談,但並不意味着我們寫每一行代碼時可以很隨意,我們完全可以選擇更優的實現方式。各種編碼規約,設計模式其實就是前人總結出來的寶貴經驗,幫助我們更好地編碼實現。

其實,代碼層面可注意的優化點非常多,這裏只列出一些供參考:

  • 正則表達式使用獨佔模式

  • JIT優化內聯:短方法;儘量private/static/final修飾方法

  • 鎖優化:鎖分離;細粒度鎖;樂觀鎖;無鎖(不可變模型/ThreadLocal等)

  • 時間換空間:數據壓縮降低網絡IO;String常量池重用

  • 空間換時間:哈希表;緩存等

公司本輪的優化基本達到了預期目標。 總體而言,設計層面的優化難度較高,對於聚合應用而言效果最顯著; 組件、代碼層面的優化也有不錯的效果,但更多的需要通過微基準測試去驗證。 最後,也是我們覺得最重要的一點就是: 系統優化必須保證系統的穩定性和正確性,必須通過嚴格的驗證和測試,否則,寧可不優化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章