接口耗時(shí)2000多秒!我人麻了!
接口耗時(shí)2000多秒!我人麻了!
前幾天早上,有個(gè)push服務(wù)不斷報(bào)警,報(bào)了很多次,每次都得運(yùn)維同學(xué)重啟服務(wù)來(lái)維持,因?yàn)檫@個(gè)是我負(fù)責(zé)的,所以我頓時(shí)緊張了起來(lái),匆忙來(lái)到公司,早飯也不吃了,趕緊排查!
1、 現(xiàn)象與排查步驟:
下面是下午時(shí)候幾次告警的截圖:
來(lái)看下圖。。。。接口超時(shí) 2000多秒。。。。我的心碎了?。?!人也麻了!??!腦瓜子嗡嗡的。。。
另外還總是報(bào)pod不健康、不可用 這些比較嚴(yán)重的警告!
我的第一反應(yīng)是調(diào)用方有群發(fā)操作,然后看了下接口的qps 貌似也不高呀!也就 9req/s, 之后我去 grafana 監(jiān)控平臺(tái) 觀察jvm信息發(fā)現(xiàn),線程數(shù)量一直往上漲,而且線程狀態(tài)是 WAITING
的也是一直漲。
如下是某一個(gè)pod的監(jiān)控:
為了觀察到底是哪個(gè)線程狀態(tài)一直在漲,我們點(diǎn)進(jìn)去看下詳情:
上圖可以看到 該pod的線程狀態(tài)圖 6種線程狀態(tài)全列出來(lái)了, 分別用不同顏色的線代表。而最高那個(gè)同時(shí)也是14點(diǎn)以后不斷遞增那個(gè)是藍(lán)線 代表的是 WAITING 狀態(tài)下的線程數(shù)量。
通過(guò)上圖現(xiàn)象,我大概知道了,肯定有線程是一直在wait
,無(wú)限wait下去
,后來(lái)我找運(yùn)維同學(xué) dump了線程文件,分析一波,來(lái)看看到底是哪個(gè)地方使線程進(jìn)入了wait ??。?!
如下 是dump下來(lái)的線程文件,可以看到搜索出427個(gè)WAITING
, 這也基本和 grafana 監(jiān)控中狀態(tài)是WAITTING的線程數(shù)量一致
重點(diǎn)來(lái)了(這個(gè) WAITING 狀態(tài)的堆棧信息,還都是從 IOSPushStrategy#pushMsgWithIOS
這個(gè)方法的某個(gè)地方出來(lái)的(151行出來(lái)的)),于是我們找到代碼來(lái)看看,是哪個(gè)小鬼在作怪?
而類(lèi) PushNotificationFuture 繼承了 CompletableFuture,他自己又沒(méi)有g(shù)et方法,所以本質(zhì)上 就是調(diào)用的 CompletableFuture的 get 方法。
ps:提一嘴,我們這里的場(chǎng)景是 等待ios push服務(wù)器的結(jié)果,不知道啥情況,今天(指發(fā)生故障的那天)ios push服務(wù)器(域名: api.push.apple.com )一直沒(méi)返回,所以就導(dǎo)致
一直等待下去了
。。
看到這, ?我 豁然開(kāi)朗 && 真相大白 ?了,原來(lái)是在使用 CompletableFuture
的 get
時(shí)候,沒(méi)設(shè)置超時(shí)時(shí)間,這樣的話會(huì)導(dǎo)致一直在等結(jié)果。。。(但代碼不是我寫(xiě)的,因?yàn)槲抑?CompletableFuture 的get不設(shè)置參數(shù)會(huì)一直等下去 ,我只是維護(hù),后期也沒(méi)怎么修改這塊的邏輯,哎 ,說(shuō)多了都是淚呀?。?/p>
好一個(gè) CompletableFuture#get();
(真是 死等啊。。。一點(diǎn)不含糊的等待下去,等的天荒地老??菔癄€也要等下去~~~ )
到此,問(wèn)題的原因找到了。
2、 修復(fù)問(wèn)題
解決辦法很簡(jiǎn)單,給CompletableFuture的get操作 加上超時(shí)時(shí)間即可,如下代碼即可修復(fù):
在修復(fù)后,截止到今天(6月8號(hào))沒(méi)有這種報(bào)警情況了,而且線程數(shù)和WAITING線程都比較穩(wěn)定,都在正常范圍內(nèi),如下截圖(一共4個(gè)pod):
至此問(wèn)題解決了~~~ 終于可以睡個(gè)好覺(jué)啦!
3、 復(fù)盤(pán)總結(jié)
3.1、 代碼淺析
既然此次的罪魁禍?zhǔn)资??CompletableFuture的get方法 ?那么我們就淺析一下 :
首先看下
get();
方法
上邊可以看到 不帶參數(shù)的get方法: if(deadLine==0) 成立
,也就是最終調(diào)用了LockSupport的park(this);方法,而這個(gè)方法最終調(diào)了unsafe的這個(gè)方法-> unsafe.park(false, 0L); 其中第二個(gè)參數(shù)就是等待多長(zhǎng)時(shí)間后,unpark即喚醒被掛起的線程,而0 則代表無(wú)限期等待。
再來(lái)看下
get(long timeOut,TimeUnit unit);
方法
我們可以看到 帶參數(shù)的get方法:
if(deadLine==0) 不成立,走的else邏輯
?也就是最終調(diào)用了LockSupport
的parkNanos(this,nanos);
方法,而這個(gè)方法最終調(diào)了unsafe
的這個(gè)方法->unsafe.park(false, nanos);
其中第二個(gè)參數(shù)就是你調(diào)用get時(shí),傳入的tiimeOut參數(shù)(只不過(guò)底層轉(zhuǎn)成納秒了)我們跑了下程序,發(fā)現(xiàn)超過(guò)指定時(shí)間后,
java復(fù)制代碼get(long timeOut,TimeUnit unit);
方法拋出 TimeoutException異常,而至于超時(shí)后我們開(kāi)發(fā)者怎么處理,就在于具體情況了。Exception in thread "main" java.util.concurrent.TimeoutException ?? ? ? ?at java.base/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1886) ?? ? ? ?at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2021)
而在 我的另一篇文章 萬(wàn)字長(zhǎng)文分析synchroized 這篇文章中,我們其實(shí)深入過(guò)openjdk
的源碼,見(jiàn)識(shí)過(guò)park
和unpark
操作,我們截個(gè)圖回憶一下:
3.2、最后總結(jié):
1.在調(diào)用外部接口時(shí)。一定要注意設(shè)置超時(shí)時(shí)間,防止三方服務(wù)出問(wèn)題后影響自身服務(wù)。 ?
2.以后無(wú)論看到什么類(lèi)型的Future,都要謹(jǐn)慎,因?yàn)檫@玩意說(shuō)的是異步,但是調(diào)用get方法時(shí),他本質(zhì)上是同步等待,所以必須給他設(shè)置個(gè)超時(shí)時(shí)間,否則他啥時(shí)候能返回結(jié)果,就得看天意了!
3.凡是和第三方對(duì)接的東西,都要做最壞的打算,快速失敗的方式很有必要。
4.遇到天大的問(wèn)題,都盡可能保持冷靜,不要亂了陣腳!