2025/8/26 zbpack 重新部署時行為異常的事件報告

PanPan

近期 Zeabur 因受到 zbpack v1 既有基礎設施淘汰的影響,正在向所有使用者推送新一代建構系統(zbpack v2)的更新。升級過程中已知造成了一些相容性的問題,而這裡會說明問題的緣由、處理方式,以及如果你遇到這個問題時,可以怎麼緩解這個問題。

為什麼會全量推送新一代的建構系統?

Zeabur 背後的編譯基礎設施是這樣運作的:

zbpack structure

「registry v1」是使用 distribution/distribution 的 registry 架設的,由於其造成的種種問題,促使我們遷移到自行設計的 registry v2 方案:

  • 早期是採跟隨編譯機器的機制(也就是一台編譯機器對應一個 registry)。不過編譯機器都比較短期,且沒有一個比較好的方式限制「某個編譯機器的容器,必須只連屬於自己機器的 registry」,因此偶爾會遇到「registry 比編譯容器早結束」的問題,導致編譯失敗(同時 retry 機制也沒有太大效果)
  • 後期我們將 registry 遷移到比較長期的機器上,讓編譯機器中的容器,自行連接到經過負載平衡後的 registry。不過寫入 image manifest 的時候,負責上傳 blob 的 registry 可能實際上還沒有完成上傳,或者是背後 Cloudflare R2 在更新上有延時,導致 registry 報出 blob unknown 的錯誤並拒絕上傳 manifest,導致啟動時無法拉取映像
  • 此外,distribution/distribution 傾向做全域 blob 去重,導致我們這裡無法在不讀取所有 manifest 的情況下,了解哪些 blobs 是用不到的。官方提供的垃圾清理 (garbage collection) 工具是要求停機執行 (stop-the-world) 的,在 Zeabur 如此大的 registry 規模下無法運作,導致 R2 累積了非常大量的 blobs,並因此造成相當嚴重的效能問題
  • 另外也有眾多使用者反饋經由 registry v1 上傳的速度非常緩慢

我們將 registry v2 設計成「建構 OCI image」後「直接推上 R2 bucket」,接著再使用 Cloudflare Workers,撰寫一個可以將 bucket 中 OCI image 結構轉換成符合 OCI Distribution Specification 裡面 Pull API 的 read-only API。這樣一來,除了效能可以獲得極大提升、可以最大化 multipart 上傳的效率,也可以避免因 registry 造成的種種問題。與此同時,我們將 blob 的去重限定在 repo 內部,因此可以做到「刪掉一個 repo 就能 gc 掉其對應的 blobs」,極大簡化內部維護且無需 stop-the-world。

不過從上面的描述中,也能看到 registry v2 的 push 流程有著相當大的變化。這部分已經在 zbpack v2 中得到實作,但 zbpack v1 由於其複雜的推送流程,以及很大依賴 buildkit CLI 進行 image 的建構,導致實作這部分較為困難,因此近一個月,我們讓屬於 zbpack v1 的專案繼續使用 zbpack v1(registry v1)編譯,惟當使用者回報 zbpack v1 無法啟動時,才手動切換到 zbpack v2 上。

但當 registry v1 愈來愈不堪重負,出錯頻率節節攀升,導致類似工單愈來愈多時,我們不得不考慮將 zbpack v1 負責 image 的部分,轉換到 zbpack v2 上。

為什麼新的建構系統會有這麼多的問題?

眼尖的開發者,應該有注意到 zbpack (v1) 的 GitHub Repo 從 Archived 轉回維護狀態,並且提交了很多和 Dockerfile 相關的更新。實際上,這部分就是在為 zbpack v1 轉接到 zbpack v2 的相容層做準備。

我們希望維持 zbpack v1 既有的 Dockerfile 產生功能,但在產完 Dockerfile 後不使用 zbpack v1 內建的建構邏輯,而是切換到 zbpack v2 上。因此,我們將 v1 現有的 Dockerfile 產生功能改為公開函式,並讓建構服務使用這個函式產出 Dockerfile,再傳遞到執行 zbpack v2 的編譯機器上。

zbpack migration plan

不過 zbpack v1 畢竟最初是設計在編譯機器上運作的,有諸多部分是需要進行實作或轉接的:

  • zbpack v1 依賴很多環境變數,而我們不可能去改變建構服務的環境變數
  • zbpack v1 最初是設計為一次性執行的 CLI,直接接到建構服務可能會執行到 zbpack 內部的 panic 邏輯
  • zbpack v1 背後的編譯機器,也傳遞了很多特化的參數給 zbpack v1,我們在相容層中都需要一個一個實作

因此,我們實作了 zbpack v1 相容層,移植 zbpack v1 編譯機器對於 zbpack v1 呼叫的處理。絕大部分明顯的問題(如程式碼讀取),我們在全域推送前都已經解決了,且在測試機器上進行內部測試時,沒有遇到判斷失誤的狀況,同時也有派駐 on-call 工程師持續關注推送帶來的影響。然而在實際推送到所有機器上時,卻發現了諸多新相容層沒有考慮到的問題。舉例來說:

  • zbpack 需要的環境變數,沒有正確傳遞給 zbpack,導致 ZBPACK_ 開頭的變數失效
  • 專案的「根目錄」沒有正確傳遞給 zbpack v1,導致它一直從根目錄的內容進行判斷
  • 相容層使用的 zbpack v1 版本,在 Dockerfile 邏輯上有大改動,而測試沒有正確覆蓋,導致 ZBPACK_DOCKERFILE_NAME 行為與先前不一致
  • 很多使用者覺得 plan type 和 plan meta 的呈現很重要,但相容層沒有如以往般在 build logs 正確顯示
  • registry v2 從大陸連線非常緩慢

考慮到部分情境沒有其對應的測試環境,以及評估回退版本造成的影響可能更大,我們在 on call 時遇到這些問題時,決定將錯就錯,快速地執行「修正」、「測試」、「推送」的流程,同時也向客戶提供 workaround 緩解問題。我們在 8/26 - 8/27 這段期間很快地實作完整 zbpack 的相容層、實作了 plan type 和 plan meta 的展示,並給 registry v2 設計了大陸 CDN 來加速下載。

our new ui and infrastructure

非常感謝所有回報這些問題的客戶,讓我們發現相容層沒有涵蓋到的 edge cases 並促使我們研究並修正。

未來這個問題是否還會發生?

上面提出的問題,相容層均已實作或修正。如果有其他不完整的部分,也請開單讓我們的工程師處理。

目前已知的問題如下:

  • 新部署的連線埠可能會被改回預設的 port 8080 上,目前仍在調查原因。可以先手動設定 PORT=對外連線埠 (如 PORT=8080)緩解這個問題
  • 如果您使用 Dockerfile 部署,而 Dockerfile 沒有正確讀取的話,可以到「設定」 > 「Dockerfile」手動填入您想使用的 Dockerfile 內容。不過考慮到這個問題應當已經修正,也希望您可以開張工單讓我們調查原因

概觀本次事故,核心原因有幾個:

  • 內部測試使用的測試案例太少,缺少根目錄、Dockerfile name 相關的測試,誤認為「相容層已經涵蓋一切環境」並視為小改動進行全體推送
  • 任何涉及行為改變的功能(如「zbpack v1 相容層」),都應當實作和遵循類似於 zbpack v2 的 feature flag 選擇性加入機制,而非全體推送
  • 缺少對於大陸地區網路情況的認知,誤將國外的網路情況套用在大陸網路上,而沒有提前實作 CDN 機制
  • 應當針對小範圍客戶進行測試,並提前收集相關的回饋。

如果您是這次受到影響的客戶,我們可以提供影響時間折算的 credit 進行補償。未來我們在推送這部分功能時會更加謹慎,也非常感謝所有發現相容層問題的客戶。