包包baobaolin.com
date
entry
011
topic
tooling
rev

每個服務都該有 /version:先確認它在跑哪個 commit

修好了、推上去了、問題還在。這時候有兩個可能:程式沒修好,或那份程式根本沒在線上跑。多數人會先假設第一個,然後花兩小時重讀自己剛寫的邏輯。

「我推上去了」和「它在跑」中間隔著一整條部署鏈:CI 有沒有觸發、build 有沒有成功、image 有沒有推到 registry、容器有沒有換版、快取有沒有清。任何一節斷掉,你都會看到跟「修錯了」一模一樣的症狀。

這個歧義可以用一個端點消掉。每個後端服務都給它一條 /version

curl -s https://api.example.com/version
{
  "git_sha":    "dea001c",
  "git_branch": "main",
  "build_time": "2026-09-05T11:32:04Z",
  "version":    "1.0.0"
}

對照 git log,三十秒之內你就知道要不要繼續讀程式碼。sha 對得上,問題在邏輯或設定;對不上,問題在部署——這兩條路要查的東西完全不同。

怎麼把 sha 烤進 binary

Go 用 -ldflags -X 在編譯時寫進變數。不需要任何函式庫:

go build -ldflags "\
  -X main.gitSHA=$(git rev-parse --short HEAD) \
  -X main.gitBranch=$(git rev-parse --abbrev-ref HEAD) \
  -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  -o server ./cmd/server
var (
    gitSHA    = "unknown"
    gitBranch = "unknown"
    buildTime = "unknown"
)

r.GET("/version", func(c *gin.Context) {
    c.JSON(200, gin.H{
        "git_sha": gitSHA, "git_branch": gitBranch,
        "build_time": buildTime, "version": appVersion,
    })
})

預設值寫 "unknown" 而不是空字串——看到 unknown 就知道這個 binary 不是走正常流程 build 出來的,那本身就是一條線索。

在容器裡的話,sha 從 build arg 傳進去:

ARG GIT_SHA=unknown
RUN go build -ldflags "-X main.gitSHA=${GIT_SHA}" -o /server ./cmd/server
docker build --build-arg GIT_SHA=$(git rev-parse --short HEAD) -t app:prod-$(git rev-parse --short HEAD) .

順帶把 sha 也放進 image tag。這樣 docker ps 一眼就看得出哪個容器跑哪版,不用進去問。

前端更需要這個

後端至少還有 docker ps 可以問。前端沒有——使用者瀏覽器裡跑的是哪一版,你完全看不到,而且舊版還在跑是常態,不是例外。

build 時把 sha 寫進 HTML 就能問:

<meta name="build-sha" content="dea001c" />
curl -s https://app.example.com/ | grep build-sha

這條 curl 回答的是「CDN 現在發的 HTML 是哪一版」。如果它是舊的,那就不是程式的問題,是快取或部署的問題——同樣三十秒定位。

再往前一步:把 sha 帶進 log 和 header

/version 回答的是「現在」。事故調查要問的常常是「當時」——三天前那批 500 發生的時候,跑的是哪一版?

兩個做法,成本都很低:

  • 服務啟動時打一行 log,內容是完整的 version 資訊。這樣每次重啟在 log 裡都有一個時間戳記的版本標記,事故時間軸可以往回找最近的那次啟動
  • 加一個 response header,例如 X-Build-SHA。客戶回報問題時附上的 curl -i 或 DevTools 截圖,就自帶版本資訊,不用再回頭問「你什麼時候試的」

第二個在跨團隊的情境特別有用。對方回報「你們 API 壞了」的時候,那個 header 直接告訴你他打到的是哪一版——省掉一輪來回。

不要回一個手維護的版本號

很多服務的 /version 回的是 {"version": "1.0.0"},而那個字串寫在某個常數裡,上次更新是兩年前。這種端點比沒有更糟,因為它看起來像是回答了問題。

判準很簡單:這個值必須由 build 產生,不能由人維護。人維護的欄位會過期,而且過期的時候不會通知任何人——這跟存下來的 zone id 或漂移的 schema 是同一種失效方式。

/version 要不要公開?我的做法是公開。它洩漏的資訊是「你們用 git、現在跑某個 short sha」,攻擊者拿不到什麼——除非你的 repo 是公開的,那才需要考慮。相對地,把它藏在認證後面,代表出事的當下最需要它的人(客服、客戶端工程師、你自己在別台機器上)拿不到。

它也是部署有沒有生效的探針

有了這條端點,部署腳本的最後一步就可以變成驗證而不是祈禱:

EXPECTED=$(git rev-parse --short HEAD)
for i in $(seq 1 30); do
  ACTUAL=$(curl -s https://api.example.com/version | jq -r .git_sha)
  [ "$ACTUAL" = "$EXPECTED" ] && { echo "deployed $ACTUAL"; exit 0; }
  sleep 5
done
echo "TIMEOUT: still serving $ACTUAL, expected $EXPECTED" >&2
exit 1

這段的價值不在成功的時候,在失敗的時候:它把「部署完成」從「腳本沒有噴錯」改成「線上真的換版了」。這兩件事的差距,就是用狀態碼判斷健康與否和真的比對內容的差距。

如果只記得一件事

偵錯的第一個問題不該是「哪裡寫錯了」,而是「現在跑的是我以為的那份程式嗎」。 前者要花時間讀,後者是一條 curl。先問便宜的那個。

修訂紀錄

  1. 首次發布