前言:
這是我上周工作過程中的一次解決問題的過程。解決的是nginx負載下站點錯誤響應導致其他節點重復響應。 我在整理這個記敘文時,在給這個文檔命名時思考了一段時間。 從業務角度說,應該是“一次短信重復發送問題解決過程”,如果這樣命名,過于土氣;從技術角度說,應該是“nginx負載下站點錯誤響應會導致其他節點重復響應”,如果這樣命名,可能沒人會注意了;最后,我懶得再費腦汁了,從人類智慧的角度我把它命名為“復雜的問題,簡單的解,小處不謹慎,”。
問題來了
3月16日,有客服、銷售、運營人員反映,客戶在saas預定機票完成后,會連續收到3條重復的支付提醒短信。 ?很多客戶都投訴了這樣的問題,一直以來的正常情況是僅發1條的。
以下是短信平臺上的截圖:
前一天的3月15日是上線日,當晚saas上過線。
迫于壓力,當天下午不得不回滾3月15日的上線。回滾之后,問題不再重現。
3月15日saas的上線包括錦如和劉濤對saas所做的代碼改動。劉濤涉及到的代碼是審批接口,稱可能不會導致這樣的問題,錦如也肯定地表示不涉及到這塊。 我讓劉濤來排查原因,他在測試環境做了幾次測試,并未發現這樣的問題。我呢,由于最近忙于另一個項目,一直沒來得及親自和他一起查找原因。
問題又來了
3月17日是周四,上線日。要發布中新融創的對接程序。 晚上上線后,立即在線上訂票,發現訂單支付提醒短信還是會重復發3遍。
當時已經是晚上21點,我決定放下手頭的活兒,來跟劉濤一起排查原因。
問題分析
客戶在saas站點里預定機票,涉及到審批。審批是獨立的一個站點。
訂單的審批邏輯是這樣的:客戶在saas訂票成功后,會跳轉到統一審批頁,提交審批后,統一審批系統會把審批狀態通知給saas,然后重定向頁面到訂單流的下一個頁面(由saas提供)。
Saas提供了一個由一般處理程序(文件是HandlerAirOrderExamineStatus.ashx)實現的接口,用來接收審批狀態。內部大致邏輯是,修改訂單的審批狀態,然后會觸發相應的對客提醒短信。 客戶重復收到的短信是支付提醒短信,那么,意味著訂單不需要審批。不需要審批在系統里表現在這些企業配置審批流。 根據現有代碼邏輯,從填單頁跳轉到統一審批頁,統一審批頁初始化時沒有獲取到新審批,則會自動執行通知saas和頁面重定向。
要補充說明一下的是,線上的審批站點和saas站點均是通過nginx做的3臺服務器的負載。
通過查看線上日志,審批系統正常,即只會在其中一臺服務器節點來請求saas接口進行審批狀態的通知,并未發現異常日志。而saas呢,卻發現3臺服務器都記錄了同樣的日志。
為什么會出現這樣的情況呢? 審批在通知saas時,nginx接收到請求后應該分發給其中一個節點服務器來處理請求才對呀。
困惑
因為之前系統運行都是正常的,并未出現這種短信重發的情況,所以我們暫先不懷疑代碼邏輯。
馬上想到,記得運維說過,如果一個服務器處理時間超長,會自動分發給另一個節點服務器。 歷史原因,獲取訂單詳情和觸發短信通知這兩段代碼執行比較慢,那么,我在這兩段代碼外面加了個Stopwatch來診斷其執行時長。
publicvoidProcessRequest(HttpContext context)
{
Stopwatch sw=newStopwatch();
sw.Start();if(context.Request.Params.Count ==0)
{
context.Response.Write("{'returnCode':'2','returnMsg':'no parameters','notifiedSMSContent':'no parameters'}");return;
}stringparametersStr = context.Request.Params[0].AsToString();
SysLogToFile.WriteAuditMessage("dat修改訂單狀態傳入參數_"+parametersStr);//下面的是200行邏輯代碼,包括獲取訂單詳情和觸發短信通知...... ...... ...... ...... ...... ...... ...... ......
...... ...... ...... ...... ...... ...... ...... ......
...... ...... ...... ...... ...... ...... ...... ......
sw.Stop();
SysLogToFile.WriteAuditMessage("審批接口響應時間"+ (sw.ElapsedMilliseconds /1000));stringreturncode, returnmsg;if(flag)
{ returncode="1"; returnmsg ="success"; }else{
returncode="2"; returnmsg ="fail";
}
context.Response.Write("{'returnCode':'"+ returncode +"','returnMsg':'"+ returnmsg +"','notifiedSMSContent':'"+ returnmsg +"'}");
}
發布后經測試,發現3個節點的執行時長都長達7~8秒。
那么,我們接下來就優化程序吧,以期把duration降到最小。20分鐘后,我們把優化的代碼再發布到服務器。發現處理時間已經下降到了1~2秒。
然而,響應時間已經都優化到2秒了,依然是3個服務器節點上都做了處理。這帶給我們的是一個更大的問號。
轉機
為什么會有一個更大的問號呢? 因為我們線上的系統都是分布式部署的,一個流程邏輯會涉及到多個系統之間的交互訪問。 每個系統都是通過nginx做的3個節點的負載,并沒有出現過這種一個請求被3個服務器節點同時響應的情況。
在這種情況,我的一貫做法是釜底抽薪。當然,這次我依然堅持釜底抽薪。
Q:釜底抽薪是什么方式呢?
A:把接口里的代碼都注釋掉。響應時間應該接近于0毫秒,難道還會出現3個節點都響應的情況?
于是,我們注釋掉所有邏輯代碼,只保留了最后的輸出(context.Response.Write)語句,發布到服務器。
由于我們每次的測試步驟是:登陸saas,選擇航班,然后訂票下單,再看日志,這一系列的操作很耗時間,同時給線上系統帶來了很多無效訂單(垃圾數據)。這次呢,我讓劉濤直接訪問那個通知接口地址來測試性能。
轉機來了,在ie里直接訪問那個接口時,頁面直接拋出了大黃頁,報“System.FormatException:輸入字符串的格式不正確”,是由主方法里最后的這條語句產生的:
context.Response.Write(string.Format("{'returnCode':'{0}','returnMsg':'{1}','notifiedSMSContent':'{2}'}", returncode, returnmsg, returnmsg));
即在string.Format方法里,你是不能亂用‘{’的~。
于是,修復后(改成字符串拼接了)再次上線。
再次訪問那個接口,返回正常了。
這時,最大的驚喜是,只有一個服務器節點處理了請求,短信重發3次的問題得到了根治。
這時,我才想起來,nginx在接收到請求后,會分發給多個服務器節點中的一個節點來處理請求,但是,當出現錯誤響應時,nginx會自動分開給另一個節點來處理,直到沒有節點可供分發,即直到所有可用節點都被分發為止。(當然,這可能是我們運維對nginx的配置策略)
后續
我們解決完這個問題,已經午夜23:40,如釋重負的打車回家了。
在回去的路上,我的腦子像過電影似的過了一遍我們這一晚的處理過程。
l? 復雜的問題,在你用心解決時,往往產生自很簡單的一行代碼。很戲劇性的是,這行代碼可能是你不經意的疏忽寫錯了,也可能是技能受限寫錯的。如果,有做簡單的測試,這樣的問題完全是可以在開發時被發現的,而不至于花費那么長的時間來。
l? 另外,我又想到,審批系統調用接口時難道沒有判斷響應結果就做下一步的重定向了?這個疑問在第二天咨詢劉濤時得到了肯定。這樣的實現不夠嚴謹,于是,我讓他加上對結果的判斷。 如果事先有這個判斷,那么,這樣的問題在審批請求時就會被發現的,何至于經歷那么漫長的排查過程呢。