Skip to content
All writing
Engineering · 1 min read

一次 Golang 版本升級引爆的除錯連鎖反應

一次自動依賴更新把 Golang 版本升到 1.25,沿著 Docker、Alpine、libvips 一路引爆。四層驗證都指向上游壞了,但四層用的是同一張有問題的圖。

四層驗證都同意「上游壞了」,但四層用的是同一張有問題的圖。四個一致的結論不是四份證據,是一份證據被數了四次。

Dependabot(GitHub 上自動幫你開 PR 升級依賴的機器人)把 go.mod(Golang 的依賴設定檔)從 1.20 升到 1.25,看起來只是語言版本升級。但 golang:1.25-alpine 背後綁的是更新的 Alpine,Alpine 帶來更新的 libvips,而更新的 libvips 裡的 cgif 編碼器會合併相同影格。GIF 影格數測試紅了,一場連鎖反應就此開始。

Dependabot go.mod 1.20 → 1.25 golang:1.25-alpine Alpine 3.22 libvips 8.15.2 → 8.18.4 TestEncodeGIF_* ✗ 4 格輸入 → 1 格輸出
一次版本升級沿著三層隱式耦合一路連鎖。

1:版本的陷阱

Golang × Alpine 是一個滑動視窗,不是固定組合。升級 Golang 版本就移動了整條依賴鏈。在手動拼湊 base image 之前,先查 tag 矩陣確認官方是否已經發布你要的組合。如果矩陣裡真的沒有,才有理由把 Golang 工具鏈(編譯器與標準函式庫)嫁接到你需要的 base image

2:尋找元兇(以及它如何騙了我們)

我們用逐層下降的方式排查:應用層 → 封裝層 → raw CLI → 版本 bisect(用二分法逐版本找出壞掉的那個),每一層都紅。看起來鐵證如山:libvips 8.18 回歸了(regression:之前正常、更新後壞掉)。

問題是,每一層都餵了同一張相同影格的測試圖。被污染的測試基準讓四層驗證說了同一個謊。直到我們認識到 GIF 編碼器合併相同影格是合法行為,換上不同影格的測試圖,同一版 libvips 瞬間全綠。

3:該有的紀律

測試結果是關於輸入的事實,不是關於系統的事實。當一個測試結果即將驅動結構性決策(釘版、換 base image、引入 graft),先用生產形態的輸入重新驗證。這個閘門幾乎不會啟動,但它啟動的時刻,正好是判斷錯誤最昂貴的時刻。

4:止血(修正後)

我們釘到 alpine:3.20,附了教科書等級的退場筆記。但 3.20 已經 EOL(End of Life,官方不再發布安全修補),apk upgrade 靜默回傳 exit 0,實際零修補。一個正確性的釘版,用 CVE(公開揭露的安全漏洞)修補流換來了一個不存在的修正。

兩道閘門都被跳過了,而任何一道都能獨立終止整個事件:Gate 1 因為「回歸」不是真的,Gate 2 因為 3.20 已經 EOL。

結局

升級是真的;GIF「回歸」不是。最終出貨:支援中的 golang:1.25.11-alpine3.22,graft 移除,測試改用不同影格的 fixture(測試用的輸入資料)。

bug 在 fixture 裡,而每一層在它下游疊上去的嚴謹——bisect、釘版、graft,都繼承了這個錯誤,讓它看起來更有根據。


參考來源:

Tags #devops #debugging #docker
// connect

Be brave | Be wise | Be grateful

21 BreakinCode

// elsewhere
LinkedInMedium (lang: en)Youtube
wh:~$William Hung· © 2026 Taipei · GMT+8 · Available for collaboration