Netflix 工程師分享:如何檢測與處理不健康的 JVM

Netflix的雲數據工程團隊運行著各種JVM應用,包括Cassandra、Elasticsearch等等。儘管大多數情況下集群用分配給它們的內存都能穩定運行,但有時“死亡查詢”或者數據存儲本身的錯誤會導致內存使用失控,可能觸發垃圾回收(GC)循環甚至JVM內存耗盡。


對這種情況我們用jvmkill進行了補救:jvmkill是一種使用JVMTI API的代理,在JVM進程中運行。當JVM內存不足或無法生成線程時,jvmkill會介入並殺死整個進程。我們把jvmkill與Hotspot -XX:HeapDumpOnOutOfMemoryError標誌一起使用,以便事後通過堆分析瞭解為什麼會造成資源耗盡。對於應用程序來說,這種處理很合適:JVM內存不足時會無法響應,一旦jvmkill介入systemd會重啟失敗的進程。


即使有了jvmkill保護,我們仍然會遇到JVM問題,大部分是內存不足造成的問題,當然也不全是如此。這些Java進程反覆執行GC,但在暫停期間幾乎沒有做任何有用的工作。由於JVM並沒有耗盡100%資源,因而jvmkill不會發現。另一方面,我們的客戶會很快注意到自己的數據存儲節點吞吐量下降了近四個數量級。


為了說明這種情況,我們對Cassandra多次加載整個數據集到內存中,以此演示針對Cassandra JVM¹的“死亡查詢”:


然後通過jstat和GC日誌觀察確認機器確實處於GC死亡螺旋中:


cqlsh> PAGING OFF
Disabled Query paging.
cqlsh> SELECT * FROM large_ks.large_table;
OperationTimedOut: errors={}, last_host=some host
cqlsh> SELECT * FROM large_ks.large_table;
Warning: schema version mismatch detected, which might be caused by DOWN nodes; if this is not the case, check the schema versions of your nodes in system.local and system.peers.
Schema metadata was not refreshed.See log for details.


$ sudo -u cassandra jstat -gcutil $(pgrep -f Cassandra) 100ms
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 100.00 100.00 100.00 97.96 95.10 21 8.678 11 140.498 149.176
0.00 100.00 100.00 100.00 97.96 95.10 21 8.678 11 140.498 149.176
0.00 100.00 100.00 100.00 97.96 95.10 21 8.678 11 140.498 149.176


從GC日誌中可以清楚地看到20秒以上的暫停重複出現,並且可以使用GCViewer工具用圖形化方式分析日誌數據:


Netflix 工程師分享:如何檢測與處理不健康的 JVM


顯然,這種情況下JVM無法滿足性能要求,而且恢復的可能性很小。這種死亡螺旋會一直持續,直到值班工程師把受影響的JVM幹掉為止。在分頁出現太多次以後,我們認為這個問題:


  1. 很容易識別;
  2. 有一個簡單的解決方案;
  3. 最好快速干預


換句話說,在人工干預前讓機器提前識別處理。


解決方案:主動識別並Kill問題JVM


我們真的很喜歡jvmkill,研究瞭如何擴展jvmkill滿足實際需求。jvmkill對JVMTI ResourceExhausted回調進行了hook。當JVM判定自身資源耗盡時,會向有問題的JVM發送SIGKILL。不幸的是,這種分類過於簡單,不能很好地處理故障模式的灰色地帶。在這種情況下,JVM耗費了大量時間進行垃圾收集,但是資源還沒有耗盡。我們還調研了JVM提供的各種選項,例如GCHeapFreeLimit、GCTimeLimit、OnOutOfMemoryError、ExitOnOutOfMemoryError和CrashOnOutOfMemoryError。最後發現,這些JVM選項要麼無法在所有JVM和垃圾收集器上表現一致,要麼調整困難或者難於理解,要麼根本無法在各種邊界情況下使用。由於調整JVM現有的ResourceExhausted classifier是一項幾乎不可能完成的任務,因此決定自己構建。


解決方案開始於jvmquake睡前思考:“這個問題到底有多難?”首先想到的是,對於任何工作負載,JVM都應該把大部分時間用來運行程序代碼而不是GC暫停。如果程序執行時間佔比長期低於某個水平,那麼這個JVM顯然是不健康的,應該把它Kill掉。


我們通過把JVM GC暫停時間建模成“債務”來實現。如果JVM花了200毫秒進行GC,將增加200毫秒的債務計數。運行程序代碼耗費的時間“償還”積累的債務,直到債務為零時停止。因此,如果上面的程序運行≥200毫秒,那麼債務計數器歸零。如果JVM花在運行上的時間與GC時間相比超過1:1(即吞吐量>50%),則債務將趨近於零。另一方面,如果吞吐量不到50%,其債務將趨近於無限。這種“債務計數器”方法類似於漏斗算法,用來跟蹤程序的吞吐量。GC時間可以看作往漏斗里加水,應用程序運行時間看作水從漏斗裡流出:


Netflix 工程師分享:如何檢測與處理不健康的 JVM


加入JVM債務計數器後,我們對JVM的健康狀況更有信心,最終對那些不健康的JVM採取措施。例如,實際使用jvmquake後的GC螺旋可能看起來像這樣:


Netflix 工程師分享:如何檢測與處理不健康的 JVM


如果把jvmquake附加在該JVM上,將在虛線處停止。


我們確定了一個可調整的閾值,默認30秒:如果JVM完成GC時債務計數器超過30秒,jvmquake將終止該進程。通過對GarbageCollectionStartGarbageCollectionFinish。JVMTI回調添加hook可以測量這些值。


除了債務閾值外,我們還添加了兩個可調參數:


  • runtime_weight:作為運行程序代碼時間的權重,以便實現除1:1(吞吐量50%)之外的目標。例如,runtime_weight設為2表示調整目標是1:2(吞吐量33%)。一般情況下,runtime_weight設為x表示比率為1:x(吞吐量=100%/(x+1))。服務器中JVM運行時的吞吐量通常超過95%,因此最低50%吞吐量已經是相當保守了。
  • 採取行動:jvmkill只會向進程發送SIGKILL,但是jvmquake增加了讓JVM內存溢出(OOM)功能,並且支持在SIGKILL之前發送任意信號的功能。下一節將解釋為什麼可能需要執行這些操作。


應用jvmquake之後,對Cassandra節點運行相同的死亡查詢,現在可以看到:


Netflix 工程師分享:如何檢測與處理不健康的 JVM


和以前一樣,JVM開始進入GC死循環,但是這次jvmquake檢測到JVM累積了30倍的GC債務(運行時權重4:1)並停止了JVM。與其讓JVM一瘸一拐地運行,不如直接kill。


不要丟掉證據!


在使用jvmkill或手動終止JVM時,儘可能使用-XX:HeapDumpOnOutOfMemoryError或jmap收集堆轉儲文件。這些堆轉儲文件對於調試內存洩漏找到其根本原因至關重要。不幸的是,jvmquake向沒有遇到OutOfMemoryError的JVM發送SIGKILL時,這兩種方法都不起作用。解決辦法很簡單:jvmquake觸發時,會激活一個線程,在JVM堆上分配大數組造成內存溢出。這樣會觸發-XX:HeapDumpOnOutOfMemoryError,並最終Kill該進程。


但是,這裡有一個嚴重的問題:Java堆轉儲寫到磁盤中,如果反覆執行自動Kill進程操作,可能會把磁盤填滿。因此,我們開始研究生成操作系統core dump而不是JVM堆轉儲。我們發現,如果可以讓一個不健康的JVM發送SIGABRT而不是SIGKILL,那麼Linux內核將自動為我們生成一個core dump。這種方法很好,因為它是所有語言運行時的標準配置(包括node.js和Python),最重要的是它能讓我們蒐集大量core dump和堆轉儲並寫入管道,這樣就可以不需要額外的磁盤存儲。


Linux生成core dump時,默認會在崩潰的進程工作目錄中寫入一個名為“core”的文件。為了防止生成core文件導致磁盤空間不足的情況,Linux對core文件大小進行了限制(ulimit -c)。默認的限值為零,不生成core文件。但是,通過使用kernel.core_pattern sysctl,可以指定應用程序通過管道傳輸core dump(請參閱core手冊中“通過管道存儲core dump”)。按照這個接口,我們寫了一個腳本壓縮core文件上傳到S3,與崩潰程序相關的元數據也一併上傳。


數據流上傳完成後,systemd會重新啟動發生OOM的JVM。這是一種折衷:把core文件同步上傳到S3,不去考慮是否需要在本地存儲core文件。實際上,我們能夠在不到兩分鐘的時間內可靠地上傳16GB core dump。


告訴我出了什麼問題


已經捕獲core dump文件,現在可以進行分析找到問題的根源:是查詢的問題、硬件問題還是配置問題?大多數情況下,可以通過用到的類和它們的大小來確定。


我們的團隊已把jvmquake部署到我們所有Java數據存儲中。截至目前,已減輕了數十起事件(每次僅幾分鐘),並且提高了我們最重要的生產數據庫集群的可用性。此外,streaming core dump和脫機轉換工具讓我們能夠調試和修復Cassandra和Elasticsearch數據存儲產品中的複雜錯誤,以便應用程序需要的數據存儲保持“始終可用”。我們已經把許多補丁反饋給了社區,期待發現並解決更多問題。


腳註


¹Cassandra 2.1.19大約有20GiB數據和12GiB大小的堆。在實驗中,我們關閉了DynamicEndpointSnitch,確保查詢可以路由到本地副本,並且關閉分頁確保該節點將整個數據集保存在內存中。


分享到:


相關文章: