最近接連聽說一台線上服務器總是不響應客戶端請求。
登錄服務器后查詢iis狀態,發現應用程序池狀態變為已停止。
按經驗想,重啟后應該就ok,第一次遇到也確實起了作用,當時完全沒在意,以為是其他人無意把服務關閉了而已。
但是之后幾天幾乎每天都出現問題,應用程序池再次成為 已停止 狀態。這個情況顯然有問題。於是開始排查設置。
線上環境很簡單,iis + API應用,數據庫在內網上,沒有反向代理。
出問題的應用程序池承載了一個基礎數據API,查詢量極小,數據量也極小,只是因為同事做實現時提過,查詢很麻煩,所以我讓他同時也檢查代碼的查詢是否過於繁瑣產生超出設置上限的資源占用。
但根據同事檢查結果,這個情況不存在,數據庫查詢響應非常快,服務器的資源監控上看相關進程也沒有出現明顯的資源飆升。因此判斷服務端代碼應該沒有問題。
經驗路線在這里就走不通了,只能嘗試在設置上找原因。 iis對應用程序池的設置里最可能影響服務的是回收策略的設置。為了避免影響其他服務,部署時設置了這個api的專用內存上限到1GB。當然,既然代碼檢查沒有問題,測試運行也沒發現資源飆升的情況,回收的專用內存上限應該不會輕易觸發。但是還是檢查了設置,畢竟從前有同事設置時把1GB給成了100M,除了登錄之外執行不了任何請求。
最終想到用netstat檢查端口情況。起初的懷疑是端口被其他進程無意占用導致問題。
啟動netstat后,讓測試發了幾次請求,順利看到進程出現在列表,且沒有端口沖突,對端地址經查證是來自app。當然,也注意到服務器狀態是一堆 Close_Wait.
當然我經驗少,Close_Wait還真是第一次見。不過既然狀態不是Established,這個應該就是問題的原因了。開始查資料,學習Close_Wait狀態是什么情況。
對於 服務器的連接狀態而言,一般有三種比較常見的: Established、 Time_Wait、Close_Wait。
從別人博客上扒了一張原理圖:
通過原理圖,我們知道了CLOSE_WAIT是被動關閉的狀態。什么意思呢?比如客戶端發了個請求,正常情況下是會收到服務器響應一個狀態的,即Response。當客戶端讀取了這個返回后,會主動告訴服務器收到了,關閉連接。
由於是客戶端發起關閉連接的請求,在TCP協議下雙方需要通過四個包的互發完成雙向確認工作,才能最終關閉這個連接。
客戶端要求關閉,此時客戶端狀態為 FIN_WAIT_1,同時向服務器發送了 FIN 包,服務器狀態變更為CLOSE_WAIT;
當然,服務器需要對收到FIN包向客戶端確認,於是服務器向客戶端發送了 ACK 包,客戶端因此變更狀態為FIN_WAIT_2;
服務器處理了這個確認后,再次主動向客戶端發送FIN包,同時自己狀態變更為LAST_ACK,收到來自服務器FIN包的客戶端也將自己狀態變更為TIME_WAIT;
最后一步,客戶端會對來自服務器的FIN包回復確認,服務器收到該ACK包后,將自己狀態置為CLOSED,如此,整個關閉過程結束。
簡單說就是,客戶端 -》 服務器,我要關閉,服務器回復OK,並開始處理后續;服務器后續處理好后,再告訴客戶端我可以關閉了,客戶端確認,服務端關閉。
所以,出現CLOSE_WAIT狀態的原因是,服務器一端因故沒有向客戶端發出FIN包,即服務端的LAST_ACK -- FIN -->客戶端這步沒能執行。
因此,看到CLOSE_WAIT狀態后,那么可以確定服務器沒有執行后續動作,即調用socket.Close()。
舉一反三,即誰被動關閉,則誰的連接釋放代碼有問題。
但是socket.Close()的調用應當是由web服務器自己完成呀,所以問題還沒確定。寫api的同事輕描淡寫提了一下說代碼沒關閉鏈接,但越想越不對。到底什么原因導致沒有進行socket.Close(),還需要具體從代碼入手。
不過,雖然沒有真正搞明白我們遇到的原因,但是原理清楚了,排查的時候還順手看到客戶端的代碼存在的問題。后續搞清楚了再更。
【1212更新】
終於有空來繼續查這個問題了。
前次排查的時候,在沒有完全理順這個原理的情況下認為是客戶端(Android)上一段關於登陸請求的超時處理問題。我是被幾篇HttpClient的文章干擾了。在那幾篇文章里,作者說是因為調用HttpClient組件后,沒有對請求資源進行釋放(即Response.Body.Dispose()),因此造成了CLOSE_WAIT狀態。 但作者沒說清楚。就像前文分析的一樣,服務端出現CLOSE_WAIT,根本問題一定是服務端的代碼有問題。當然,既然排故,就順便一說。在同事的請求部分代碼上,設置了Timeout時間,並且做了不太安全的異常處理:
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 內部,且僅catch了IOException 【我沒細看OKClient的文檔,不清楚是否有其他的Ex可能性】
這段代碼的問題在於,一定要使用try catch進行流程控制並不合理。此外,response會帶有服務端返回的標准Http 狀態,因此此處對非200狀態的處理過於粗糙,浪費性能。 所以在服務端出現大量CLOSE_WAIT后,這里應該收到大量的500狀態,非200只需要正常輸出信息並做相應操作即可。
【像Try catch這樣的代碼,更好的使用方式是用於做應對不可預料的錯誤,而不是邏輯分支控制。大多數的錯誤應當由正常的檢驗代碼進行處理。】
但是這部分代碼最多給客戶端造成問題,不會導致服務端出錯。
因此,我們轉回看服務端問題。
因為出問題的只有Login操作,於是在清楚問題的原理后,同事為Login操作加上了超時的判定。所以我的余下分析只能在修改后的代碼上進行。
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(); } }
當然,仔細看了登陸操作的業務代碼后,我們能發現一些問題。在這個項目上,登陸后需要初始化一些列表類的信息給客戶端。這里的操作當然沒什么問題。因為同事在參與這個項目時,不止一次向我抱怨數據庫設計有問題,查詢很繁瑣。但是我沒有在這個項目上,所以沒有具體關注數據庫設計的缺陷。
不過從這段代碼看,前端需要接受4組信息,分別是 Stations、 BusinessUsers、 RegularBoxes、 Valverooms,數據庫給了四張表。這里采用Linq進行查詢操作。一般說來,問題不大。實際上,這段代碼里查表的部分實際的執行也沒有任何明顯性能不足的情況。硬挑的話,第一個分支里對空集合轉Json反倒會產生不必要的損失。即便如此,在正常情況下,請求執行時間在200ms以內。
既然代碼在測試下並沒有什么問題,那原因在哪呢?
回想了一下,IIS有一項設置是回收策略的。因為原先遇到過因為回收沒有自己定義,幾個舊版的很占資源的API還沒更新的時候就在回收周期內把服務器資源吃滿了,導致其他api無法運行。所以后來會根據測試時的資源情況為線上服務器設置這個回收值。當然,對於一個不怎么復雜的查詢類API而言,並不需要多大的資源。於是第一次設置計划給200M的專用內存。不過顯然,因為某一兩個數據量比較大的API在短時內重復請求,造成了回收,此時登陸測試碰巧遭遇了釋放,於是就造成了上述 服務端被動關閉連接,且沒有繼續向客戶端返回FIN包的狀態,最終產生了幾條CLOSE_WAIT。
回想一下,大家遇到的CLOSE_WAIT基本上都是大量出現,而我們遇到的情況只有少量的幾條,遠沒達到客戶端測試的請求數量。排查下來看也確實是調試的時候遇到的小概率問題。不過不清楚原理的話,排查起來還是很沒頭緒的。論理論的必要性。在大多數時候,不需要那么精通理論能做到80分,但是想做到99分甚至更高,就是拼對理論的理解深度了。
【Finally,解決方案】
實際上我們為這個問題修正了三個地方。
第一是對客戶端代碼上做了修改,改進非200請求的反饋方式。
第二是將iis的回收策略修改,提高了專用內存的上限,降低回收時間間隔【看來從實際出發,這樣做更有效】
第三,依然對服務端代碼進行了修改,對部分請求增加了超時的處理,至於發現的不太利於性能的代碼,也會在重構時改進。
當然,對這個問題貢獻比較大的是前兩條。畢竟出現的主要情況還是服務端被動關閉。
這個問題我當然還沒研究的太深。如果有錯誤請務必指出。
相關資料:
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
感謝以上優秀的技術文章。