問題背景
在開發(fā)網(wǎng)絡(luò)服務(wù)器應(yīng)用系統(tǒng)的時(shí)候炸裆,有時(shí)會(huì)碰到服務(wù)器有大量的socket處于CLOSE_WAIT狀態(tài)娜搂,也無法關(guān)閉薛闪,導(dǎo)致服務(wù)器無法接受新的用戶請求辛馆,最終導(dǎo)致服務(wù)器奔潰,系統(tǒng)重啟才能解決豁延。
為什么會(huì)出現(xiàn)大量的CLOSE_WAIT狀態(tài)呢昙篙?
要解決這個(gè)問題,我們得先介紹一下socket斷開過程中的四次揮手术浪。
終止TCP連接的四次揮手
由于TCP連接是全雙工的瓢对,因此每個(gè)方向都必須單獨(dú)進(jìn)行關(guān)閉。
假設(shè)終止命令由client端發(fā)起胰苏。
當(dāng)clien端傳輸完成數(shù)據(jù)硕蛹,或者需要斷開連接時(shí):
- Client端發(fā)送一個(gè)FIN報(bào)文給Server端。(序號(hào)為M)
1.1. 表示要終止Client到Server這個(gè)方向的連接硕并。
1.1. 通過調(diào)用close(socket) API法焰。
1.3 表示Client不再會(huì)發(fā)送數(shù)據(jù)到Server端。(但Server還能繼續(xù)發(fā)給Client端)
1.4 Client狀態(tài)變?yōu)镕IN_WAIT_1 - Server端收到FIN后倔毙,發(fā)送一個(gè)ACK報(bào)文給Client端埃仪。(序號(hào)為M+1)
2.1 Server狀態(tài)變?yōu)镃LOSE_WAIT
2.2 Client收到序號(hào)為(M+1)的ACK后狀態(tài)變?yōu)镕IN_WAIT_2
。陕赃。卵蛉。 - Server端也發(fā)送一個(gè)FIN報(bào)文給Client端。(序號(hào)為N)
3.1 表示Server也要終止到Client端這個(gè)方向的連接么库。
3.2. 通過調(diào)用close(socket) API傻丝。
3.3 Server端狀態(tài)變?yōu)長AST_ACK - Client端收到報(bào)文FIN后,也發(fā)送一個(gè)ACK報(bào)文給服務(wù)器诉儒。(序號(hào)N+1)
4.1 Client狀態(tài)變?yōu)門IME_WAIT - Server端收到序號(hào)為(N+1)的ACK
5.1 Server的狀態(tài)變?yōu)镃LOSED. - 等帶2MSL之后
6.1 Client的狀態(tài)也變?yōu)镃LOSE. - 至此葡缰,一個(gè)完整的TCP連接就關(guān)閉了。
兩個(gè)基本問題:
- Q: 我們看到CLOSE_WAIT出現(xiàn)在什么時(shí)候呢忱反?
A: 在Sever端收到Client的FIN消息之后泛释。 - Q: 狀態(tài)CLOSE_WAIT在什么時(shí)候轉(zhuǎn)換成下一個(gè)狀態(tài)呢?
A: 在Server端向Client發(fā)送FIN消息之后温算。
至此似乎明白了為什么會(huì)出現(xiàn)CLOSE_WAIT的狀態(tài):如果Server端一直沒有向client端發(fā)送FIN消息(調(diào)用close() API)怜校,那么這個(gè)CLOSE_WAIT會(huì)一直存在下去。
原因分析
從上面我們看到出現(xiàn)CLOSE_WAIT注竿,說明Server端沒有發(fā)起close()操作韭畸,這基本上是用戶server端程序的問題了宇智;通常情況下蔓搞,Server都是等待Client訪問胰丁,如果Client退出請求關(guān)閉連接,server端自覺close()對(duì)應(yīng)的連接喂分。
當(dāng)然這也可能是業(yè)務(wù)實(shí)現(xiàn)上的需要锦庸,暫時(shí)不發(fā)送FIN,因?yàn)榉?wù)器可能還有數(shù)據(jù)要發(fā)往客戶端蒲祈,等發(fā)送完所有應(yīng)用數(shù)據(jù)最后再發(fā)送FIN消息了甘萧;這個(gè)場景并不是這里我們討論的大量COLSE_WAIT的問題了,因?yàn)檫@個(gè)還是可控的梆掸。
我們要討論的場景是什么扬卷?我們先介紹兩個(gè)系統(tǒng)調(diào)用,前面也提到并且用到的close(socket)和shutdown(socket,HOW)接著往下分析酸钦。
我們知道一個(gè)進(jìn)程打開一個(gè)socket怪得,然后此進(jìn)程fork出子進(jìn)程的時(shí)候,父進(jìn)程已打開的socket是會(huì)被繼承的卑硫,即子進(jìn)程能夠繼續(xù)訪問這個(gè)socket徒恋。其結(jié)果就是,一個(gè)socket被兩個(gè)進(jìn)程打開欢伏,一個(gè)父進(jìn)程和一個(gè)子進(jìn)程入挣,此時(shí)socket的引用計(jì)數(shù)會(huì)變成2。
- 調(diào)用close(socket)時(shí)硝拧,內(nèi)核先檢查socket上的引用計(jì)數(shù)器:如果引用計(jì)數(shù)大于1径筏,那么將這個(gè)引用計(jì)數(shù)減1,然后直接返回障陶。如果引用計(jì)數(shù)等于1滋恬,那么內(nèi)核才會(huì)真正關(guān)閉此socket。(通過發(fā)送FIN到對(duì)端來關(guān)閉TCP連接)
- 調(diào)用shutdown(socket咸这,HOW)時(shí)夷恍,內(nèi)核不會(huì)檢查此socket對(duì)應(yīng)的引用計(jì)數(shù)器,直接向?qū)Χ税l(fā)送FIN來關(guān)閉TCP連接媳维。
據(jù)此分析酿雪,很大可能性是用戶服務(wù)器的程序?qū)崿F(xiàn)有問題導(dǎo)致的大量CLOSE_WAIT的socket,比如父進(jìn)程打開了socket侄刽,然后通過fork出子進(jìn)程來處理業(yè)務(wù)指黎,父進(jìn)程繼續(xù)對(duì)網(wǎng)絡(luò)請求進(jìn)行監(jiān)聽,永遠(yuǎn)不會(huì)終止州丹;當(dāng)客戶端發(fā)FIN過來的時(shí)候醋安,處理業(yè)務(wù)的子進(jìn)程處理此FIN消息杂彭,調(diào)用close()對(duì)本端進(jìn)行關(guān)閉,然而這個(gè)close()調(diào)用只是把socket的引用計(jì)數(shù)器減1吓揪,因?yàn)楦高M(jìn)程還在運(yùn)行亲怠,socket并沒關(guān)閉,這樣就導(dǎo)致系統(tǒng)中又多了一個(gè)CLOSE_WAIT的socket柠辞,長此以往团秽,就這樣了。
關(guān)于TIME_WAIT狀態(tài)
多說兩句關(guān)于TIME_WAIT的狀態(tài)叭首,這個(gè)發(fā)生在client端习勤,而且是不可避免的,其時(shí)間長度是固定的2MSL焙格,到期自動(dòng)轉(zhuǎn)為CLOSED图毕,不會(huì)導(dǎo)致系統(tǒng)資源耗盡的問題。
MSL是一個(gè)系統(tǒng)級(jí)參數(shù)眷唉,可調(diào)予颤。