及早發現發佈後立即崩潰的應用程式
9 月 25 日,有三次發佈被標示為成功,儘管該應用程式的 API 在每次啟動後的數秒內都會崩潰。因為它們「成功」了,所以沒有執行回滾;幾分鐘後,一項後台檢查將它們判定為失敗,整個網站因此變成 404。健康檢查是在託管平台的事件清單中計算崩潰次數,而該清單每部機器只保留最近的五個事件,所以崩潰循環會令清單一直保持滿載,計數便永遠不會增加。現在,檢查改為比較最新一次崩潰的時間。如果發佈遇到崩潰循環,它就會失敗,並還原先前的版本。
發佈在等待什麼
當你在 Mythex 上發佈一個帶有後端的應用程式時,新版本會被部署到我們託管平台的機器上,而健康檢查會判斷這次發佈是已上線還是失敗。如果失敗,回滾就會把先前的版本還原。
困難之處在於,「已啟動」並不等於「正在提供服務」。平台一旦機器啟動就會將其回報為已啟動。啟動後一秒就崩潰的應用程式會立即被重新啟動,過了片刻又會被視為已啟動。因此,健康檢查隨著時間推移增加了幾層檢查:
- 檢查兩次。 機器被回報為已啟動之後,稍候片刻再檢查一次。啟動時的崩潰,往往會在第二次檢查時被發現。
- 留意崩潰循環。 如果平台回報某部機器持續崩潰,而它已放棄重新啟動,就判定失敗。
- 計算發佈開始之後的崩潰次數。 兩次「已啟動」的檢查,可能跨越了一次重新啟動。因此,如果自發佈開始以來發生過任何崩潰,就再檢查第三次;如果崩潰次數比上一次檢查更多,就表示應用程式並沒有保持運行。
在這次事件中失效的,正是第三項檢查。
為何計數一直沒有增加
崩潰資訊來自每部機器的事件清單:啟動、結束、重新啟動。平台每部機器只保留最近五個事件。
想像一個應用程式像其中一個那樣,在每次啟動後 2.5 秒就崩潰。每個循環都會增加一次崩潰和一次啟動。經過幾個循環後,清單就滿了。從那時起,每個新事件都會擠走一個舊事件。清單不斷向前滑動,但它總是保持大致相同的組合:幾次崩潰,幾次啟動。
因此,「自發佈開始以來的崩潰次數」在每次檢查時都得出同一個數字。第二次檢查看到的可能是兩次崩潰;第三次檢查看到的也是兩次——雖然是不同的崩潰,但數目相同。檢查於是得出結論:崩潰已經停止,並讓這次發佈通過。
這種情況在 9 月 25 日、橫跨兩個應用程式中發生了三次。每次都是:
- 發佈被標示為已上線,
- 沒有執行回滾,因為沒有任何東西失敗,
- 我們的對帳程式(一項在事後檢查部署的後台工作)在幾分鐘後把這次部署判定為失敗,
- 而整個網站都返回 404。
如果發佈及時失敗,還是可以挽回:回滾會還原先前的版本。但如果發佈在被宣布為成功之後才幾分鐘才失敗,它就已經略過了回滾。
我們如何找到它
我們在本地重現了這個問題:取一個正常運作的 API,讓它進入休眠,然後以一項會在啟動時崩潰的改動重新發佈它。健康檢查把它標示為已上線。
這與正式環境的情況吻合:在三次發佈中,應用程式每次啟動後數秒都會死掉,但檢查所比較的崩潰次數,在兩次檢查之間從未增加。
修正方法:比較最新一次崩潰,而非比較次數
現在,檢查不再計算崩潰次數,而是查看自發佈開始以來最近一次崩潰的時間。
如果第二次檢查顯示自發佈開始以來有任何崩潰,檢查便會等待並再檢查第三次。如果第三次檢查中最新一次崩潰的時間晚於第二次檢查中的最新一次崩潰,就表示機器在這段時間內又再次崩潰了。它並沒有保持運行,發佈會以一則訊息失敗,指出機器自發佈以來反覆崩潰,而且仍在不斷重新啟動。
無論事件清單有多短,這個方法都行得通。五個事件的滑動視窗可能會掩蓋崩潰的總次數,但它無法掩蓋剛剛又發生了一次新的崩潰——最新的事件始終是被保留下來的那一個。
經過這項改動後,本地重現的情況表現正確:發佈失敗,並還原先前的映像檔。我們還加入了一項測試,模擬平台的行為——每次檢查都會增加一次崩潰和一次啟動,並把清單修剪為五個——然後確認判定結果是失敗,而不是通過。
我們從中學到的
- 了解你所計算的數據的形態。 在一個有上限的清單上計數,一旦清單滿了,這個數字就不再有意義。這個上限並非隱藏起來;我們只是沒有針對它來設計檢查。
- 優先問「有沒有發生新的事情?」,而不是「發生了多少次?」 最新的時間戳在清單被截短後仍然存在,總數則不然。
- 遲來的失敗比早來的失敗更糟。 同樣一個有問題的版本,如果發佈失敗並回滾,就不會造成損害;但如果它先被宣布為成功,就會令網站停擺。
- 在真實條件下重現問題。 這個缺陷需要一部機器已經崩潰了足夠多次,足以填滿它的事件清單。測試中一次快速的崩潰,是不會揭露它的。
這項修正與同一天的另一項修正相配合,後者確保回滾會把舊版本還原,而不是刪除它:失敗的重新發佈絕不會刪除運作中的後端。如果你自己的應用程式因為啟動時崩潰而無法發佈,發佈疑難排解文件,以及我們的除錯 AI 建立的應用程式指南,都是很好的起點。