閒聊 - 系統異常已排除,網站卻繼續崩潰瘋狂 503?
| | 0 | |
翻到很久以前的內部技術分享投影片,覺得對新手開發朋友們或許有點幫助,整理成文章分享。
先說,這是熱門網站獨有的煩惱,不是每個開發者都需要面對。大家在演唱會訂票、馬拉松報名、疫苗預約… 等秒殺網站爆炸新聞中,應該都有看過一個神祕數字 - 503:
依據網站開發權威參考網站 MDN 的說明,HTTP 狀態 503 是流量過載會出現的經典錯誤:
HTTP 503 Service Unavailable 伺服器錯誤回應狀態碼表示伺服器尚未準備好處理請求。常見的原因包括伺服器正在維護或過載。在維護期間,伺服器管理員可能會暫時將所有流量導向 503 頁面,或者在軟體更新期間自動發生這種情況。在過載的情況下,一些伺服器端應用程式會在記憶體、CPU 或連接池等資源達到上限時,以 503 狀態拒絕請求。丟棄傳入的請求可以產生背壓,防止伺服器的計算資源被完全耗盡,從而避免更嚴重的故障。
網站撐不住,其實可再分成兩種情況。第一種是搶門票搶報名搶限量商品這類瘋狂情境,原本便知道會瞬間湧入數倍、數十倍或數百倍於系統設計的流量,網站團隊需做好從容就義的心理準備,要如何優雅化解海嘯等級的瞬間流量是門高深學問,不在本文的討論範圍(主要也是因為超出我的能力範圍,笑);這篇想聊的是第二種狀況,網站面對一般的日常請求量,系統出了小差錯但很快修復,之後系統卻陷入流量超載,狂噴 503 錯誤。
以下是我的模擬情境。假設有個系統如下圖,標準的瀏覽器、網站、API 服務、資料庫多層架構:
客戶端使用瀏覽器連上五台伺服器組成的網站,有負載平衡器將請求平均分配給五台網站處理,背後則呼叫 WebAPI 串接 DB 實現業務邏輯,為求聚焦本案例會把 WebAPI 與 DB 段當成黑盒子。
系統在尖峰時間的同時線上人數不算多,大約一千人,但由於所提供服務是其他作業的先導步聚,故使用者有強烈的使用動機,沒成功不會輕易放棄。
理論上五台機器撐一千人應該游刃有餘,但某天就出事了... 事件的時間線如下:
- 09:00 網頁服務回應緩慢或無回應
- 09:05 使用者通報 HTTP 503/500
- 09:10 維運人員調查,鎖定 WebAPI 異常
- 09:15 確認問題來源,使用 3R 大絕 - 重啟 WebAPI 服務排除異常
- 09:20 線上有大量焦急使用者,WebAPI 再次 503/500
- 09:45 尖峰已過,需求逐漸消化,恢復平靜
事件最開始的現象是網頁偏慢或無回應,五分鐘後有人遇到 503 或 500。五分鐘後 OP 很快由 Log 查出問題可能是 WebAPI 異常,討論五分鐘後決定動用 3R 大絕(Restart、Reboot 跟 Reinstall)的 Restart。WebAPI 重啟後異常消失,但問題沒改善,網站再次冒出 503/500,直到半小時後尖峰過去,需求逐漸消化才恢復平靜。
這背後發生了什麼事?先從網站伺服器的一些習性說起:
- 請求要排隊等處理
送到網站的請求會先被放進 Queue,等待網站伺服器指派 Worker Thread 取出處理。當請求消化速度過慢,排隊隊伍愈來愈長,當等待的請求數量超過 Queue 容量上限,網站便會吐回 HTTP 503 結束該請求。 - 隊伍愈長消化愈慢
消化速度變慢的一種可能原因是獨佔型資源必須排隊使用,人愈多就要等愈久。當等太久超過時限,會出錯傳回 HTTP 500,造成 503、500 參雜出現。
另一方面,每條連線請求要消耗記憶體,配置及 GC 需要成本,伺服器啟動過多執行緒同時處理大量請求,會導致 CPU Context Switch 成本上升,也會讓速度變慢。
還有一種情境是多人同時使用,會出現兩個請求互相持有對方所需資源等待對方放手(術語叫 Deadlock),此時系統會犧牲其中一方以化解僵局,被犧牲的一方也會得到錯誤 HTTP 500。 - 服務重啟如睡獅初醒
OP 重啟 WebAPI 消除故障卻沒解決問題,很快又 503 原因之一是主機啟動後需一段時間才會達到正常效能水準。伺服器有些一次性動作像是讀取資料檔、JIT 編譯、初始化、建立連線、建立 Cache 等需要時間,也讓 CPU 跟 IO 比較忙,是剛啟動效能比較差的原因。(所以效能評測時常會留一段 Warmup 時間再記錄數據) - 請求一旦開始處理就要做到完
另外有一點常被忽略,即使客戶端關閉頁籤、關掉瀏覽器,已經送到伺服器開始處理的請求還是要做到完。
接著將焦點移到客戶端:使用者有個重要天性,遇到網頁很慢或沒反應,反射動作就是按 F5 重新整理。
剛才有說到已送出的 Request 在伺服器會做到完,因此當使用者按 F5 重新整理,等同放棄了前一次請求,但這個作廢的請求仍會在伺服器端繼續跑完,而重新整理已多送了新請求雪上加霜。用一個迷你 ASP.NET Core 網站來證明這點,它只有一個網頁,每次接到請求時顯示 Start,等三秒再顯示 End 傳回文字。
由實測結果,每次用瀏覽器檢視網頁會看到一個 Start、一個 End,第一次、第二次、第三次我們正常存取,再來改成狂按重新整理圖示,一陣瘋狂輸出後直接看到第 13 次請求顯示的網頁,但由伺服器端輸出可以看到前面第 4 到第 12 個請求一次都沒被略過,全都乖乖跑完了。證明請求一旦被伺服器執行,即使瀏覽器端已放棄,仍然會跑到完。
基於以上知識,我們試著還原現場,拼湊當時發生什麼事。
最早是 WebAPI 異常回應太慢,讓隊伍過長出現 503,並因為 Timeout、Deadlock 夾雜 HTTP 500;前面五台 Web 請求塞車,卡在 WebAPI 無法消化,累積在 Queue 裡的請求愈來愈多。
OP 重啟 WebAPI 後,異常排除,但 Queue 已累積了很多請求而持續增加,而 WebAPI 剛重啟還沒達正常效能水準,睡獅初醒便被圍毆。隊伍愈長消化愈慢,回應愈慢隊伍愈長,加上使用者狂按 F5,伺服器必須處理大量作廢請求,形成惡性循環,再次引爆 503/500。
遇上這類暫時性錯誤引發的效能風暴,我們該如何如何粉飾太平維持系統穩定?
所謂暫時性錯誤,也是系統異常,但其具備再試一次或稍侯再試可恢復的特性,典型案例像是 DB Deadlock、網路掉包、效能不佳 Timeout、服務重開… 等。
面對暫時性錯誤,我們有些因應策略,像是:
- Retry 若屬可預期暫時錯誤,再試一次或多次
- Circuit Breaker 暫時不接受請求
- Timeout 等一段時間後放棄
- Bulkhead Isolation 停用部分服務或設同時執行數量上限,避免耗光資源
- Fallback 出錯時使用替代方案,像是服務壞掉時用 Cache
- PolicyWrap 則是組合拳 ,組合上述多種策略並用
若想了解更多,推薦兩篇舊文:處理 Deadlock、網路瞬斷、伺服器忙線等暫時性故障的利器 - Polly)、軟體系統的保險絲 - .NET Polly CircuitBreakerPolicy
套用到本案例,有幾個可考慮的改善方向。
- Bulkhead Isolation
依系統負載設計限制同時處理上限,超出時拒絕服務。
可以想成排隊美食發號碼牌限定人數,保證容量內服務品質,好過塞爆沒有人吃得到。 - Circuit Breaker
連續錯誤或嚴重錯誤時先暫停服務一段時間,提供重啟服務達到正常效能水準的喘息空間。
這招可阻擋不理性的 F5 (避免塞爆 Queue 增加平行處理壓力)
最後補充兩個進階議題,第一個是 Retry 時機,要等多久?一般會等一小段時間再試成功率較高,若要多次重試,等待時間逐步拉長,2, 4, 16 秒之類的,另外等待時間會加上隨機值,避免 100 個請求失敗,2 秒後 100 個請求同時重試,加入少量隨機延遲可避免大家一起重試形成尖峰。
第二是 Circuit Breaker 運作狀態循環,開始為 Closed,連續錯誤 N 次後 Open,一段時間後進入 Half Open,Half Open 後首次執行視為測試,成功切回 Closed,失敗回到 Open。
The article discusses handling HTTP 503 errors due to temporary overloads in websites. It explains server-client dynamics, common server issues, and user behavior like frequent page refreshes. Solutions include retry strategies, circuit breakers, and bulkhead isolation to manage temporary faults effectively.
Comments
Be the first to post a comment