- 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。先問便宜的那個。
修訂紀錄
- 首次發布