震驚!線上4臺(tái)機(jī)器同時(shí)OOM,到底發(fā)生了什么?
昨天晚上突然短信收到 APM 大量告警。緊接著運(yùn)維打來(lái)電話告知線上部署的四臺(tái)機(jī)器全部 OOM (out of memory,內(nèi)存不足),服務(wù)全部不可用,趕緊查看問題!
圖片來(lái)自 Pexels
問題排查
首先運(yùn)維先重啟了機(jī)器,保證線上服務(wù)可用,然后再仔細(xì)地看了下線上的日志,確實(shí)是因?yàn)?OOM 導(dǎo)致服務(wù)不可用:
第一時(shí)間想到 Dump 當(dāng)時(shí)的內(nèi)存狀態(tài),但由于為了讓線上盡快恢復(fù)服務(wù),運(yùn)維重啟了機(jī)器,導(dǎo)致無(wú)法 Dump 出事發(fā)時(shí)的內(nèi)存。所以我又看了下我們 APM 中對(duì) JVM 的監(jiān)控圖表。
畫外音:一種方式不行,嘗試另外的角度切入!再次強(qiáng)調(diào),監(jiān)控非常重要!完善的監(jiān)控能還原當(dāng)時(shí)的事發(fā)現(xiàn)場(chǎng),方便定位問題。
不看不知道,一看嚇一跳,從 16:00 開始應(yīng)用中創(chuàng)建的線程居然每時(shí)每刻都在上升,一直到 3W 左右,重啟后(藍(lán)色箭頭),線程也一直在不斷增長(zhǎng),正常情況下的線程數(shù)是多少呢,600!
問題找到了,應(yīng)該是在下午 16:00 左右發(fā)了一段有問題的代碼,導(dǎo)致線程一直在創(chuàng)建,且創(chuàng)建的線程一直未消亡!
查看發(fā)布記錄,發(fā)現(xiàn)發(fā)布記錄只有這么一段可疑的代碼 diff:在 HttpClient 初始化的時(shí)候額外加了一個(gè) evictExpiredConnections 配置。
問題定位了,應(yīng)該就是這個(gè)配置導(dǎo)致的!(線程上升的時(shí)間點(diǎn)和發(fā)布時(shí)間點(diǎn)完全吻合!),于是先把這個(gè)新加的配置給干掉上線,上線之后線程數(shù)果然恢復(fù)正常了。
那 evictExpiredConnections 做了什么導(dǎo)致線程數(shù)每時(shí)每刻在上升呢?這個(gè)配置又是為了解決什么問題而加上的呢?于是找到了相關(guān)同事來(lái)了解加這個(gè)配置的前因后果。
還原事發(fā)經(jīng)過(guò)
最近線上出現(xiàn)不少 NoHttpResponseException 的異常,那是什么導(dǎo)致了這個(gè)異常呢?在說(shuō)這個(gè)問題之前我們得先了解一下 Http 的 keep-alive 機(jī)制。
先看下正常的一個(gè) TCP 連接的生命周期:
可以看到每個(gè) TCP 連接都要經(jīng)過(guò)三次握手建立連接后才能發(fā)送數(shù)據(jù),要經(jīng)過(guò)四次揮手才能斷開連接。
如果每個(gè) TCP 連接在 Server 返回 Response 后都立馬斷開,則發(fā)起多個(gè) Http 請(qǐng)求就要多次創(chuàng)建斷開 TCP,這在 Http 請(qǐng)求很多的情況下無(wú)疑是很耗性能的。
如果在 Server 返回 Response 不立即斷開 TCP 鏈接,而是復(fù)用這條鏈接進(jìn)行下一次的 Http 請(qǐng)求,則無(wú)形中省略了很多創(chuàng)建/斷開 TCP 的開銷,性能上無(wú)疑會(huì)有很大提升。
如下圖示,左圖是不復(fù)用 TCP 發(fā)起多個(gè) Http 請(qǐng)求的情況,右圖是復(fù)用 TCP 的情況。
可以看到發(fā)起三次 Http 請(qǐng)求,復(fù)用 TCP 的話可以省去兩次建立/斷開 TCP 的開銷。
理論上發(fā)起一個(gè)應(yīng)用只要啟一個(gè) TCP 連接即可,其他 Http 請(qǐng)求都可以復(fù)用這個(gè) TCP 連接,這樣 N 次 Http 請(qǐng)求可以省去 N-1 次創(chuàng)建 / 斷開 TCP 的開銷。這對(duì)性能的提升無(wú)疑是有巨大的幫助。
回過(guò)頭來(lái)看 keep-alive (又稱持久連接,連接復(fù)用)做的就是復(fù)用連接, 保證連接持久有效。
畫中音: Http 1.1 之后 keep-alive 才默認(rèn)支持并開啟,不過(guò)目前大部分網(wǎng)站都用了 HTTP 1.1 了,也就是說(shuō)大部分都默認(rèn)支持鏈接復(fù)用了。
天下沒有免費(fèi)的午餐 ,雖然 keep-alive 省去了很多不必要的握手/揮手操作,但由于連接長(zhǎng)期?;?,如果一直沒有 Http 請(qǐng)求的話,這條連接也就長(zhǎng)期閑著了,會(huì)占用系統(tǒng)資源,有時(shí)反而會(huì)比復(fù)用連接帶來(lái)更大的性能消耗。
所以我們一般會(huì)為 keep-alive 設(shè)置一個(gè) timeout,這樣如果連接在設(shè)置的 timeout 時(shí)間內(nèi)一直處于空閑狀態(tài)(未發(fā)生任何數(shù)據(jù)傳輸),經(jīng)過(guò) timeout 時(shí)間后,連接就會(huì)釋放,就能節(jié)省系統(tǒng)開銷。
看起來(lái)給 keep-alive 加 timeout 是完美了,但是又引入了新的問題(一波已平,一波又起)。
考慮如下情況:如果服務(wù)端關(guān)閉連接,發(fā)送 FIN 包(注:在設(shè)置的 timeout 時(shí)間內(nèi)服務(wù)端如果一直未收到客戶端的請(qǐng)求,服務(wù)端會(huì)主動(dòng)發(fā)起帶 FIN 標(biāo)志的請(qǐng)求以斷開連接釋放資源)。
在這個(gè) FIN 包發(fā)送但是還未到達(dá)客戶端期間,客戶端如果繼續(xù)復(fù)用這個(gè) TCP 連接發(fā)送 Http 請(qǐng)求報(bào)文的話,服務(wù)端會(huì)因?yàn)樵谒拇螕]手期間不接收?qǐng)?bào)文而發(fā)送 RST 報(bào)文給客戶端。
客戶端收到 RST 報(bào)文就會(huì)提示異常(即NoHttpResponseException)。
我們?cè)儆昧鞒虉D仔細(xì)梳理一下上述這種產(chǎn)生 NoHttpResponseException 的原因,這樣能看得更明白一些:
費(fèi)了這么大的功夫,我們終于知道了產(chǎn)生 NoHttpResponseException 的原因,那么該怎么解決呢?
有如下兩種策略:
- 重試,收到異常后,重試一兩次,由于重試后客戶端會(huì)用有效的連接去請(qǐng)求,所以可以避免這種情況,不過(guò)一次要注意重試次數(shù),避免引起雪崩!
- 設(shè)置一個(gè)定時(shí)線程,定時(shí)清理上述的閑置連接,可以將這個(gè)定時(shí)時(shí)間設(shè)置為 keep alive timeout 時(shí)間的一半以保證超時(shí)前回收。
evictExpiredConnections 就是用的上述第二種策略,來(lái)看下官方用法使用說(shuō)明:
Makes this instance of HttpClient proactively evict idle connections from the connection pool using a background thread.
調(diào)用這個(gè)方法只會(huì)產(chǎn)生一個(gè)定時(shí)線程,那為啥應(yīng)用中線程會(huì)一直增加呢?因?yàn)槲覀儗?duì)每一個(gè)請(qǐng)求都創(chuàng)建了一個(gè) HttpClient!
這樣由于每一個(gè) HttpClient 實(shí)例都會(huì)調(diào)用 evictExpiredConnections,導(dǎo)致有多少請(qǐng)求都會(huì)創(chuàng)建多少個(gè)定時(shí)線程!
還有一個(gè)問題,為啥線上四臺(tái)機(jī)器幾乎同一時(shí)間點(diǎn)全掛呢?因?yàn)橛捎谪?fù)載均衡,這四臺(tái)機(jī)器的權(quán)重是一樣的,硬件配置也一樣,收到的請(qǐng)求其實(shí)也可以認(rèn)為是差不多的。
這樣這四臺(tái)機(jī)器由于創(chuàng)建 HttpClient 而生成的后臺(tái)線程也在同一時(shí)間達(dá)到最高點(diǎn),然后同時(shí) OOM。
解決問題
所以針對(duì)以上提到的問題,我們首先把 HttpClient 改成了單例,這樣保證服務(wù)啟動(dòng)后只會(huì)有一個(gè)定時(shí)清理線程。
另外我們也讓運(yùn)維針對(duì)應(yīng)用的線程數(shù)做了監(jiān)控,如果超過(guò)某個(gè)閾值直接告警,這樣能在應(yīng)用 OOM 前及時(shí)發(fā)現(xiàn)處理。
畫外音:再次強(qiáng)調(diào),監(jiān)控相當(dāng)重要,能把問題扼殺在搖籃里!
總結(jié)
本文通過(guò)線上四臺(tái)機(jī)器同時(shí) OOM 的現(xiàn)象,來(lái)詳細(xì)剖析定位了產(chǎn)生問題的原因。
可以看到我們?cè)趹?yīng)用某個(gè)庫(kù)時(shí)首先要對(duì)這個(gè)庫(kù)要有充分的了解(上述 HttpClient 的創(chuàng)建不用單例顯然是個(gè)問題),其次必要的網(wǎng)絡(luò)知識(shí)還是需要的。
所以要成為一個(gè)合格的程序員,不光對(duì)語(yǔ)言本身有所了解,還要對(duì)網(wǎng)絡(luò),數(shù)據(jù)庫(kù)等也要有所涉獵,這些對(duì)排查問題以及性能調(diào)優(yōu)等會(huì)有非常大的幫助。
再次,完善的監(jiān)控非常重要,通過(guò)觸發(fā)某個(gè)閾值提前告警,可以將問題扼殺在搖籃里!