2025年11月23日
Kubernetes 探針怎麼設計?Liveness、Readiness、Startup 的責任劃分
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 等 較耗時的項目):
// Program.cs
builder.Services.AddHealthChecks()
.AddCheck<StartupHealthCheck>("startup", tags: new[] { "startup" });
app.MapHealthChecks("/probe/startup",
new HealthCheckOptions { Predicate = c => c.Tags.Contains("startup") });
// 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),不做
完整初始化邏輯:
// 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 則用較短的檢查週期:
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)時間, 再真正結束程序:
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,而不是直接關閉程序