SRE高延遲問題的罪魁禍首System.gc()

01 案例一:

某日,支付平臺的開發人員找到SRE,需要SRE幫助解決一個棘手的問題。他們發現一個調用第三方支付接口的應用裏面,偶爾出現請求超時的情況。第三方平臺保證他們的服務99%10秒內完成,算上網絡傳輸時間,15秒足夠了,儘管支付平臺設置的超時時間是30秒,但還是發生了超時的情況。

從最近一週日誌的數據來看,大概每天出現15~40次超時問題。發生的時間也沒有什麼大致規律,有時同時出現2~3次,有時過2個小時才又出現一次。儘管客戶只要重試幾乎全部成功,並且這種請求錯誤出現的次數相對於總請求數比例極低,可這確實影響部分用戶的支付體驗。

出現的總數量雖相對不高,但影響用戶體驗的問題必須是大問題

支付平臺的小夥伴先是和這個外部支付公司聯繫,根據我們提供的業務ID,該公司確認他們那邊都是很快完成了支付,不存在超時的現象

那麼還有一個可能是中間的網絡問題,網絡先是通過我們的內網,然後是互聯網,最後是他們的內網,這中間任何一步都有可能出現網絡的抖動,導致我們的業務超時。

可是爲什麼又每天出現這麼多次呢? 因爲網絡問題涉及外網,並沒有那麼容易查清楚,並且每天分佈的時間點並沒有什麼規律,所以支付平臺的小夥伴轉而找到SRE尋求幫助。

SRE拿到這個問題之後,也感覺很棘手。這種超時問題,我們一般分爲三段:

首先是服務提供者,它的處理時間變長,直接導致超時,這個問題中第三方已經確認他們並沒有超時。
其次是中間的網絡傳輸,這個問題中我們又跨越3段網絡:我們的內網,互聯網和服務提供商的網絡。任何一段出問題,都會導致問題超時,並且這裏面有很多是我們很難拿到數據去診斷的。
再次是服務客戶端,我們的代碼裏面偶爾也會出現使程序變慢的地方,但這也是我們最不容易去懷疑的地方,因爲它是我們自己寫的代碼。

鑑於每天出現的數量和網絡的不確定性,SRE決定先排除我們這端代碼的問題。這個應用有幾十臺服務器分佈在三個數據中心,每個數據中心的服務器都出現過這種超時問題。平時這個業務都是在5秒左右完成,但發生超時的時候等待30秒都無法完成。

SRE先是找到一個具體的錯誤日誌,然後觀察了那臺服務器的各種監控指標,發現那臺服務器的GC開銷在出錯的時間點左右有明顯增高,初步懷疑是GC導致的變慢

繼而查看當時的verbose GC日誌,確認那個時間點確實有Full GC發生,並且Full GC時間超過30秒。不過發生的時間點上堆內存和永久代內存都有足夠空閒,於是懷疑是System.gc()導致的問題

又查看其它幾個出錯的例子,發現一樣的問題。查看更長時間的verbose GC 日誌,發現這種Full GC對於每一臺機器都是每隔24小時出現一次。與服務啓動成功時間對比,發現第一次發生的時間點正是服務剛啓動之後的時間點,之後每天這個時間點再來一次。

開發人員搜索並確認了他們的代碼中並沒有明確的System.gc()調用,所以只能懷疑某個依賴庫中存在這個調用。雖然確認的過程稍顯複雜,但是在JVM啓動參數中添加-XX:+DisableExplicitGC後終於徹底解決了這個問題

02 案例二:

某日,監控平臺上突然跳出一個新的報警,一個應用的CPU使用率突然達到了60%以上。SRE查看這個應用的歷史記錄,發現之前它的CPU使用率基本在5%以下。智能的雲監控平臺已經檢測到這個問題,並且已經嘗試通過替換新機器的方式幫它去修復了。

SRE並沒有發現這個應用最近有什麼新的代碼部署,也沒有發現最近有新的請求URL進入。聯繫開發人員,他們說可能上游最近有變動,發過來的請求內部有不同的參數,導致可能走不同的代碼路徑。

查看這個應用的監控指標數據,發現這個應用的GC開銷明顯提高,是GC導致JVM CPU開銷增加。於是SRE查看了它的verbose GC日誌,具體如圖1所示:

圖1(點擊可查看大圖)

我們看到堆空間有大量空閒,卻頻繁地在做Full GC。從圖2的原始日誌可以看到,永久代也有大量空閒內存。

圖2(點擊可查看大圖)

於是我們通過 jstat 命令查看了做Full GC的原因:

圖3(點擊可查看大圖)

原來也是System.gc()方法導致的。也就是說新的代碼路徑裏面有System.gc()的調用。和上面的問題一樣,加上**-XX:+DisableExplicitGC參數**後就修復了這個問題。

03 如何排查及修復

上面兩個案例都是有人在代碼中明確調用System.gc()導致的問題,並且都通過添加JVM啓動參數-XX:+DisableExplicitGC得以解決

那麼如果真的想要從代碼層面修復這個問題,怎麼才能找到這段代碼呢?

通常,如果這個調用發生在我們可控的源代碼裏,基本通過全文搜索或者藉助IDE裏面的方法調用查詢,都能找到。

如果這個System.gc()調用在某個依賴包裏面,如何才能找出來呢,這經常是比較麻煩的一步。這裏介紹兩個方法:

  1. 如果這個應用可以本地調試,只要開啓調試(debug)模式,然後在System.gc()方法上設置斷點,那麼一旦運行到這一步,調試視圖就會立馬告訴你。
  2. 使用FindBugs工具,它能分析工程裏面的源代碼和依賴包,通過DM_GC這個規則就能查找出調用點。具體見圖4:

圖4(點擊可查看大圖)

04 System.gc()存在的必要性

System.gc()方法一旦被調用並被執行,它將是一次Full GC。Full GC對於絕大多數Hotspot JVM實現裏面已有的GC算法而言,將是一次相對比較長的全局停頓(stop-the-world)

在一些對時延要求比較高的服務上,這將造成一定程度的不能滿足SLA。甚至可能在瞬間因爲客戶端的超時,帶來更多不必要的重試請求。正常情況下,JVM都有自適應算法去判斷什麼時候需要做一次GC,並且盡力避免Full GC,所以大多數情況下,不需要編程人員調用System.gc()方法

這並非說這個方法沒有用處,在以下場景,這個方法能帶來一些有益的效果:

1. 服務啓動並初始化完成,在沒有提供服務之前調用System.gc()。這樣可以回收掉啓動過程中一些未來沒必要存在的對象,大幅提高可用堆的數量,並且把一些對象從年輕代直接移到了老年代,不再需要經過多次在Survival空間來回移動才最終移到老年代。
2. 在做heap dump之前。這樣回收掉一些已經沒有被引用的對象,減少heap中沒必要分析的對象。
3. 微觀測試(microbenchmark)之前。減小由於測試當中可能出現GC過程引起的測試結果不正確。

05 總結

System.gc()作爲Java提供的一個方法,在如今JVM已經很智能的情況下,大多數時候已經不再需要我們明確去調用它。一旦發生了延遲較長的情況,可以把這個方法的調用作爲一個懷疑的對象。通過一些具體的方法能夠找到調用的點並且修復這個問題。

本文轉載自公衆號eBay技術薈(ID:eBayTechRecruiting)

原文鏈接

https://mp.weixin.qq.com/s/Ivtfu4bH5vVd0KiAOonoCA

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