可觀測性:三態健康檢查與 tracing 抽象層

三態健康檢查(Healthy / Degraded / Unhealthy)比 up/down 更貼近 K8s probe。tracing 則只依賴一個 Tracer 介面,OTel 當 adapter 接上。這篇講介面驅動的可觀測性。

前置:A3 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 等全部跑完。這是 A4 的並行加 A5 的同步原語,落在維運面的用法。

Tracer 介面 + OTel adapter

這節是 A3 介面驅動的活教材。框架只依賴一個小介面:

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,只是換一個實作,業務碼一行不改,這就是 A3 的 adapter。adapter 內部用一張 severityMap,把 gortex 的 8 級嚴重性(DEBUG=10EMERGENCY=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 往下傳。整條鏈路就靠 context 串起來,不用全域變數、也不用手動到處傳。

重點整理

  • 三態健康(Healthy / Degraded / Unhealthy)比 up/down 貼近 K8s probe;聚合 worst-wins。
  • check 背景定時跑 + 快取;執行時 WaitGroup 並行、各自 timeout,慢的不拖垮快的。
  • tracing 只依賴 Tracer / EnhancedTracer 介面(介面嵌入);NoOp / Simple / OTel 三種實作可抽換,正是 A3 的 adapter。
  • span 用私有 contextKey + context.WithValue 沿鏈路傳遞;8 級 severity 對映 OTel。
  • 可觀測性建在介面上,換後端不動業務碼。

原始碼:yshengliao/gortex


大綱 Sheng,內文 Claude 協助 · 環境 Go 1.25(gortex go.mod)· 本系列為事後回填整理 · 列入 20260613 blog 翻新計劃,新漆未乾。