【服务器】一次对Close_Wait 状态故障的排查经历
最近接連聽說一臺(tái)線上服務(wù)器總是不響應(yīng)客戶端請(qǐng)求。
登錄服務(wù)器后查詢iis狀態(tài),發(fā)現(xiàn)應(yīng)用程序池狀態(tài)變?yōu)橐淹V埂?/p>
按經(jīng)驗(yàn)想,重啟后應(yīng)該就ok,第一次遇到也確實(shí)起了作用,當(dāng)時(shí)完全沒在意,以為是其他人無意把服務(wù)關(guān)閉了而已。
但是之后幾天幾乎每天都出現(xiàn)問題,應(yīng)用程序池再次成為 已停止 狀態(tài)。這個(gè)情況顯然有問題。于是開始排查設(shè)置。
?
線上環(huán)境很簡單,iis + API應(yīng)用,數(shù)據(jù)庫在內(nèi)網(wǎng)上,沒有反向代理。
?
出問題的應(yīng)用程序池承載了一個(gè)基礎(chǔ)數(shù)據(jù)API,查詢量極小,數(shù)據(jù)量也極小,只是因?yàn)橥伦鰧?shí)現(xiàn)時(shí)提過,查詢很麻煩,所以我讓他同時(shí)也檢查代碼的查詢是否過于繁瑣產(chǎn)生超出設(shè)置上限的資源占用。
但根據(jù)同事檢查結(jié)果,這個(gè)情況不存在,數(shù)據(jù)庫查詢響應(yīng)非常快,服務(wù)器的資源監(jiān)控上看相關(guān)進(jìn)程也沒有出現(xiàn)明顯的資源飆升。因此判斷服務(wù)端代碼應(yīng)該沒有問題。
?
經(jīng)驗(yàn)路線在這里就走不通了,只能嘗試在設(shè)置上找原因。 iis對(duì)應(yīng)用程序池的設(shè)置里最可能影響服務(wù)的是回收策略的設(shè)置。為了避免影響其他服務(wù),部署時(shí)設(shè)置了這個(gè)api的專用內(nèi)存上限到1GB。當(dāng)然,既然代碼檢查沒有問題,測(cè)試運(yùn)行也沒發(fā)現(xiàn)資源飆升的情況,回收的專用內(nèi)存上限應(yīng)該不會(huì)輕易觸發(fā)。但是還是檢查了設(shè)置,畢竟從前有同事設(shè)置時(shí)把1GB給成了100M,除了登錄之外執(zhí)行不了任何請(qǐng)求。
?
最終想到用netstat檢查端口情況。起初的懷疑是端口被其他進(jìn)程無意占用導(dǎo)致問題。
啟動(dòng)netstat后,讓測(cè)試發(fā)了幾次請(qǐng)求,順利看到進(jìn)程出現(xiàn)在列表,且沒有端口沖突,對(duì)端地址經(jīng)查證是來自app。當(dāng)然,也注意到服務(wù)器狀態(tài)是一堆 Close_Wait.
?
當(dāng)然我經(jīng)驗(yàn)少,Close_Wait還真是第一次見。不過既然狀態(tài)不是Established,這個(gè)應(yīng)該就是問題的原因了。開始查資料,學(xué)習(xí)Close_Wait狀態(tài)是什么情況。
?
對(duì)于 服務(wù)器的連接狀態(tài)而言,一般有三種比較常見的: Established、 Time_Wait、Close_Wait。?
從別人博客上扒了一張?jiān)韴D:
?
通過原理圖,我們知道了CLOSE_WAIT是被動(dòng)關(guān)閉的狀態(tài)。什么意思呢?比如客戶端發(fā)了個(gè)請(qǐng)求,正常情況下是會(huì)收到服務(wù)器響應(yīng)一個(gè)狀態(tài)的,即Response。當(dāng)客戶端讀取了這個(gè)返回后,會(huì)主動(dòng)告訴服務(wù)器收到了,關(guān)閉連接。
由于是客戶端發(fā)起關(guān)閉連接的請(qǐng)求,在TCP協(xié)議下雙方需要通過四個(gè)包的互發(fā)完成雙向確認(rèn)工作,才能最終關(guān)閉這個(gè)連接。
客戶端要求關(guān)閉,此時(shí)客戶端狀態(tài)為 FIN_WAIT_1,同時(shí)向服務(wù)器發(fā)送了 FIN 包,服務(wù)器狀態(tài)變更為CLOSE_WAIT;
當(dāng)然,服務(wù)器需要對(duì)收到FIN包向客戶端確認(rèn),于是服務(wù)器向客戶端發(fā)送了 ACK 包,客戶端因此變更狀態(tài)為FIN_WAIT_2;
服務(wù)器處理了這個(gè)確認(rèn)后,再次主動(dòng)向客戶端發(fā)送FIN包,同時(shí)自己狀態(tài)變更為LAST_ACK,收到來自服務(wù)器FIN包的客戶端也將自己狀態(tài)變更為TIME_WAIT;
最后一步,客戶端會(huì)對(duì)來自服務(wù)器的FIN包回復(fù)確認(rèn),服務(wù)器收到該ACK包后,將自己狀態(tài)置為CLOSED,如此,整個(gè)關(guān)閉過程結(jié)束。
簡單說就是,客戶端 -》 服務(wù)器,我要關(guān)閉,服務(wù)器回復(fù)OK,并開始處理后續(xù);服務(wù)器后續(xù)處理好后,再告訴客戶端我可以關(guān)閉了,客戶端確認(rèn),服務(wù)端關(guān)閉。
?
所以,出現(xiàn)CLOSE_WAIT狀態(tài)的原因是,服務(wù)器一端因故沒有向客戶端發(fā)出FIN包,即服務(wù)端的LAST_ACK -- FIN -->客戶端這步?jīng)]能執(zhí)行。
因此,看到CLOSE_WAIT狀態(tài)后,那么可以確定服務(wù)器沒有執(zhí)行后續(xù)動(dòng)作,即調(diào)用socket.Close()。
舉一反三,即誰被動(dòng)關(guān)閉,則誰的連接釋放代碼有問題。
?
但是socket.Close()的調(diào)用應(yīng)當(dāng)是由web服務(wù)器自己完成呀,所以問題還沒確定。寫api的同事輕描淡寫提了一下說代碼沒關(guān)閉鏈接,但越想越不對(duì)。到底什么原因?qū)е聸]有進(jìn)行socket.Close(),還需要具體從代碼入手。
不過,雖然沒有真正搞明白我們遇到的原因,但是原理清楚了,排查的時(shí)候還順手看到客戶端的代碼存在的問題。后續(xù)搞清楚了再更。
?
【1212更新】
終于有空來繼續(xù)查這個(gè)問題了。
前次排查的時(shí)候,在沒有完全理順這個(gè)原理的情況下認(rèn)為是客戶端(Android)上一段關(guān)于登陸請(qǐng)求的超時(shí)處理問題。我是被幾篇HttpClient的文章干擾了。在那幾篇文章里,作者說是因?yàn)檎{(diào)用HttpClient組件后,沒有對(duì)請(qǐng)求資源進(jìn)行釋放(即Response.Body.Dispose()),因此造成了CLOSE_WAIT狀態(tài)。 但作者沒說清楚。就像前文分析的一樣,服務(wù)端出現(xiàn)CLOSE_WAIT,根本問題一定是服務(wù)端的代碼有問題。當(dāng)然,既然排故,就順便一說。在同事的請(qǐng)求部分代碼上,設(shè)置了Timeout時(shí)間,并且做了不太安全的異常處理:
1 ... 2 Response res = client.newCall(request).execute(); 3 if(res.isSuccessful()){ 4 return res.body().string(); 5 } 6 else{ 7 throw new IOException("Unexpected code" + res); 8 } 9 ...并且這段代碼在一組try catch 內(nèi)部,且僅catch了IOException 【我沒細(xì)看OKClient的文檔,不清楚是否有其他的Ex可能性】
這段代碼的問題在于,一定要使用try catch進(jìn)行流程控制并不合理。此外,response會(huì)帶有服務(wù)端返回的標(biāo)準(zhǔn)Http 狀態(tài),因此此處對(duì)非200狀態(tài)的處理過于粗糙,浪費(fèi)性能。 所以在服務(wù)端出現(xiàn)大量CLOSE_WAIT后,這里應(yīng)該收到大量的500狀態(tài),非200只需要正常輸出信息并做相應(yīng)操作即可。
【像Try catch這樣的代碼,更好的使用方式是用于做應(yīng)對(duì)不可預(yù)料的錯(cuò)誤,而不是邏輯分支控制。大多數(shù)的錯(cuò)誤應(yīng)當(dāng)由正常的檢驗(yàn)代碼進(jìn)行處理。】
但是這部分代碼最多給客戶端造成問題,不會(huì)導(dǎo)致服務(wù)端出錯(cuò)。
?
因此,我們轉(zhuǎn)回看服務(wù)端問題。
因?yàn)槌鰡栴}的只有Login操作,于是在清楚問題的原理后,同事為Login操作加上了超時(shí)的判定。所以我的余下分析只能在修改后的代碼上進(jìn)行。
public void TLogin(string username, string pwd,out string resultJson){using (RtuHmiEntities db = new RtuHmiEntities()){var user = db.Base_Users.Where(x => x.UserCode == username && x.Password == pwd).ToList();if (user.Count() == 0){resultJson = user.ToJSON();return;}var siteList = db.SiteTable.Select(x => new { ID = x.Id, Name = x.SiteName }).ToList();var dtuList = db.DtuTable.Where(x => x.SiteId == null).Select(x => new { ID = x.ID, Name = x.DtuName }).ToList();var regularboxList = db.PressBoxTable.Select(x => new { ID = x.Id, Name = x.BoxName }).ToList();var valveList = db.ValveChamber.Select(x => new { ID = x.Id, Name = x.Name }).ToList();var result = new{Stations = siteList,BusinessUsers = dtuList,Regularboxes = regularboxList,Valverooms = valveList};resultJson = result.ToJSON(); }}當(dāng)然,仔細(xì)看了登陸操作的業(yè)務(wù)代碼后,我們能發(fā)現(xiàn)一些問題。在這個(gè)項(xiàng)目上,登陸后需要初始化一些列表類的信息給客戶端。這里的操作當(dāng)然沒什么問題。因?yàn)橥略趨⑴c這個(gè)項(xiàng)目時(shí),不止一次向我抱怨數(shù)據(jù)庫設(shè)計(jì)有問題,查詢很繁瑣。但是我沒有在這個(gè)項(xiàng)目上,所以沒有具體關(guān)注數(shù)據(jù)庫設(shè)計(jì)的缺陷。
不過從這段代碼看,前端需要接受4組信息,分別是 Stations、 BusinessUsers、 RegularBoxes、 Valverooms,數(shù)據(jù)庫給了四張表。這里采用Linq進(jìn)行查詢操作。一般說來,問題不大。實(shí)際上,這段代碼里查表的部分實(shí)際的執(zhí)行也沒有任何明顯性能不足的情況。硬挑的話,第一個(gè)分支里對(duì)空集合轉(zhuǎn)Json反倒會(huì)產(chǎn)生不必要的損失。即便如此,在正常情況下,請(qǐng)求執(zhí)行時(shí)間在200ms以內(nèi)。
既然代碼在測(cè)試下并沒有什么問題,那原因在哪呢?
回想了一下,IIS有一項(xiàng)設(shè)置是回收策略的。因?yàn)樵扔龅竭^因?yàn)榛厥諞]有自己定義,幾個(gè)舊版的很占資源的API還沒更新的時(shí)候就在回收周期內(nèi)把服務(wù)器資源吃滿了,導(dǎo)致其他api無法運(yùn)行。所以后來會(huì)根據(jù)測(cè)試時(shí)的資源情況為線上服務(wù)器設(shè)置這個(gè)回收值。當(dāng)然,對(duì)于一個(gè)不怎么復(fù)雜的查詢類API而言,并不需要多大的資源。于是第一次設(shè)置計(jì)劃給200M的專用內(nèi)存。不過顯然,因?yàn)槟骋粌蓚€(gè)數(shù)據(jù)量比較大的API在短時(shí)內(nèi)重復(fù)請(qǐng)求,造成了回收,此時(shí)登陸測(cè)試碰巧遭遇了釋放,于是就造成了上述 服務(wù)端被動(dòng)關(guān)閉連接,且沒有繼續(xù)向客戶端返回FIN包的狀態(tài),最終產(chǎn)生了幾條CLOSE_WAIT。
?
回想一下,大家遇到的CLOSE_WAIT基本上都是大量出現(xiàn),而我們遇到的情況只有少量的幾條,遠(yuǎn)沒達(dá)到客戶端測(cè)試的請(qǐng)求數(shù)量。排查下來看也確實(shí)是調(diào)試的時(shí)候遇到的小概率問題。不過不清楚原理的話,排查起來還是很沒頭緒的。論理論的必要性。在大多數(shù)時(shí)候,不需要那么精通理論能做到80分,但是想做到99分甚至更高,就是拼對(duì)理論的理解深度了。
?
【Finally,解決方案】
實(shí)際上我們?yōu)檫@個(gè)問題修正了三個(gè)地方。
第一是對(duì)客戶端代碼上做了修改,改進(jìn)非200請(qǐng)求的反饋方式。
第二是將iis的回收策略修改,提高了專用內(nèi)存的上限,降低回收時(shí)間間隔【看來從實(shí)際出發(fā),這樣做更有效】
第三,依然對(duì)服務(wù)端代碼進(jìn)行了修改,對(duì)部分請(qǐng)求增加了超時(shí)的處理,至于發(fā)現(xiàn)的不太利于性能的代碼,也會(huì)在重構(gòu)時(shí)改進(jìn)。
當(dāng)然,對(duì)這個(gè)問題貢獻(xiàn)比較大的是前兩條。畢竟出現(xiàn)的主要情況還是服務(wù)端被動(dòng)關(guān)閉。
?
這個(gè)問題我當(dāng)然還沒研究的太深。如果有錯(cuò)誤請(qǐng)務(wù)必指出。
?
?
?相關(guān)資料:
http://blog.csdn.net/shootyou/article/details/6622226
http://mp.weixin.qq.com/s?__biz=MzI4MjA4ODU0Ng==&mid=402163560&idx=1&sn=5269044286ce1d142cca1b5fed3efab1&3rd=MzA3MDU4NTYzMw==&scene=6#rd
感謝以上優(yōu)秀的技術(shù)文章。
轉(zhuǎn)載于:https://www.cnblogs.com/DannielZhang/p/8000496.html
總結(jié)
以上是生活随笔為你收集整理的【服务器】一次对Close_Wait 状态故障的排查经历的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: cesium进行模型高度测量的代码片段
- 下一篇: 100家大公司java笔试题汇总(带答案