撳兩次發布,而家只會發布一次
同一個 app 嘅三個發布請求,相隔 84 毫秒抵達我哋嘅 API,每個都觸發咗該 app 嘅一次完整部署。佢哋互相爭奪同一個託管平台,而結果是幾個發布因為另一個已經搶先,而失敗;唯一可見嘅徵兆,就係一封「發布失敗」嘅電郵,而 app 本身最終都正常。而家,如果同一 app 嘅發布喺另一個發布運行期間抵達,就唔會再啟動第二次建構。資料庫會決定邊個請求勝出,其他請求則會得到實際嘅答案:發布已經在進行中。
發生咗咩事
當你喺 Mythex 發布一個 app,API 會為佢記錄一個部署,並要求我哋嘅編排器(即係執行建構嘅服務)去建構並發布佢。發布可以由發布按鈕開始、由另一個瀏覽器分頁開始,或者由代理在你要求發布時開始。連按兩下、兩個分頁,或者代理請求被送出兩次,都可能同時為同一個 app 產生多次發布。
呢次個案中,三個請求喺 84 毫秒內抵達。每個都運行咗一次完整部署。喺託管平台上面,同一個 app 嘅發布會喺同一秒內建立,而其中三個因為其他嘅已經在進行中而失敗。之後一個發布卡住,但 app 之後運作正常。
咁樣令問題好容易被忽略。用戶見到嘅係一個正常運作嘅 app。問題只會以發布失敗嘅電郵形式浮現。
點解冇嘢阻止佢
發布代碼確實有一個步驟,會檢查該項目進行中嘅部署。但佢所做嘅,係喺開始之前,將所有進行中嘅部署標記為失敗,理由是「被新發布取代」。佢本意係清理因崩潰程序而遺留下嚟嘅建構。
實際上,就係一個喺建構中途抵達嘅第二個發布,會宣告第一個已死,然後啟動自己嘅建構。兩個建構都會針對同一個 app 完整運行。冇任何鎖;反而有一個步驟,主動幫忙清出一條路,令競爭得以發生。
對於一個 app 嘅第一次發布,仲有一個更奇怪嘅徵狀。由於仲未有部署記錄可以重用,兩個同時發生嘅第一次發布,都會試圖插入同一筆記錄。資料庫對 app 嘅連結名稱(即 slug,例如 your-app.mythex.ai 入面嘅 your-app)有唯一索引,所以第二次插入會失敗。API 將呢個失敗翻譯成「呢個連結名稱已被使用」,並建議用戶改名,但該 app 其實正正喺嗰一刻,以嗰個名稱成功發布緊。
修正:宣告即係鎖
而家認領一筆部署記錄,就係一個單一嘅條件式資料庫更新:只有當冇人正在發布該記錄時,先將它取過來並標記為建構中。因為係一個陳述式,資料庫會解決同一毫秒內抵達嘅請求。只有一個請求會改變該資料列,其他請求會看到冇資料列被改變,於是停止,永遠唔會到達編排器。
鎖亦需要一個出口,以應付持有者死亡嘅情況。如果 API 喺發布途中重新啟動,該資料列就會永遠停留喺「建構中」,而 app 就會再冇辦法發布。所以,如果一個資料列嘅擁有者已經停止發送心跳,就可以被重新認領。正在運行嘅發布每 30 秒更新一次該資料列,而沉默五分鐘就視為死亡。呢個門檻來自心跳,而唔係估計建構需要幾耐。用戶自己嘅 Dockerfile 建構可以合理咁運行好長時間,而一個仍然持續發送心跳嘅慢速建構,會保留自己嘅認領。
喺發布之前,已經唔會再運行將所有進行中部署標記為失敗嘅步驟。只有被死掉嘅程序遺留嘅資料列,才會被清理。
誠實告知輸家
輸掉嘅請求而家會收到一個清楚嘅答案:publish_in_progress,並附上正在運行中嘅部署 ID。每個可以啟動發布嘅地方都會處理呢個情況:
- 工作區會跟住已經運行中嘅建構,而唔會喺上面顯示錯誤。
- 代理會被告知該 app 嘅發布已經在進行中,並叫佢等待,而唔係被告知有嘢失敗。以前呢種資訊可能會令佢去「修正」Dockerfile 並重試。
- 第一次發布遇到連結名稱衝突會被檢查:如果佔住該名稱嘅資料列,屬於本項目自己正在進行中嘅發布,答案就係「已經發布緊」,而唔係「該名稱已被使用」。真正與另一個項目發生衝突,仍然會收到改名嘅訊息。
點樣驗證
我哋寫咗一個重現腳本:喺真實資料庫同一個假扮嘅編排器上,同時運行同一個 app 嘅三次發布,然後計算編排器被要求建構幾次。呢個測試涵蓋第一次發布,以及一個正在運行中 app 嘅重新發布。修正之前,計數係兩次部署。修正之後,係一次。
腳本亦涵蓋咗逃生出口:佢將該 app 嘅資料列標記為建構中,但十分鐘都冇心跳,然後再發布一次,並檢查新嘅發布會認領該資料列並成功運行。
單元測試將鎖限制喺它必須做到嘅兩件事:同時三次重新發布之中,只有一個可以通過,另外兩個會被告知發布正在進行中;以及擁有者已經死亡嘅建構可以被認領。另一個測試則確認,新嘗試唔會沿用上一次嘗試嘅錯誤訊息。
我哋學到嘅嘢
- 清理代碼有時會隱藏競爭。 「先將所有進行中嘅嘢標記為失敗,然後開始」聽落好似衛生措施。但如果有兩個呼叫者,就等於每一個都會取消另一個。
- 讓資料庫負責仲裁。 先讀後寫,會留下一個容許另一個請求插入嘅空隙。一個單一嘅條件式更新就冇呢個空隙。
- 每個鎖都需要與存活狀態掛鉤嘅過期機制。 冇咗佢,崩潰就會變成 app 卡住。
- 錯誤訊息就係指示。 「該名稱已被使用」令人去做錯誤嘅事。正確嘅訊息更簡單,而且真實。
更多關於發布嘅資料,可以睇文件:點樣發布。另一個相關嘅修正係:你已發布嘅 app 喺重新發布時仍然保持運行。