如何把部署的 502 錯誤降至零
每次部署 Mythex 的後端,都會有少量請求失敗,並回傳 502 Bad Gateway。數量不多,但部署期間出現 502,就代表某位用戶的點擊沒有任何反應,或是他正在看的建構停頓了一下。我們追查後發現兩個獨立的問題:一個自我修復的看門程式與部署互相角力,以及伺服器停止監聽與負載平衡器停止向它發送流量之間的時間差。修復之後,連續三次正式部署的數字由 10 降至 6,再降至 0。
架構
我們的 API 和協調器以容器形式運行在一組小型伺服器上,位於 AWS Application Load Balancer(ALB)之後。部署會逐台更新伺服器:排空舊容器,啟動新容器,然後移到下一台。每台伺服器上都有一個看門程式,每分鐘檢查容器是否在運行,並重新啟動任何停止的容器——它的存在,就是為了讓凌晨三點崩潰的程序自行恢復,無需有人被叫醒。
兩者單獨來看都合理。但放在一起就不然。
問題一:看門程式與部署競爭
看門程式每分鐘運行一次,並在一次健康檢查失敗後就進行「修復」。而部署會刻意排空服務長達 150 秒——在此期間,健康檢查本來就應該失敗。因此,看門程式在每一次部署期間都會觸發。
大多數時候它輸掉這場競賽,而無傷大雅。但有一次它贏了:它在部署進行中改寫了 compose 檔案(描述哪些容器應該運行的檔案),令一台伺服器完全沒有閘道容器——而部署工作卻報告成功。
修復方法:
- 同一把鎖。 部署現在會取得與伺服器啟動腳本相同的檔案鎖,因此看門程式與部署永遠不會同時運行。
- 四次失敗才行動,而不是一次。 看門程式需要連續四次健康檢查失敗才會採取行動。正在排空的服務不再被誤判為已死。
- 單一定義。 compose 檔案現在只有一個來源,並發佈到伺服器,而不是可能彼此偏離的多份副本。
- 驗證,而不是信任。 部署工作不再因為某個命令返回而通過。它會先檢查每台伺服器的 API、協調器、閘道和發佈工作程序,然後才可能成功。
問題二:伺服器比流量先離開
第二個問題較為隱蔽。當部署排空一台伺服器時,該伺服器會開始以 503 回應其健康端點,好讓負載平衡器將它移出輪替。我們的伺服器這樣做了 5 秒,然後才關閉。
但 ALB 只會每隔一段時間檢查健康狀況,而且要連續多次失敗才會停止向某個目標路由,端到端最多約需 60 秒。因此,在大約 55 秒內,負載平衡器仍會把真正的請求發送給一台已經停止監聽的伺服器。那些就是我們的 502。
修復方法是令兩條時間線互相配合:
- 目標群組現在每 5 秒檢查一次,並在連續 2 次失敗後將目標標示為不健康——大約 10 秒便能察覺。
- 排空會在開始讓健康檢查失敗之後,再繼續為真正的流量提供服務 15 秒,因此總會有重疊的時間。
- 這些設定寫在負責佈建基礎設施的腳本中,而不是某人曾經在控制台上點擊設定的地方。
讓各數字互相吻合
這些逾時相互依賴。排空時間必須長於負載平衡器察覺所需的時間;排空加上關閉必須在容器被強制終止之前完成。某人在六個月後更改其中一個數字,然後悄悄地令錯誤重現,是很容易發生的事。
因此,我們把這些關係寫成測試:排空時間必須長於健康檢查間隔乘以失敗閾值;重新被標示為健康所需的時間必須保持簡短;整個排空過程必須能放入容器的停止寬限期之內。只要更改其中一個數字而沒有同時調整其他數字,CI 便會失敗。我們還對測試本身進行了變異測試——把每個數字改成錯誤的值,然後檢查是否有測試能夠捕捉到。
測量部署很容易出錯
我們最初的測量結果顯示,一次正常的部署大約有 3% 是壞的。但事實並非如此。我們計算的是所有非 200 的回應,而排空期間健康端點回傳的 503 並不是失敗——它是排空在盡其本分,告訴負載平衡器不要選取該伺服器,而該伺服器仍在繼續處理真正的請求。
用戶真正感受到的是 502:請求連一個能回應的目標都沒有到達。只計算這些,才得出真實的情況——連續三次正式部署分別為 10、6,然後是 0。
我們的心得
- 自我修復需要知道計劃中的停機。 一個無法分辨部署與崩潰的看門程式,遲早會與部署互相角力。
- 排空是一次握手。 伺服器必須持續提供服務,直到負載平衡器確定已停止發送流量,而這個時間是在負載平衡器上設定的,而不是在伺服器上。
- 把不變量編碼為測試。 營運數字會逐漸偏移。把它們彼此關聯起來的測試則不會。
- 在測量之前,先決定你要量度什麼。「錯誤」是錯誤的指標;「沒有到達任何目標的請求」才是正確的指標。
如果你在 Mythex 上發佈應用程式,這一切對你都是看不見的——這正是目標所在。相關閱讀:你的已發佈應用程式如何在重新發佈期間保持上線。