選單

這次效能最佳化, QPS 翻倍了

這次效能最佳化, QPS 翻倍了

前段時間我們的服務遇到了效能瓶頸,由於前期需求太急沒有注意這方面的最佳化,到了要還技術債的時候就非常痛苦了。

在很低的 QPS 壓力下伺服器 load 就能達到 10-20,CPU 使用率 60% 以上,而且在每次流量峰值時介面都會大量報錯,雖然使用了服務熔斷框架 Hystrix,但熔斷後服務卻遲遲不能恢復。每次變更上線更是提心吊膽,擔心會成為壓死駱駝的最後一根稻草,導致服務雪崩。

在需求終於緩下來後,leader 給我們定下目標,限我們在兩週內把服務效能問題徹底解決。近兩週的排查和梳理中,發現並解決了多個性能瓶頸,修改了系統熔斷方案,最終實現了服務能處理的 QPS 翻倍,能實現在極高 QPS(3-4倍)壓力下服務正常熔斷,且能在壓力降低後迅速恢復正常,以下是部分問題的排查和解決過程。

伺服器高CPU、高負載

首先要解決的問題就是服務導致伺服器整體負載高、CPU 高的問題。

我們的服務整體可以歸納為從某個儲存或遠端呼叫獲取到一批資料,然後就對這批資料進行各種花式變換,最後返回。由於資料變換的流程長、操作多,系統 CPU 高一些會正常,但平常情況下就 CPU us 50% 以上,還是有些誇張了。

我們都知道,可以使用 top 命令在伺服器上查詢系統內各個程序的 CPU 和記憶體佔用情況。可是 JVM 是 Java 應用的領地,想檢視 JVM 裡各個執行緒的資源佔用情況該用什麼工具呢?

jmc 是可以的,但使用它比較麻煩,要進行一系列設定。我們還有另一種選擇,就是使用 ,jtop 只是一個 jar 包,它的專案地址在 yujikiriki/jtop, 我們可以很方便地把它複製到伺服器上,獲取到 java 應用的 pid 後,使用  即可輸出 JVM 內部統計資訊。

jtop 會使用預設引數 打印出最耗 CPU 的 5 種執行緒棧。

形如:

透過觀察執行緒棧,我們可以找到要最佳化的程式碼點。

在我們的程式碼裡,發現了很多 json 序列化和反序列化和 Bean 複製耗 CPU 的點,之後透過程式碼最佳化,透過提升 Bean 的複用率,使用 PB 替代 json 等方式,大大降低了 CPU 壓力。

熔斷框架最佳化

服務熔斷框架上,我們選用了 Hystrix,雖然它已經宣佈不再維護,更推薦使用  和阿里開源的 sentinel,但由於部門內技術棧是 Hystrix,而且它也沒有明顯的短板,就接著用下去了。

先介紹一下基本情況,我們在控制器介面最外層和內層 RPC 呼叫處添加了 Hystrix 註解,隔離方式都是執行緒池模式,介面處超時時間設定為 1000ms,最大執行緒數是 2000,內部 RPC 呼叫的超時時間設定為 200ms,最大執行緒數是 500。

響應時間不正常

要解決的第一個問題是介面的響應時間不正常。在觀察介面的 access 日誌時,可以發現介面有耗時為 1200ms 的請求,有些甚至達到了 2000ms 以上。由於執行緒池模式下,Hystrix 會使用一個非同步執行緒去執行真正的業務邏輯,而主執行緒則一直在等待,一旦等待超時,主執行緒是可以立刻返回的。所以介面耗時超過超時時間,問題很可能發生在 Hystrix 框架層、Spring 框架層或系統層。

這時候可以對執行時執行緒棧來分析,我使用 jstack 打印出執行緒棧,並將多次列印的結果製作成火焰圖(參見 應用除錯工具-火焰圖)來觀察。

如上圖,可以看到很多執行緒都停在  處,這些執行緒都被鎖住了,向下看來源發現是 , 再向下就是我們的業務程式碼了。

Hystrix 註釋裡解釋這些 TimerListener 是 HystrixCommand 用來處理非同步執行緒超時的,它們會在呼叫超時時執行,將超時結果返回。而在呼叫量大時,設定這些 TimerListener 就會因為鎖而阻塞,進而導致介面設定的超時時間不生效。

接著排查呼叫量為什麼 TimerListener 特別多。

由於服務在多個地方依賴同一個 RPC 返回值,平均一次介面響應會獲取同樣的值 3-5 次,所以介面內對這個 RPC 的返回值添加了 LocalCache。排查程式碼發現 HystrixCommand 被新增在了 LocalCache 的 get 方法上,所以單機 QPS 1000 時,會透過 Hystrix 呼叫方法 3000-5000 次,進而產生大量的 Hystrix TimerListener。

程式碼類似於:

修改程式碼,將 HystrixCommand 修改到 localCache 的 load 方法上來解決這個問題。此外為了進一步降低 Hystrix 框架對效能的影響,將 Hystrix 的隔離策略改為了訊號量模式,之後介面的最大耗時就穩定了。而且由於方法都在主執行緒執行,少了 Hystrix 執行緒池維護和主執行緒與 Hystrix 執行緒的上下文切換,系統 CPU 使用率又有進一步下降。

但使用訊號量隔離模式也要注意一個問題:訊號量只能限制方法是否能夠進入執行,在方法返回後再判斷介面是否超時並對超時進行處理,而無法干預已經在執行的方法,這可能會導致有請求超時時,一直佔用一個訊號量,但框架卻無法處理。

服務隔離和降級

另一個問題是服務不能按照預期的方式進行服務降級和熔斷,我們認為流量在非常大的情況下應該會持續熔斷時,而 Hystrix 卻表現為偶爾熔斷。

最開始除錯 Hystrix 熔斷引數時,我們採用日誌觀察法,由於日誌被設定成非同步,看不到實時日誌,而且有大量的報錯資訊干擾,過程低效而不準確。後來引入 Hystrix 的視覺化介面後,才提升了除錯效率。

Hystrix 視覺化模式分為服務端和客戶端,服務端是我們要觀察的服務,需要在服務內引入  包並新增一個介面來輸出 Metrics 資訊,再啟動  客戶端並填入服務端地址即可。

透過類似上圖的視覺化介面,Hystrix 的整體狀態就展示得非常清楚了。

由於上文中的最佳化,介面的最大響應時間已經完全可控,可以透過嚴格限制介面方法的併發量來修改介面的熔斷策略了。假設我們能容忍的最大介面平均響應時間為 50ms,而服務能接受的最大 QPS 為 2000,那麼可以透過  得到適合的訊號量限制,如果被拒絕的錯誤數過多,可以再新增一些冗餘。

這樣,在流量突變時,就可以透過拒絕一部分請求來控制介面接受的總請求數,而在這些總請求裡,又嚴格限制了最大耗時,如果錯誤數過多,還可以透過熔斷來進行降級,多種策略同時進行,就能保證介面的平均響應時長了。

熔斷時高負載導致無法恢復

接下來就要解決介面熔斷時,服務負載持續升高,但在 QPS 壓力降低後服務遲遲無法恢復的問題。

在伺服器負載特別高時,使用各種工具來觀測服務內部狀態,結果都是不靠譜的,因為觀測一般都採用打點收集的方式,在觀察服務的同時已經改變了服務。例如使用 jtop 在高負載時檢視佔用 CPU 最高的執行緒時,獲取到的結果總是 JVM TI 相關的棧。

不過,觀察服務外部可以發現,這個時候會有大量的錯誤日誌輸出,往往在服務已經穩定好久了,還有之前的錯誤日誌在列印,延時的單位甚至以分鐘計。大量的錯誤日誌不僅造成 I/O 壓力,而且執行緒棧的獲取、日誌記憶體的分配都會增加伺服器壓力。而且服務早因為日誌量大改為了非同步日誌,這使得透過 I/O 阻塞執行緒的屏障也消失了。

之後修改服務內的日誌記錄點,在列印日誌時不再列印異常棧,再重寫 Spring 框架的 ExceptionHandler,徹底減少日誌量的輸出。結果符合預期,在錯誤量極大時,日誌輸出也被控制在正常範圍,這樣熔斷後,就不會再因為日誌給服務增加壓力,一旦 QPS 壓力下降,熔斷開關被關閉,服務很快就能恢復正常狀態。

Spring 資料繫結異常

另外,在檢視 jstack 輸出的執行緒棧時,還偶然發現了一種奇怪的棧。

jstack 的一次輸出中,可以看到多個執行緒的棧頂都停留在 Spring 的異常處理,但這時候也沒有日誌輸出,業務也沒有異常,跟進程式碼看了一下,Spring 竟然偷偷捕獲了異常且不做任何處理。

結合程式碼上下文再看,原來 Spring 在處理我們的控制器資料繫結,要處理的資料是我們的一個引數類 ApiContext。

控制器程式碼類似於:

按照正常的套路,我們應該為這個 ApiContext 類新增一個引數解析器(HandlerMethodArgumentResolver),這樣 Spring 會在解析這個引數時會呼叫這個引數解析器為方法生成一個對應型別的引數。可是如果沒有這麼一個引數解析器,Spring 會怎麼處理呢?

答案就是會使用上面的那段”奇怪”程式碼,先建立一個空的 ApiContext 類,並將所有的傳入引數依次嘗試 set 進這個類,如果 set 失敗了,就 catch 住異常繼續執行,而 set 成功後,就完成了 ApiContext 類內一個屬性的引數繫結。

而不幸的是,我們的介面上層會為我們統一傳過來三四十個引數,所以每次都會進行大量的”嘗試繫結”,造成的異常和異常處理就會導致大量的效能損失,在使用引數解析器解決這個問題後,介面效能竟然有近十分之一的提升。

小結

效能最佳化不是一朝一夕的事,把技術債都堆到最後一塊解決絕不是什麼好的選擇。平時多注意一些程式碼寫法,在使用黑科技時注意一下其實現有沒有什麼隱藏的坑才是正解,還可以進行定期的效能測試,及時發現並解決程式碼裡近期引入的不安定因素。

最近面試BAT,整理一份面試資料《

Java面試BATJ通關手冊

》,覆蓋了Java核心技術、JVM、Java併發、SSM、微服務、資料庫、資料結構等等。

文章有幫助的話,在看,轉發吧。

謝謝支援喲 (*^__^*)