Release It!:通過測試的系統,為什麼上線後還是會出事

正式環境韌性(Production Resilience):系統不只要在正常情況下運作,還要能在依賴變慢、部分服務失敗、流量升高、資源耗盡與部署變更時,限制故障範圍、維持可觀察性,並讓服務可以安全恢復。Michael T. Nygard 在《Release It!》裡強調的核心,就是把「故障一定會發生」當成設計前提。

很多系統在測試環境都很好。

功能測試通過、整合測試通過、品質檢查也沒發現問題,上線之後卻還是可能突然卡死、延遲暴增,甚至因為一個外部服務出問題,拖垮整條流程。

原因不一定是程式邏輯寫錯。

正式環境和測試環境最大的差別,是它有真實流量、真實資料量、真實依賴、長時間運作,以及各種你沒有預先安排好的失敗組合。

《Release It!》處理的重點,就是這些「功能正確之外」的問題。

目錄 隱藏

通過測試,不等於已經準備好上線

一般功能測試最常驗證的是:

  • 輸入正確時,輸出對不對。
  • 某個流程能不能完成。
  • 某個 API 回傳內容是不是符合預期。

但正式環境還會問更多問題:

  • 下游服務很久都不回應時會怎樣?
  • 資料量變大時,查詢會不會把記憶體吃光?
  • 某個連線池被占滿時,其他功能會不會一起停住?
  • 新版程式部署到一半時,新舊版本能不能同時工作?
  • 故障發生時,團隊能不能快速知道問題在哪裡?

這些才是正式環境會實際考驗的地方。

第一個風險:整合點最容易讓故障擴散

整合點(Integration Point)指的是系統和外部東西接觸的位置,例如資料庫、第三方 API、付款服務、Email、快取、訊息佇列或另一個內部服務。

每一次跨出去,都要假設對方可能:

  • 失敗。
  • 很慢。
  • 回傳不完整資料。
  • 暫時不可用。
  • 在你最忙的時候開始不穩。

更危險的一種情況,是服務一直不回應。等待中的連線、執行緒與請求會持續占用資源,最後可能把原本健康的服務也拖慢。

逾時(Timeout):每一個外部呼叫都要知道「最多等多久」

如果呼叫外部服務時沒有逾時設定,系統就可能無限等待。

一個付款 API 變慢,前端請求開始堆積;執行緒被占滿;新的正常請求也進不來。最後看起來像整個網站都掛了。

比較合理的做法,是為連線與讀取設定清楚的逾時條件,讓系統知道什麼時候該停止等待。

逾時的作用,是避免一個失敗依賴無限制消耗整個系統;它不等於直接放棄使用者。

斷路器(Circuit Breaker):下游一直失敗,就先停止繼續撞

斷路器的概念很像家裡電路過載時先跳電。

如果某個下游服務持續失敗,系統不應該每一個新請求都再去撞一次。

斷路器通常會有三個狀態:

  • 關閉:正常放行呼叫。
  • 開啟:失敗累積到門檻後暫時停止呼叫。
  • 半開:過一段時間後允許少量測試,確認服務是否恢復。

這樣做的目的,是把失敗局部化,避免整個系統因為一個依賴失靈而產生連鎖故障。

艙壁隔離(Bulkhead):不要讓一個服務吃光所有共同資源

船艙會用隔板把空間分開,某一區進水時不至於整艘船一起沉。

軟體裡的艙壁隔離也是同樣概念。

如果付款、搜尋、Email 都共用同一組執行緒或連線池,Email 服務一旦卡住,就可能把資源全部占滿,連付款也跟著不能用。

把重要依賴分開配置資源,可以讓故障停在比較小的範圍內。

重試不是越多越安全,小心重試風暴

重試(Retry)看起來很直覺:失敗就再試一次。

但當服務已經過載時,所有客戶端同時重試,反而會增加更多流量,形成重試風暴(Retry Storm)。

因此重試通常要搭配:

  • 重試次數上限。
  • 指數退避(Exponential Backoff)。
  • 抖動(Jitter),讓不同請求不要同時再次湧入。
  • 整體重試預算,避免系統把大部分資源花在重送失敗請求。

重試是恢復工具,不應該變成放大故障的工具。

快速失敗(Fail Fast):已經知道做不到,就不要浪費更多資源

當系統已經知道某個請求目前無法完成時,快速失敗會盡快回傳明確結果,避免繼續消耗資源。

例如某個必要依賴已經被斷路器判定不可用,就不需要再讓請求一路等待到最深層才失敗。

越早停止不可能成功的工作,就能保留更多資源給仍然可以完成的請求。

不要忘記「無界限」本身就是風險

很多問題在資料量小時完全看不出來。

例如:

  • 一次讀取全部訂單。
  • 列表 API 沒有分頁。
  • Log 永遠累積不清。
  • Session、暫存檔或快取只增加不淘汰。

這些在測試資料只有少量時可能完全正常,正式環境累積幾個月後才開始出事。

所以穩態(Steady State)的概念很重要:系統長期運作時,會不會自動清掉不再需要的資源,還是只會一直增加?

容量(Capacity)不是一個數字

系統容量不是「這台主機可以撐多少人」這麼單純。

最先碰到瓶頸的,可能是:

  • CPU。
  • 記憶體。
  • 網路。
  • 磁碟 I/O。
  • 資料庫連線池。
  • 執行緒。
  • 第三方 API 的速率限制。

容量規劃(Capacity Planning)的重點,是知道哪個資源會先到極限,以及到極限時系統會怎麼退化。

如果你正在規劃大型系統的需求與架構取捨,可以延伸閱讀System Design:大型系統設計不是先畫架構圖,而是先搞清楚需求與取捨。

行銷成功,也可能變成一次容量事故

網站最尷尬的狀況之一,是直播、廣告、促銷或媒體曝光終於成功帶來大量流量,結果報名頁、結帳頁或會員登入先撐不住。

這不是「被攻擊」才需要處理的問題。正式環境本來就要把突發流量視為正常風險之一。

如果事前知道某場直播、開賣或活動會帶來集中流量,就應該把行銷節奏和容量規劃放在同一張圖上:哪個頁面會先被打到、哪個服務最容易成為瓶頸、失敗時能不能排隊或降級。

對商業網站來說,穩定性本身就是成交流程的一部分。流量來了卻無法報名或付款,前面買流量、做內容、辦活動的成本都可能一起浪費。

負載、壓力、長時間與突發測試,各自看的問題不同

負載測試不只一種。

  • 負載測試(Load Test):模擬預期使用量,確認正常尖峰下的表現。
  • 壓力測試(Stress Test):持續提高負載,看系統從哪裡開始退化。
  • 長時間測試(Soak Test):維持一段時間,找記憶體、連線、檔案等慢性資源洩漏。
  • 突發測試(Spike Test):模擬短時間突然湧入大量流量。

這些測試的目的,是提早知道系統會怎麼壞、從哪裡開始退化,而不是宣稱它永遠不會故障。

部署(Deployment)和發布(Release)最好分開

把新版程式放上伺服器,和讓所有使用者立刻使用新版,其實是兩件不同的事。

如果兩者綁死,每一次部署都等於一次全面押注。

更安全的方式,是讓程式可以先部署,再透過功能旗標(Feature Flag)、金絲雀發布(Canary Release)、藍綠部署(Blue-Green Deployment)或滾動部署(Rolling Deployment)逐步開放。

這樣一旦發現異常,可以先停止發布範圍,而不是只能整套回滾。

資料庫遷移也要考慮新舊版本同時存在

部署過程中,新舊版本常常會短暫同時運作。

如果新版一上線就直接刪除舊欄位,舊程式可能立刻失效。

展開—收縮(Expand-Contract)是一種較安全的資料庫變更思路:先加入新結構,讓新舊程式都能運作;完成資料遷移與版本切換後,再移除舊結構。

這能降低資料庫變更把部署變成單向門的風險。

可觀測性(Observability):不能只知道「掛了」

系統故障時,最麻煩的是已經知道有問題,卻不知道問題在哪裡。

正式環境至少要有幾種不同訊號:

  • 日誌(Log):發生了什麼事件。
  • 指標(Metric):數量、比率與趨勢。
  • 追蹤(Trace):一個請求跨過哪些服務、每段花多久。

這幾種訊號會彼此補充,一起幫你回答「現在健康嗎」「哪裡開始變慢」「這個錯誤從哪一段傳過來」。

健康檢查(Health Check)不能只問「程式還活著嗎」

一個程序還活著,不代表它真的能服務使用者。

如果資料庫已經斷線、快取不可用、佇列滿了,單純回傳 HTTP 200 的健康檢查沒有太大意義。

比較完整的健康檢查,要能區分:

  • 程式本身是否存活。
  • 必要依賴是否可用。
  • 關鍵資源是否接近耗盡。

這樣流量入口才能避免繼續把請求送到已經失去服務能力的節點。

警報要看使用者症狀,不要只盯 CPU

CPU 很高不一定代表使用者正在受影響;CPU 正常,也不代表服務沒有出問題。

更接近使用者體驗的訊號通常是:

  • 錯誤率。
  • 請求延遲。
  • 成功率。
  • 可用性。

RED 方法可以幫服務整理三個基本面向:請求速率(Rate)、錯誤(Errors)、持續時間(Duration)。

重點是先看使用者感受到的症狀,再往下追查 CPU、記憶體或資料庫連線等原因。

正式環境韌性,也和技術債直接相關

沒有逾時、沒有斷路器、查詢無上限、部署只能一次全開,這些都是會累積營運風險的技術債。

如果舊系統已經讓團隊不敢改,可以搭配技術債怎麼還?不要大重寫,而是讓舊系統一點一點重新變得安全,先把變更路徑保護起來,再逐步補正式環境韌性。

失敗測試(Failure Testing):韌性不能只靠想像

如果從來沒有看過依賴失敗時系統會怎麼反應,就很難真的知道保護機制有沒有用。

失敗測試可以在受控環境裡,驗證像是下游不可用、延遲升高、某個節點退出等情況下,系統是否會依預期降級與恢復。

這類測試必須有明確範圍、授權、回復方式與觀察指標。重點是驗證系統設計,不是為了「故意把正式站打掛」。

混沌工程(Chaos Engineering)的價值,是把未知風險變成已知風險

混沌工程常被誤解成隨機破壞。

比較成熟的做法,是先定義穩定狀態與假設,再在可控制的範圍內驗證:某個元件失敗時,整體服務是不是仍然守住預期。

可以先從非正式環境與低風險範圍開始,確認觀測、回復與停止條件都存在,再逐步擴大。

一張表快速檢查正式環境韌性

檢查項目 要回答的問題
逾時 每個外部呼叫最多會等多久?
斷路器 依賴持續失敗時,系統會不會停止繼續撞?
艙壁隔離 單一依賴能不能吃光所有共同資源?
重試 重試會不會反而放大故障?
容量 哪一個資源會先成為瓶頸?
部署 能不能逐步發布、快速停止?
健康檢查 能不能看出必要依賴已經失效?
可觀測性 故障時能不能快速定位?

常見問題

功能測試都通過,為什麼正式環境還會掛?

因為功能測試主要驗證程式在已知條件下是否正確,正式環境還會加入真實流量、延遲、長時間運作、資源限制、資料量與外部依賴故障。

所有外部 API 都要設定逾時嗎?

原則上應該有清楚的等待上限。沒有逾時,就等於允許單一依賴無限制占用資源。

斷路器和重試有什麼不同?

重試是在短暫失敗時再嘗試;斷路器則是在失敗持續發生時,暫時停止繼續呼叫。兩者通常需要一起設計,而不是各自獨立。

使用雲端自動擴充,就不用做容量規劃嗎?

還是需要。自動擴充本身有啟動時間、配額與其他資源限制,而且瓶頸可能在資料庫、外部 API 或連線池,不一定是主機數量。

健康檢查回傳 200 就代表服務正常嗎?

不一定。程式程序可能活著,但必要資料庫、快取或佇列已經無法工作。正式環境通常需要更有層次的健康訊號。

混沌工程一定要在正式環境做嗎?

不一定。可以先在非正式環境和小範圍驗證。若要進入正式環境,必須有明確授權、觀測、停止條件與回復計畫。

Release It! 最重要的提醒,是把失敗當成正常設計條件

正式環境不會永遠保持理想狀態。

依賴會慢、資源會滿、流量會突然增加、部署會出錯,外部服務也一定會有不可用的時候。

可靠系統的標準,不是承諾永遠不故障;更重要的是故障發生時能限制影響、快速察覺、維持重要功能,並有清楚的恢復路徑。

當逾時、斷路器、艙壁隔離、容量規劃、漸進式發布、健康檢查與可觀測性都被當成產品的一部分,系統才算從「測試能過」走向「正式環境能長期運作」。

參考資料

Michael T. Nygard:Release It! Design and Deploy Production-Ready Software。

元氣站長
元氣站長

經營網路事業,賺取持續收入!
網路賺錢修煉,成就理想人生!