Skip to content
所有文章
工程 · 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)都繼承了這個錯誤,讓它看起來更有根據。


參考來源:

↑↓ 移動 ↵ 開啟 esc 關閉