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