![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiclRnblN2XjlGcjcmbw5iYzgjZ5IGO3EDOyE2MhhDMkdTZ3gTZ2ATO5IjYwMWMx8CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
随着接口自動化監控的完善,線上大多數的接口都被自動化監控系統接入,是以開發、測試人員可以及時的了解API的健康情況,監控内容包括檢查API的狀态碼是否正常、傳回的内容斷言是否成功等,異常的接口會對外發出報警資訊,從某個時間周期開始我們發現了報警内容的一些異常情況,每天都會收到一些接口狀态碼為502的報警。
報警資訊由自動化監控系統發給釘釘報警群
根據報警的時間我們查了對應服務的日志,并沒有發現有任何的異常資訊,跟運維同學溝通後,在這個時間上運維那邊也并沒有對Tomcat、Nginx做相關操作,由于也沒有業務人員回報線上業務存在問題,于是502的問題暫時被擱置了起來,随着時間推移,502報警資訊每天還是不定期的如約而至,這讓報警群顯得有些“不安靜”,于是這個問題重新被重視起來,自動化監控的政策是每5分鐘跑一次接口list、不定期報出502說明在監控執行這段時間内服務可能會有問題,由于是5分鐘執行一次,是以每次502的周期、時間都不固定,這就解釋了為什麼報警時間、報警次數沒有任何規律。如果每天接口真的存在若幹次的502、那麼一定會影響到這個時間上的使用者體驗,而這樣的情況是不能夠被接受的。
Nginx 502錯誤的原因比較多,reload會造成502、代理伺服器無響應也會造成502,由于排除了運維對Nginx的操作,是以直接把目光放到了Tomcat上,由于Tomcat本身服務正常,并沒有報警,是以502的原因大機率是JVM的GC導緻的STW,于是我們找了JVM的監控資料。
監控資訊由OpenFalcon提供
當看到gc監控後第一反應:驚呆了!!!剛好每個小時Full GC一次,對比接口報警時間後發現,報警的時間剛好能夠跟gc時間對應上,Why???
從圖中可以看到每間隔一個小時就會執行一次FGC,FGC後old區使用記憶體降低,說明記憶體被回收了,但是在回收之前總使用記憶體隻有200多M,JVM設定的參數是3G,為什麼在200M是FGC了呢,Why???
JVM參數為 -Xms3072m -Xmx3072m -XX:PermSize=1536M -XX:MaxNewSize=1536m -MaxPermSize=1536m
帶着疑問我們先看下JVM在什麼情況下會執行FGC
- old區滿時
- young區存活對象複制到old區,old區記憶體不夠時
- 方法區滿時
- 調用System.gc()
FGC發生在old區使用了200M左右,排除1和2,方法區最大設定的1.5G,排除3,是以4是最大的嫌疑。
如果是因為調用System.gc()方法導緻的,那麼我隻需要禁用調System.gc(),然後在觀察gc的情況就可以了,JVM提供了禁用System.gc()的方法,在JVM啟動參數新增 -XX:DisableExplicitGC,然後重新開機Tomcat。
-XX:DisableExplicitGC
在JVM增加以上參數并重新開機
禁用System.gc()後的gc情況
過了一個半小時仍然沒有FGC,基本可以确認是System.gc()導緻的FGC,但是直接禁用會存在隐患,一些使用了堆外記憶體、或者某些架構依賴System.gc()禁用後會出現不可預知的問題,如果不能直接禁用System.gc,就需要找出是誰調用了System.gc方法,然後在優化。
查閱資料後發現Tomcat有個防止記憶體洩漏的JreMemoryLeakPreventionListener監聽器,會注冊一個定時器,每一個小時調用System.gc(),然而當我們反編譯我們所使用的Tomcat版本後發現,這個定時器的周期已經改為Long.valueOf(Long.MAX_VALUE - 1) 是以也就排除了這個原因。
Class<?> clazz = Class.forName("sun.misc.GC");
Method method = clazz.getDeclaredMethod(
"requestLatency",
new Class[] {long.class});
method.invoke(null, Long.valueOf(Long.MAX_VALUE - 1));
反射調用sun.misc.GC.requestLatency,傳入latency參數為Long.valueOf(Long.MAX_VALUE - 1)
繼續尋找…之後發現Java的RMI中也有每隔一個小時執行一次gc調用
- DGC:為RMI分布式垃圾收集(DGC)提供類和接口。 當RMI伺服器将對象傳回給其用戶端(遠端方法的調用者)時,它會跟蹤遠端對象在用戶端中的使用情況。 當用戶端上沒有對遠端對象的引用時,或者引用的“租約”到期且未續訂時,伺服器會收集遠端對象。
- sun.rmi.transport.DGCClient會注冊一個延時GC任務,會建立一個Daemon的線程,run方法中如果滿足間隔時間就會執行System.gc()方法
我們系統中并沒有單獨使用了RMI協定,于是馬上就想到了Dubbo,因為Dubbo是支援RMI協定的,而我們的RPC架構是使用的Dubbo。
Dubbo monitor中可以檢視服務注冊相關資訊
因曆史原因,在一些比較陳舊的系統中有使用到Dubbo的RMI協定,而所有新系統則都使用了Dubbo協定,為了驗證是因為Dubbo的RMI協定引發的FGC,我們把RMI的DGC的gc時間設定為2個小時
-Dsun.rmi.dgc.client.gcInterval=7200000 -Dsun.rmi.dgc.server.gcInterval=7200000
在jvm中加入以上參數并重新開機服務
從上圖可以看出,12點10分左右、13點10分左右間隔一小時觸發了GC,在加入jvm參數并重新開機服務後14點第一次GC後,在16點過了2個小時後觸發了第二次GC,由此确定問題是RMI的DGC導緻的。
為了确定将RMI協定改為Dubbo協定後問題可以解決,于是我們将DGC時間調整為5秒,并分别使用RMI協定和Dubbo協定來看FGC的次數變化
RMI協定
可以看到每5秒一次FGC
Dubbo協定
FGC次數沒有變化
問題已經定位出來了,接下來隻需要将RMI協定替換成Dubbo協定,然後重新開機所有相關服務即可,如何找出所有使用了RMI協定的服務呢?
打開Dubbo monitor,按下F12,在Console中輸入:
$('table').eq(1).find('tr').each(function(){
var app = $(this).find('td').eq(0).html();
$.ajax({url : 'http://DubboMonitorAddress/providers.html?application=' + app, success:function(data){
if(data.indexOf('rmi') != -1) {
console.log(app)
}
}})
});
先執行引入jquery,然後執行腳本,可以在console中看到所有使用了rmi協定的dubbo服務
在一段時間後,我們完成了所有服務的協定替換,之後在也沒有出現過502報警了。