← 回文章列表

2025年11月23日

Kubernetes 探針怎麼設計?Liveness、Readiness、Startup 的責任劃分

KubernetesASP.NET CoreHealth CheckSRE

Kubernetes 的三種探針(Liveness、Readiness、Startup)機制本身不難,官方文件 也寫得很清楚。真正常出問題的地方,是把不該放進某個探針的檢查放了進去—— 最常見的就是把「檢查資料庫連線」這種較重的邏輯放進 Liveness,結果資料庫一慢, 整個服務被 Kubernetes 誤判成掛掉,開始無限重啟,把本來只是慢的問題直接搞成中 斷。完整可執行的範例程式碼放在 dotnet-k8s-health-checks。

三種探針各自該檢查什麼

探針應檢查的面向失敗後的效果
Liveness(存活)進程是否卡死、是否陷入無限迴圈、主要資源是否已不可回復(如主執行緒崩潰)容器被 Kubernetes 重啟
Readiness(就緒)啟動流程是否已完成、依賴(DB、Redis、外部服務)是否連通、必要資源(cache、config)是否已載入Pod 從 Service 的 Endpoints 移除,暫停接收流量
Startup(啟動)啟動邏輯是否完成(如 migration、依賴初始化)成功前暫停 Liveness 判斷,避免應用還沒啟動完就被判定存活失敗而重啟

關鍵原則:Liveness 要極度輕量,只回答「進程還活著嗎」;真正檢查依賴是否可 用的邏輯,屬於 Readiness 或 Startup。

實作範例(ASP.NET Core)

Startup:啟動期間做完整的依賴初始化檢查(資料庫、Redis、Kafka topic 等 較耗時的項目):

C#
// Program.cs
builder.Services.AddHealthChecks()
    .AddCheck<StartupHealthCheck>("startup", tags: new[] { "startup" });

app.MapHealthChecks("/probe/startup",
    new HealthCheckOptions { Predicate = c => c.Tags.Contains("startup") });
C#
// StartupHealthCheck.cs(節錄)
public async Task<HealthCheckResult> CheckHealthAsync(
    HealthCheckContext context, CancellationToken cancellationToken = default)
{
    await DatabaseConnectionCheckAsync(cancellationToken);
    await RedisConnectionCheckAsync(cancellationToken);
    await KafkaConnectionCheckAsync(cancellationToken);

    return HealthCheckResult.Healthy("All dependencies initialized.");
}

Readiness:只做快速、輕量的可達性檢查(SELECT 1、Redis PING),不做 完整初始化邏輯:

C#
// ReadinessHealthCheck.cs(節錄)
public static volatile bool IsShutdownInitiated = false;

public async Task<HealthCheckResult> CheckHealthAsync(
    HealthCheckContext context, CancellationToken cancellationToken = default)
{
    if (IsShutdownInitiated)
        return HealthCheckResult.Unhealthy("App is stopping");

    await DatabaseConnectionCheckAsync(cancellationToken); // quick check
    await redis.GetDatabase().PingAsync();

    return HealthCheckResult.Healthy("Ready: dependencies are reachable.");
}

Deployment YAML:三種探針的典型設定,注意 failureThreshold 與 periodSeconds 的搭配——Startup 給足夠的容錯次數(範例中 30 次 × 10 秒 = 最 多容忍 5 分鐘啟動時間),Liveness/Readiness 則用較短的檢查週期:

YAML
livenessProbe:
  httpGet: { path: /probe/live, port: 8080 }
  initialDelaySeconds: 10
  periodSeconds: 10
  failureThreshold: 3

readinessProbe:
  httpGet: { path: /probe/ready, port: 8080 }
  initialDelaySeconds: 5
  periodSeconds: 5
  failureThreshold: 3

startupProbe:
  httpGet: { path: /probe/startup, port: 8080 }
  periodSeconds: 10
  failureThreshold: 30

容易漏掉的一步:優雅退出(Graceful Shutdown)

Pod 被終止時,Kubernetes 會先送出 SIGTERM,但這個當下 Service 的 Endpoints 不會立刻更新——如果應用馬上就地關閉,仍在路上的請求會直接被中斷。正確順序 是收到 SIGTERM 後,先把 Readiness 轉成 Unhealthy,等一段排空(drain)時間, 再真正結束程序:

C#
app.Lifetime.ApplicationStopping.Register(() =>
{
    // 1. 讓 Readiness 立刻回傳 Unhealthy,
    //    Pod 會從 Service Endpoints 移除,不再收到新流量
    ReadinessHealthCheck.IsShutdownInitiated = true;

    // 2. 等待 Endpoints 更新擴散到所有 Node/Load Balancer,
    //    讓既有連線把手上的請求做完(drain)
    Thread.Sleep(TimeSpan.FromSeconds(10));

    // 3. 排空結束才真正進入資源釋放與終止流程
});

Drain 時間要抓多長,取決於應用最長的請求處理時間與 Endpoints 更新擴散的延 遲,10~30 秒是常見起點;如果有更長的清理作業,要另外搭配 terminationGracePeriodSeconds 與 preStop hook,避免清理做到一半就被 Kubernetes 強制 kill。

總結

  • Startup 解決「啟動慢被 Liveness 誤判」的問題
  • Liveness 只保證容器能自我療癒(卡死時重啟),檢查邏輯必須極輕量
  • Readiness 控制流量路由,決定 Pod 準備好了沒、要不要收新請求
  • 關閉服務時,Readiness 要先轉 Unhealthy 再等 drain,而不是直接關閉程序