可觀測性:三態健康檢查與 tracing 抽象層
三態健康檢查(Healthy / Degraded / Unhealthy)比 up/down 更貼近 K8s probe。tracing 則只依賴一個 Tracer 介面,OTel 當 adapter 接上。這篇講介面驅動的可觀測性。
前置閱讀:〈interface 與組合〉
三態健康檢查
傳統健康檢查只有 up/down,但真實世界常是「還能跑但快不行了」。gortex 用三態,貼近 K8s 的 readiness / liveness:
const (
HealthStatusHealthy HealthStatus = "healthy"
HealthStatusDegraded HealthStatus = "degraded" // 還能服務,但接近極限
HealthStatusUnhealthy HealthStatus = "unhealthy"
)
聚合採 worst-wins:任一 check 是 Unhealthy,整體就 Unhealthy;否則有 Degraded 就 Degraded;全好才 Healthy。內建的 MemoryHealthCheck 示範得很清楚:超過上限是 Unhealthy,到 80% 就先報 Degraded 預警。
並行檢查與逾時
HealthChecker 背景開一個 goroutine 按 interval 定時跑(runChecks + ticker),結果快取起來,/healthz 那類端點直接讀快取、不每次現跑。
實際跑的時候所有 check 並行,各自帶 timeout,一個慢 check 不會拖垮其他:
for name, check := range checks {
wg.Add(1)
go func(n string, c HealthCheck) {
defer wg.Done()
checkCtx, cancel := context.WithTimeout(ctx, hc.timeout)
defer cancel()
result := c(checkCtx) // 每個 check 各自的 deadline
// ...存結果
}(name, check)
}
wg.Wait()
sync.WaitGroup 等全部跑完。這是〈goroutine 與 channel〉的並行加〈記憶體與併發原語〉的同步原語,落在維運面的用法。
Tracer 介面 + OTel adapter
這節是 interface 那篇講的介面驅動設計的活教材。框架只依賴一個小介面:
type Tracer interface {
StartSpan(ctx context.Context, operation string) (context.Context, *Span)
FinishSpan(span *Span)
AddTags(span *Span, tags map[string]string)
SetStatus(span *Span, status SpanStatus)
}
type EnhancedTracer interface { // 介面嵌入
Tracer
StartEnhancedSpan(ctx context.Context, operation string) (context.Context, *EnhancedSpan)
}
附三種實作:NoOpTracer(關掉 tracing)、SimpleTracer(in-memory,開發用)、OTelTracerAdapter(接 OpenTelemetry)。要從 in-memory 換到 Jaeger / OTel,只是換一個實作,業務碼一行不改,這就是 interface 那篇講的 adapter。adapter 內部用一張 severityMap,把 gortex 的 8 級嚴重性(DEBUG=10 … EMERGENCY=80)對映到 OTel 的屬性。
Context propagation
span 怎麼跟著請求一路傳下去?用 context,而且用私有的 contextKey 型別避免 key 撞車:
type contextKey string
const spanContextKey contextKey = "span"
func ContextWithSpan(ctx context.Context, span *Span) context.Context {
return context.WithValue(ctx, spanContextKey, span)
}
func SpanFromContext(ctx context.Context) *Span { /* ctx.Value(spanContextKey) */ }
StartSpan 先用 SpanFromContext 取出父 span、建子 span、再把子 span 放回 ctx 往下傳。整條 call chain 就靠 context 串起來,不用全域變數、也不用手動到處傳。
補充筆記
- 三態健康(Healthy / Degraded / Unhealthy)比 up/down 貼近 K8s probe;聚合 worst-wins。
- check 背景定時跑 + 快取;執行時
WaitGroup並行、各自 timeout,慢的不拖垮快的。 - tracing 只依賴
Tracer/EnhancedTracer介面(介面嵌入);NoOp / Simple / OTel 三種實作可抽換,正是 interface 那篇的 adapter。 - span 用私有
contextKey+context.WithValue沿 call chain 傳遞;8 級 severity 對映 OTel。 - 可觀測性建在介面上,換後端不動業務碼。
原始碼:yshengliao/gortex。
想法與技術判斷出自 Sheng,和 Claude 一起起草 · 環境 Go 1.25(gortex go.mod)· 本系列為事後回填整理 · 列入 20260613 blog 翻新計劃,新漆未乾。