環境簡述
要說清楚問題,先要簡單說下生產環境的網絡拓撲(畢竟是個網絡問題對吧)
看,挺簡單的對吧,一個OpenResty做SLB承受客戶端請求,反響代理到幾台應用服務器。由於業務要求,必須要同步調用
第三方運營商的接口並返回結果到客戶端。
怎么”掛“了
深夜接到某妹子電話本該是激動人心的事,但是奈何怎么都高興不起來,因為來電是來告訴我環境掛了。趕緊問清楚,回答說是一開始響應很慢,后來就徹底拿不到數據了。
好吧,自己摸出手機試下,果然.... 此時夜里11點。
第一反應(請注意:這里開始是我的排查思路)
從開始打開電腦到電腦點亮的時間里我已經想好了一個初步的排查檢查思路,既然是拿不到數據,哪有哪些可能呢?
- 是不是特例還是所有情況下的數據都獲取不到?
- 是不是網絡斷了?
- 是不是服務停了?
- 是不是應用服務器都CPU 100%了?
- 看看監控系統有沒有報警?
- 看看DB是不是被人刪了?
好,因為我們有雲監控,看了下
- SLB的心跳還活着,排除網絡問題
- 所有服務器CPU/Memory/IO 都還在正常沒有峰值
- 關鍵進程還在
- DB也還健在
開始檢查
既然初步排除上述的問題,那下一步基本就是SSH到服務器上去看情況了。 自然從網絡開始,這里要想說給很多在做或者即將做在線生產環境支持的小伙伴說的第一句話: “先聽聽操作系統的聲音,讓操作系統來告訴你問題在哪”。 不論是windows和Linux都提供了一堆小工具小命令,在過度依賴安裝第三方工具前請先看看是否操作系統自帶的工具已經不夠支撐你了。
好,第一個檢查就是本機的網絡連接:netstat -anop tcp
結果:
.......此處省略100多行.....
我擦,close_wait又讓我撞見了. 看了幾台應用服務器都是上百個close_wait. (加起來有近千個close_wait, 發財了)。
網上有太多文章描述這個東西了所以我不會展開去解釋,就濃縮成以下幾點,大家參考這圖理解
- close_wait 是TCP關閉連接過程中的一個正常狀態
- close_wait 只會發生在被動關閉鏈接的那一端(各位姑娘們,請不要把圖里的client/server和項目里的客戶端服務端混淆)
- close_wait 除非你殺進程,close.wait是不會自動消失的。當然不消失意味着占着資源呢,這里就是占的FD。
看到這里基本拿不到數據的原因之一找到了,大量的close_wait,我之前項目也見過有的開發見到這種情況的直覺反應就是重啟大法,其實也不能算這個做法有錯,畢竟這個服務當了,客戶瘋了,夜已深了,你想休息了。但,這樣真的對嗎?
“停” 先別急着重啟
如果你這時候重啟了,的確立竿見影解決了當前問題,但你卻失去真正解決問題的機會。這就是我想說的第二句話:保留一下現場,不是所有問題的根源都能從日志里找到的
。close_wait 絕對就是這類問題,如果你是一位有過類似經歷的開發或者DevOps,你到現在應該有了下面2個疑問:
- 為啥一台機器區區幾百個close_wait就導致不可繼續訪問?不合理啊,一台機器不是號稱最大可以開到65535個端口嗎?
- 為啥明明有多個服務器承載,卻幾乎同時出了close_wait?又為什么同時不能再服務?那要SLB還有啥用呢?
好,這也是我當時的問題,讓我們繼續往下分析:
1. 先理順出現close_wait的鏈接流向
前面說過close_wait 是關閉連接過程中的正常狀態,但是正常情況下close_wait的狀態很快就會轉換所以很難被捕捉到。所以如果你能發現大批量的close_wait基本可以確定是出問題了。那第一個要確定是自然是連接的流向,判斷依據很簡單(還是nnetstat -anop tcp
)
命令返回里有一欄Foreign Address,這個就是代表對方的IP地址,這個時候再結合上面那張TCP的握手圖,我們就知道是哪台機器和你連接着但是卻主動關閉了連接。
2. 根據項目數據請求流向還原可能的場景
知道了是哪台IP,那接下來就可以根據項目實際情況還原連接的場景。在我這里所有的close_wait都發生在和SLB的連接上。因此說明,是SLB主動關閉了連接但是多台應用服務器都沒有相應ack導致了close_wait。只是這樣夠嗎?明顯不夠。繼續SLB作為負載均衡,基本沒有業務邏輯,那它會主動關閉連接的場景有哪些?
- 進程退出(正常或非正常)
- TCP連接超時
這2個情況很好判斷而且大多數情況下是第二種(我遇見的也是),如果你還記得我文章一開始的環境結構圖,我想基本可以得出以下結論是:
由於調用第三方的請求API太慢而導致SLB這邊請求超時引起的SLB關閉了連接.
那解決方案也很容易就有了:
- 加大SLB到應用服務器的連接超時時間
- 在調用第三方的時候采用異步請求
完了嗎? (我怎么那么啰嗦。。。)
**“再等等” 還有問題沒被回答 **
-
為啥一台機器區區幾百個close_wait就導致不可繼續訪問?不合理啊,一台機器不是號稱最大可以開到65535個端口嗎?
-
為啥明明有多個服務器承載,卻幾乎同時出了close_wait? 又為什么同時不能再服務?那要SLB還有啥用呢
是啊,解釋了為什么出close_wait,但並不能解釋這2個問題。好吧,既然找到了第一層原因,就先重啟服務讓服務可以用吧。剩下的我們可以兩個簡單的原型代碼模擬一個。此時我的目光回到了我們用的Tornodo上面來,當你有問題解釋不了的時候,你還沒有發現真正的問題
Tornado是一個高性能異步非阻塞的HTTP 服務器(還不明白這個啥意思的可以看 “從韓梅梅和林濤的故事中,學習一下I/O模型 ” 這篇文章,生動!!!),其核心類就是IOLoop,默認都是用HttpServer單進程單線程的方式啟動 (Tornado的process.fork_processes 也支持多進程方式,但官方並不建議)。我們還是通過圖來大概說下
IOLoop干了啥:
-
維護每個listen socket注冊的fd;
-
當listen socket可讀時回調_handle_events處理客戶端請求,這里面的實現就是基於epoll 模型
好,現在我們知道:
-
Tornado是單進程啟動的服務,所以IOLoop也就一個實例在監聽並輪詢
-
IOLoop在監聽端口,當對應的fd ready時會回調注冊的handler去處理請求,這里的handler就是我們寫業務邏輯的RequestHandler
-
如果我們啟用了Tornado的 @tornado.gen.coroutine,那理論上一個請求很慢不會影響其他的請求,那一定是代碼什么地方錯了。
進而查看實現代碼,才真相大白,雖然我們用了 @tornado.gen.coroutine 和yield,但是在向第三方請求時用的是urllib2 庫。這是一個徹頭徹尾的同步庫,人家就不支持AIO(Tornado 有自己AsyncHTTPClient支持AIO).
由此讓我們來總結下原因:
-
Tornado是單進程啟動的服務,所以IOLoop也就一個實例在監聽並輪詢
-
Tornado在bind每個socket的時候有默認的鏈接隊列(也叫backlog)為128個
-
由於代碼錯誤,我們使用了同步庫urllib2 做第三方請求,導致訪問第三方的時候當前RequestHandler是同步的(yield不起作用),因此當IOLoop回調這個RequestHandler時會等待它返回
-
第三方接口真的不快!
最后來回答這兩個問題:
- 為啥一台機器區區幾百個clise_wait就導致不可繼續訪問?不合理啊,一台機器不是號稱最大可以打開65535個端口嗎?
回答:由於原因#4和#3所以導致整個IoLoop慢了,進而因為#2導致很多請求堆積,也就是說很多請求在被真正處理前已經在backlog里等了一會了。導致了SLB這端的鏈接批量的超時,同時又由於close_wait狀態不會自動消失,導式最終無法再這個端口上創建新的鏈接引起了停止服務。
- 為啥明明有多個服務器承載,卻幾乎同時出了close_wait?又為什么同時不能再服務?那要SLB還有啥用呢?
回答:有了上一個答案,結合SLB的特性,這個也就很好解釋。這就是所謂的洪水蔓延,當SLB發現下面的一個節點不可用會把請求routing到其他可用節點上,導致其他節點壓力增大。也猶豫相同原因,加速了其他節點出現clise_wait.