很多系統在測試環境都很好。
功能測試通過、整合測試通過、品質檢查也沒發現問題,上線之後卻還是可能突然卡死、延遲暴增,甚至因為一個外部服務出問題,拖垮整條流程。
原因不一定是程式邏輯寫錯。
正式環境和測試環境最大的差別,是它有真實流量、真實資料量、真實依賴、長時間運作,以及各種你沒有預先安排好的失敗組合。
《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。
