案發現場程序員
昨天晚上忽然短信收到 APM (即 Application Performance Management 的簡稱),咱們內部本身搭建了這樣一套系統來對應用的性能、可靠性進行線上的監控和預警的一種機制)大量告警數據庫
畫外音: 監控是一種很是重要的發現問題的手段,沒有的話必定要及時創建哦網絡
緊接着運維打來電話告知線上部署的四臺機器所有 OOM (out of memory, 內存不足),服務所有不可用,趕忙查看問題!負載均衡
問題排查運維
首先運維先重啓了機器,保證線上服務可用,而後再仔細地看了下線上的日誌,確實是由於 OOM 致使服務不可用
ide
第一時間想到 dump 當時的內存狀態,但因爲爲了讓線上儘快恢復服務,運維重啓了機器,致使沒法 dump 出事發時的內存。因此我又看了下咱們 APM 中對 JVM 的監控圖表性能
畫外音: 一種方式不行,嘗試另外的角度切入!再次強調,監控很是重要!完善的監控能還原當時的事發現場,方便定位問題。
網站
不看不知道,一看嚇一跳,從 16:00 開始應用中建立的線程竟然每時每刻都在上升,一直到 3w 左右,重啓後(藍色箭頭),線程也一直在不斷增加),正常狀況下的線程數是多少呢,600!問題找到了,應該是在下午 16:00 左右發了一段有問題的代碼,致使線程一直在建立,且建立的線程一直未消亡!查看發佈記錄,發現發佈記錄只有這麼一段可疑的代碼 diff:在 HttpClient 初始化的時候額外加了一個 evictExpiredConnections 配置
this
問題定位了,應該是就是這個配置致使的!(線程上升的時間點和發佈時間點徹底吻合!),因而先把這個新加的配置給幹掉上線,上線以後線程數果真恢復正常了。那 evictExpiredConnections 作了什麼致使線程數每時每刻在上升呢?這個配置又是爲了解決什麼問題而加上的呢?因而找到了相關同事來了解加這個配置的來龍去脈線程
還原事發通過
最近線上出現很多 NoHttpResponseException 的異常,那是什麼致使了這個異常呢?
在說這個問題以前咱們得先了解一下 http 的 keep-alive 機制。
先看下正常的一個 TCP 鏈接的生命週期
能夠看到每一個 TCP 鏈接都要通過 三次握手 創建鏈接後才能發送數據,要通過 四次揮手 才能斷開鏈接,若是每一個 TCP 鏈接在 server 返回 response 後都立馬斷開,則發起多個 HTTP 請求就要屢次建立斷開 TCP, 這在 Http 請求不少 的狀況下無疑是很耗性能的, 若是在 server 返回 response 不當即斷開 TCP 連接,而是 複用 這條連接進行下一次的 Http 請求,則無形中省略了不少建立 / 斷開 TCP 的開銷,性能上無疑會有很大提高。
以下圖示,左圖是不復用 TCP 發起多個 HTTP 請求的狀況,右圖是複用 TCP 的狀況,能夠看到發起三次 HTTP 請求,複用 TCP 的話能夠省去兩次創建 / 斷開 TCP 的開銷,理論上發起 一個應用只要啓一個 TCP 鏈接便可,其餘 HTTP 請求均可以複用這個 TCP 鏈接,這樣 n 次 HTTP 請求能夠省去 n-1 次建立 / 斷開 TCP 的開銷。這對性能的提高無疑是有巨大的幫助。
回過頭來看 keep-alive (又稱持久鏈接,鏈接複用)作的就是複用鏈接, 保證鏈接持久有效。
畫中音: Http 1.1 以後 keep-alive 才默認支持並開啓,不過目前大部分網站都用了 http 1.1 了,也就是說大部分都默認支持連接複用了
天下沒有免費的午飯,雖然 keep-alive 省去了不少沒必要要的握手/揮手操做,但因爲鏈接長期保活,若是一直沒有 http 請求的話,這條鏈接也就長期閒着了,會佔用系統資源,有時反而會比複用鏈接帶來更大的性能消耗。 因此咱們通常會爲 keep-alive 設置一個 timeout, 這樣若是鏈接在設置的 timeout 時間內一直處於空閒狀態(未發生任何數據傳輸),通過 timeout 時間後,鏈接就會釋放,就能節省系統開銷。
看起來給 keep-alive 加 timeout 是完美了,可是又引入了新的問題(一波已平,一波又起!),考慮以下狀況:
若是服務端關閉鏈接,發送 FIN 包(注:在設置的 timeout 時間內服務端若是一直未收到客戶端的請求,服務端會主動發起帶 Fin 標誌的請求以斷開鏈接釋放資源),在這個 FIN 包發送可是還未到達客戶端期間,客戶端若是繼續複用這個 TCP 鏈接發送 HTTP 請求報文的話,服務端會由於在四次揮手期間不接收報文而發送 RST 報文給客戶端,客戶端收到 RST 報文就會提示異常 (即 NoHttpResponseException )
咱們再用流程圖仔細梳理一下上述這種產生 NoHttpResponseException 的緣由,這樣能看得更明白一些
費了這麼大的功夫,咱們終於知道了產生 NoHttpResponseException 的緣由,那該怎麼解決呢,有兩種策略
重試,收到異常後,重試一兩次,因爲重試後客戶端會用有效的鏈接去請求,因此能夠避免這種狀況,不過一次要注意重試次數,避免引發雪崩!
設置一個定時線程,定時清理上述的閒置鏈接,能夠將這個定時時間設置爲 keep alive timeout 時間的一半以保證超時前回收。
evictExpiredConnections就是用的上述第二種策略,來看下官方用法使用說明
Makes this instance of HttpClient proactively evict idle connections from the connection pool using a background thread.
調用這個方法只會產生一個定時線程,那爲啥應用中線程會一直增長呢,由於咱們對每個請求都建立了一個 HttpClient! 這樣因爲每個 HttpClient 實例都會調用 evictExpiredConnections ,致使有多少請求都會建立多少個 定時線程!
還有一個問題,爲啥線上四臺機器幾乎同一時間點全掛呢?
由於因爲負載均衡,這四臺機器的權重是同樣的,硬件配置也同樣,收到的請求其實也能夠認爲是差很少的,這樣這四臺機器因爲建立 HttpClient 而生成的後臺線程也在同一時間達到最高點,而後同時 OOM。
解決問題
因此針對以上提到的問題,咱們首先把 HttpClient 改爲了單例,這樣保證服務啓動後只會有一個定時清理線程,另外咱們也讓運維針對應用的線程數作了監控,若是超過某個閾值直接告警,這樣能在應用 OOM 前及時發現處理。
畫外音:再次強調,監控至關重要,能把問題扼殺在搖籃裏!
總結
本文經過線上四臺機器同時 OOM 的現象,來詳細剖析產定位了產生問題的緣由,能夠看到咱們在應用某個庫時首先要對這個庫要有充分的了了解(上述 HttpClient 的建立不用單例顯然是個問題),其次必要的網絡知識仍是須要的,因此要成爲一個合格的程序員,不關對語言自己有所瞭解,還要對網絡,數據庫等也要有所涉獵,這些對排查問題以及性能調優等會有很是大的幫助,再次,完善的監控很是重要,經過觸發某個閾值提早告警,能夠將問題扼殺在搖籃裏!