[{"data":1,"prerenderedAt":923},["ShallowReactive",2],{"article-\u002Farticles\u002Fdotnet-k8s-health-checks":3},{"id":4,"title":5,"body":6,"date":909,"description":910,"excerpt":911,"extension":912,"meta":913,"navigation":198,"path":914,"seo":915,"stem":916,"tags":917,"__hash__":922},"articles\u002Farticles\u002Fdotnet-k8s-health-checks.md","Kubernetes 探針怎麼設計？Liveness、Readiness、Startup 的責任劃分",{"type":7,"value":8,"toc":903},"minimark",[9,26,30,102,108,112,117,252,378,391,557,571,757,761,768,863,874,877,899],[10,11,12,13,17,18,25],"p",{},"Kubernetes 的三種探針（Liveness、Readiness、Startup）機制本身不難，官方文件\n也寫得很清楚。真正常出問題的地方，是",[14,15,16],"strong",{},"把不該放進某個探針的檢查放了進去","——\n最常見的就是把「檢查資料庫連線」這種較重的邏輯放進 Liveness，結果資料庫一慢，\n整個服務被 Kubernetes 誤判成掛掉，開始無限重啟，把本來只是慢的問題直接搞成中\n斷。完整可執行的範例程式碼放在\n",[19,20,24],"a",{"href":21,"rel":22},"https:\u002F\u002Fgithub.com\u002Ftom3019\u002Fdotnet-k8s-health-checks",[23],"nofollow","dotnet-k8s-health-checks","。",[27,28,29],"h2",{"id":29},"三種探針各自該檢查什麼",[31,32,33,49],"table",{},[34,35,36],"thead",{},[37,38,39,43,46],"tr",{},[40,41,42],"th",{},"探針",[40,44,45],{},"應檢查的面向",[40,47,48],{},"失敗後的效果",[50,51,52,70,88],"tbody",{},[37,53,54,61,64],{},[55,56,57,60],"td",{},[14,58,59],{},"Liveness","（存活）",[55,62,63],{},"進程是否卡死、是否陷入無限迴圈、主要資源是否已不可回復（如主執行緒崩潰）",[55,65,66,67],{},"容器被 Kubernetes ",[14,68,69],{},"重啟",[37,71,72,78,81],{},[55,73,74,77],{},[14,75,76],{},"Readiness","（就緒）",[55,79,80],{},"啟動流程是否已完成、依賴（DB、Redis、外部服務）是否連通、必要資源（cache、config）是否已載入",[55,82,83,84,87],{},"Pod 從 Service 的 Endpoints ",[14,85,86],{},"移除","，暫停接收流量",[37,89,90,96,99],{},[55,91,92,95],{},[14,93,94],{},"Startup","（啟動）",[55,97,98],{},"啟動邏輯是否完成（如 migration、依賴初始化）",[55,100,101],{},"成功前暫停 Liveness 判斷，避免應用還沒啟動完就被判定存活失敗而重啟",[10,103,104,105],{},"關鍵原則：",[14,106,107],{},"Liveness 要極度輕量，只回答「進程還活著嗎」；真正檢查依賴是否可\n用的邏輯，屬於 Readiness 或 Startup。",[27,109,111],{"id":110},"實作範例aspnet-core","實作範例（ASP.NET Core）",[10,113,114,116],{},[14,115,94],{},"：啟動期間做完整的依賴初始化檢查（資料庫、Redis、Kafka topic 等\n較耗時的項目）：",[118,119,124],"pre",{"className":120,"code":121,"language":122,"meta":123,"style":123},"language-csharp shiki shiki-themes github-dark","\u002F\u002F Program.cs\nbuilder.Services.AddHealthChecks()\n    .AddCheck\u003CStartupHealthCheck>(\"startup\", tags: new[] { \"startup\" });\n\napp.MapHealthChecks(\"\u002Fprobe\u002Fstartup\",\n    new HealthCheckOptions { Predicate = c => c.Tags.Contains(\"startup\") });\n","csharp","",[125,126,127,136,150,193,200,218],"code",{"__ignoreMap":123},[128,129,132],"span",{"class":130,"line":131},"line",1,[128,133,135],{"class":134},"sAwPA","\u002F\u002F Program.cs\n",[128,137,139,143,147],{"class":130,"line":138},2,[128,140,142],{"class":141},"s95oV","builder.Services.",[128,144,146],{"class":145},"svObZ","AddHealthChecks",[128,148,149],{"class":141},"()\n",[128,151,153,156,159,162,165,168,172,175,178,181,185,188,190],{"class":130,"line":152},3,[128,154,155],{"class":141},"    .",[128,157,158],{"class":145},"AddCheck",[128,160,161],{"class":141},"\u003C",[128,163,164],{"class":145},"StartupHealthCheck",[128,166,167],{"class":141},">(",[128,169,171],{"class":170},"sU2Wk","\"startup\"",[128,173,174],{"class":141},", ",[128,176,177],{"class":145},"tags",[128,179,180],{"class":141},": ",[128,182,184],{"class":183},"snl16","new",[128,186,187],{"class":141},"[] { ",[128,189,171],{"class":170},[128,191,192],{"class":141}," });\n",[128,194,196],{"class":130,"line":195},4,[128,197,199],{"emptyLinePlaceholder":198},true,"\n",[128,201,203,206,209,212,215],{"class":130,"line":202},5,[128,204,205],{"class":141},"app.",[128,207,208],{"class":145},"MapHealthChecks",[128,210,211],{"class":141},"(",[128,213,214],{"class":170},"\"\u002Fprobe\u002Fstartup\"",[128,216,217],{"class":141},",\n",[128,219,221,224,227,230,233,236,239,242,245,247,249],{"class":130,"line":220},6,[128,222,223],{"class":183},"    new",[128,225,226],{"class":145}," HealthCheckOptions",[128,228,229],{"class":141}," { Predicate ",[128,231,232],{"class":183},"=",[128,234,235],{"class":145}," c",[128,237,238],{"class":183}," =>",[128,240,241],{"class":141}," c.Tags.",[128,243,244],{"class":145},"Contains",[128,246,211],{"class":141},[128,248,171],{"class":170},[128,250,251],{"class":141},") });\n",[118,253,255],{"className":120,"code":254,"language":122,"meta":123,"style":123},"\u002F\u002F StartupHealthCheck.cs（節錄）\npublic async Task\u003CHealthCheckResult> CheckHealthAsync(\n    HealthCheckContext context, CancellationToken cancellationToken = default)\n{\n    await DatabaseConnectionCheckAsync(cancellationToken);\n    await RedisConnectionCheckAsync(cancellationToken);\n    await KafkaConnectionCheckAsync(cancellationToken);\n\n    return HealthCheckResult.Healthy(\"All dependencies initialized.\");\n}\n",[125,256,257,262,287,312,317,328,337,347,352,372],{"__ignoreMap":123},[128,258,259],{"class":130,"line":131},[128,260,261],{"class":134},"\u002F\u002F StartupHealthCheck.cs（節錄）\n",[128,263,264,267,270,273,275,278,281,284],{"class":130,"line":138},[128,265,266],{"class":183},"public",[128,268,269],{"class":183}," async",[128,271,272],{"class":145}," Task",[128,274,161],{"class":141},[128,276,277],{"class":145},"HealthCheckResult",[128,279,280],{"class":141},"> ",[128,282,283],{"class":145},"CheckHealthAsync",[128,285,286],{"class":141},"(\n",[128,288,289,292,295,297,300,303,306,309],{"class":130,"line":152},[128,290,291],{"class":145},"    HealthCheckContext",[128,293,294],{"class":145}," context",[128,296,174],{"class":141},[128,298,299],{"class":145},"CancellationToken",[128,301,302],{"class":145}," cancellationToken",[128,304,305],{"class":183}," =",[128,307,308],{"class":183}," default",[128,310,311],{"class":141},")\n",[128,313,314],{"class":130,"line":195},[128,315,316],{"class":141},"{\n",[128,318,319,322,325],{"class":130,"line":202},[128,320,321],{"class":183},"    await",[128,323,324],{"class":145}," DatabaseConnectionCheckAsync",[128,326,327],{"class":141},"(cancellationToken);\n",[128,329,330,332,335],{"class":130,"line":220},[128,331,321],{"class":183},[128,333,334],{"class":145}," RedisConnectionCheckAsync",[128,336,327],{"class":141},[128,338,340,342,345],{"class":130,"line":339},7,[128,341,321],{"class":183},[128,343,344],{"class":145}," KafkaConnectionCheckAsync",[128,346,327],{"class":141},[128,348,350],{"class":130,"line":349},8,[128,351,199],{"emptyLinePlaceholder":198},[128,353,355,358,361,364,366,369],{"class":130,"line":354},9,[128,356,357],{"class":183},"    return",[128,359,360],{"class":141}," HealthCheckResult.",[128,362,363],{"class":145},"Healthy",[128,365,211],{"class":141},[128,367,368],{"class":170},"\"All dependencies initialized.\"",[128,370,371],{"class":141},");\n",[128,373,375],{"class":130,"line":374},10,[128,376,377],{"class":141},"}\n",[10,379,380,382,383,386,387,390],{},[14,381,76],{},"：只做快速、輕量的可達性檢查（",[125,384,385],{},"SELECT 1","、Redis ",[125,388,389],{},"PING","），不做\n完整初始化邏輯：",[118,392,394],{"className":120,"code":393,"language":122,"meta":123,"style":123},"\u002F\u002F ReadinessHealthCheck.cs（節錄）\npublic static volatile bool IsShutdownInitiated = false;\n\npublic async Task\u003CHealthCheckResult> CheckHealthAsync(\n    HealthCheckContext context, CancellationToken cancellationToken = default)\n{\n    if (IsShutdownInitiated)\n        return HealthCheckResult.Unhealthy(\"App is stopping\");\n\n    await DatabaseConnectionCheckAsync(cancellationToken); \u002F\u002F quick check\n    await redis.GetDatabase().PingAsync();\n\n    return HealthCheckResult.Healthy(\"Ready: dependencies are reachable.\");\n}\n",[125,395,396,401,426,430,448,466,470,478,495,499,511,531,536,552],{"__ignoreMap":123},[128,397,398],{"class":130,"line":131},[128,399,400],{"class":134},"\u002F\u002F ReadinessHealthCheck.cs（節錄）\n",[128,402,403,405,408,411,414,417,419,423],{"class":130,"line":138},[128,404,266],{"class":183},[128,406,407],{"class":183}," static",[128,409,410],{"class":183}," volatile",[128,412,413],{"class":183}," bool",[128,415,416],{"class":145}," IsShutdownInitiated",[128,418,305],{"class":183},[128,420,422],{"class":421},"sDLfK"," false",[128,424,425],{"class":141},";\n",[128,427,428],{"class":130,"line":152},[128,429,199],{"emptyLinePlaceholder":198},[128,431,432,434,436,438,440,442,444,446],{"class":130,"line":195},[128,433,266],{"class":183},[128,435,269],{"class":183},[128,437,272],{"class":145},[128,439,161],{"class":141},[128,441,277],{"class":145},[128,443,280],{"class":141},[128,445,283],{"class":145},[128,447,286],{"class":141},[128,449,450,452,454,456,458,460,462,464],{"class":130,"line":202},[128,451,291],{"class":145},[128,453,294],{"class":145},[128,455,174],{"class":141},[128,457,299],{"class":145},[128,459,302],{"class":145},[128,461,305],{"class":183},[128,463,308],{"class":183},[128,465,311],{"class":141},[128,467,468],{"class":130,"line":220},[128,469,316],{"class":141},[128,471,472,475],{"class":130,"line":339},[128,473,474],{"class":183},"    if",[128,476,477],{"class":141}," (IsShutdownInitiated)\n",[128,479,480,483,485,488,490,493],{"class":130,"line":349},[128,481,482],{"class":183},"        return",[128,484,360],{"class":141},[128,486,487],{"class":145},"Unhealthy",[128,489,211],{"class":141},[128,491,492],{"class":170},"\"App is stopping\"",[128,494,371],{"class":141},[128,496,497],{"class":130,"line":354},[128,498,199],{"emptyLinePlaceholder":198},[128,500,501,503,505,508],{"class":130,"line":374},[128,502,321],{"class":183},[128,504,324],{"class":145},[128,506,507],{"class":141},"(cancellationToken); ",[128,509,510],{"class":134},"\u002F\u002F quick check\n",[128,512,514,516,519,522,525,528],{"class":130,"line":513},11,[128,515,321],{"class":183},[128,517,518],{"class":141}," redis.",[128,520,521],{"class":145},"GetDatabase",[128,523,524],{"class":141},"().",[128,526,527],{"class":145},"PingAsync",[128,529,530],{"class":141},"();\n",[128,532,534],{"class":130,"line":533},12,[128,535,199],{"emptyLinePlaceholder":198},[128,537,539,541,543,545,547,550],{"class":130,"line":538},13,[128,540,357],{"class":183},[128,542,360],{"class":141},[128,544,363],{"class":145},[128,546,211],{"class":141},[128,548,549],{"class":170},"\"Ready: dependencies are reachable.\"",[128,551,371],{"class":141},[128,553,555],{"class":130,"line":554},14,[128,556,377],{"class":141},[10,558,559,562,563,566,567,570],{},[14,560,561],{},"Deployment YAML","：三種探針的典型設定，注意 ",[125,564,565],{},"failureThreshold"," 與\n",[125,568,569],{},"periodSeconds"," 的搭配——Startup 給足夠的容錯次數（範例中 30 次 × 10 秒 = 最\n多容忍 5 分鐘啟動時間），Liveness／Readiness 則用較短的檢查週期：",[118,572,576],{"className":573,"code":574,"language":575,"meta":123,"style":123},"language-yaml shiki shiki-themes github-dark","livenessProbe:\n  httpGet: { path: \u002Fprobe\u002Flive, port: 8080 }\n  initialDelaySeconds: 10\n  periodSeconds: 10\n  failureThreshold: 3\n\nreadinessProbe:\n  httpGet: { path: \u002Fprobe\u002Fready, port: 8080 }\n  initialDelaySeconds: 5\n  periodSeconds: 5\n  failureThreshold: 3\n\nstartupProbe:\n  httpGet: { path: \u002Fprobe\u002Fstartup, port: 8080 }\n  periodSeconds: 10\n  failureThreshold: 30\n","yaml",[125,577,578,587,616,626,635,645,649,656,679,688,696,704,708,715,738,747],{"__ignoreMap":123},[128,579,580,584],{"class":130,"line":131},[128,581,583],{"class":582},"s4JwU","livenessProbe",[128,585,586],{"class":141},":\n",[128,588,589,592,595,598,600,603,605,608,610,613],{"class":130,"line":138},[128,590,591],{"class":582},"  httpGet",[128,593,594],{"class":141},": { ",[128,596,597],{"class":582},"path",[128,599,180],{"class":141},[128,601,602],{"class":170},"\u002Fprobe\u002Flive",[128,604,174],{"class":141},[128,606,607],{"class":582},"port",[128,609,180],{"class":141},[128,611,612],{"class":421},"8080",[128,614,615],{"class":141}," }\n",[128,617,618,621,623],{"class":130,"line":152},[128,619,620],{"class":582},"  initialDelaySeconds",[128,622,180],{"class":141},[128,624,625],{"class":421},"10\n",[128,627,628,631,633],{"class":130,"line":195},[128,629,630],{"class":582},"  periodSeconds",[128,632,180],{"class":141},[128,634,625],{"class":421},[128,636,637,640,642],{"class":130,"line":202},[128,638,639],{"class":582},"  failureThreshold",[128,641,180],{"class":141},[128,643,644],{"class":421},"3\n",[128,646,647],{"class":130,"line":220},[128,648,199],{"emptyLinePlaceholder":198},[128,650,651,654],{"class":130,"line":339},[128,652,653],{"class":582},"readinessProbe",[128,655,586],{"class":141},[128,657,658,660,662,664,666,669,671,673,675,677],{"class":130,"line":349},[128,659,591],{"class":582},[128,661,594],{"class":141},[128,663,597],{"class":582},[128,665,180],{"class":141},[128,667,668],{"class":170},"\u002Fprobe\u002Fready",[128,670,174],{"class":141},[128,672,607],{"class":582},[128,674,180],{"class":141},[128,676,612],{"class":421},[128,678,615],{"class":141},[128,680,681,683,685],{"class":130,"line":354},[128,682,620],{"class":582},[128,684,180],{"class":141},[128,686,687],{"class":421},"5\n",[128,689,690,692,694],{"class":130,"line":374},[128,691,630],{"class":582},[128,693,180],{"class":141},[128,695,687],{"class":421},[128,697,698,700,702],{"class":130,"line":513},[128,699,639],{"class":582},[128,701,180],{"class":141},[128,703,644],{"class":421},[128,705,706],{"class":130,"line":533},[128,707,199],{"emptyLinePlaceholder":198},[128,709,710,713],{"class":130,"line":538},[128,711,712],{"class":582},"startupProbe",[128,714,586],{"class":141},[128,716,717,719,721,723,725,728,730,732,734,736],{"class":130,"line":554},[128,718,591],{"class":582},[128,720,594],{"class":141},[128,722,597],{"class":582},[128,724,180],{"class":141},[128,726,727],{"class":170},"\u002Fprobe\u002Fstartup",[128,729,174],{"class":141},[128,731,607],{"class":582},[128,733,180],{"class":141},[128,735,612],{"class":421},[128,737,615],{"class":141},[128,739,741,743,745],{"class":130,"line":740},15,[128,742,630],{"class":582},[128,744,180],{"class":141},[128,746,625],{"class":421},[128,748,750,752,754],{"class":130,"line":749},16,[128,751,639],{"class":582},[128,753,180],{"class":141},[128,755,756],{"class":421},"30\n",[27,758,760],{"id":759},"容易漏掉的一步優雅退出graceful-shutdown","容易漏掉的一步：優雅退出（Graceful Shutdown）",[10,762,763,764,767],{},"Pod 被終止時，Kubernetes 會先送出 SIGTERM，但",[14,765,766],{},"這個當下 Service 的 Endpoints\n不會立刻更新","——如果應用馬上就地關閉，仍在路上的請求會直接被中斷。正確順序\n是收到 SIGTERM 後，先把 Readiness 轉成 Unhealthy，等一段排空（drain）時間，\n再真正結束程序：",[118,769,771],{"className":120,"code":770,"language":122,"meta":123,"style":123},"app.Lifetime.ApplicationStopping.Register(() =>\n{\n    \u002F\u002F 1. 讓 Readiness 立刻回傳 Unhealthy，\n    \u002F\u002F    Pod 會從 Service Endpoints 移除，不再收到新流量\n    ReadinessHealthCheck.IsShutdownInitiated = true;\n\n    \u002F\u002F 2. 等待 Endpoints 更新擴散到所有 Node／Load Balancer，\n    \u002F\u002F    讓既有連線把手上的請求做完（drain）\n    Thread.Sleep(TimeSpan.FromSeconds(10));\n\n    \u002F\u002F 3. 排空結束才真正進入資源釋放與終止流程\n});\n",[125,772,773,787,791,796,801,813,817,822,827,849,853,858],{"__ignoreMap":123},[128,774,775,778,781,784],{"class":130,"line":131},[128,776,777],{"class":141},"app.Lifetime.ApplicationStopping.",[128,779,780],{"class":145},"Register",[128,782,783],{"class":141},"(() ",[128,785,786],{"class":183},"=>\n",[128,788,789],{"class":130,"line":138},[128,790,316],{"class":141},[128,792,793],{"class":130,"line":152},[128,794,795],{"class":134},"    \u002F\u002F 1. 讓 Readiness 立刻回傳 Unhealthy，\n",[128,797,798],{"class":130,"line":195},[128,799,800],{"class":134},"    \u002F\u002F    Pod 會從 Service Endpoints 移除，不再收到新流量\n",[128,802,803,806,808,811],{"class":130,"line":202},[128,804,805],{"class":141},"    ReadinessHealthCheck.IsShutdownInitiated ",[128,807,232],{"class":183},[128,809,810],{"class":421}," true",[128,812,425],{"class":141},[128,814,815],{"class":130,"line":220},[128,816,199],{"emptyLinePlaceholder":198},[128,818,819],{"class":130,"line":339},[128,820,821],{"class":134},"    \u002F\u002F 2. 等待 Endpoints 更新擴散到所有 Node／Load Balancer，\n",[128,823,824],{"class":130,"line":349},[128,825,826],{"class":134},"    \u002F\u002F    讓既有連線把手上的請求做完（drain）\n",[128,828,829,832,835,838,841,843,846],{"class":130,"line":354},[128,830,831],{"class":141},"    Thread.",[128,833,834],{"class":145},"Sleep",[128,836,837],{"class":141},"(TimeSpan.",[128,839,840],{"class":145},"FromSeconds",[128,842,211],{"class":141},[128,844,845],{"class":421},"10",[128,847,848],{"class":141},"));\n",[128,850,851],{"class":130,"line":374},[128,852,199],{"emptyLinePlaceholder":198},[128,854,855],{"class":130,"line":513},[128,856,857],{"class":134},"    \u002F\u002F 3. 排空結束才真正進入資源釋放與終止流程\n",[128,859,860],{"class":130,"line":533},[128,861,862],{"class":141},"});\n",[10,864,865,866,869,870,873],{},"Drain 時間要抓多長，取決於應用最長的請求處理時間與 Endpoints 更新擴散的延\n遲，10～30 秒是常見起點；如果有更長的清理作業，要另外搭配\n",[125,867,868],{},"terminationGracePeriodSeconds"," 與 ",[125,871,872],{},"preStop"," hook，避免清理做到一半就被\nKubernetes 強制 kill。",[27,875,876],{"id":876},"總結",[878,879,880,886,891,896],"ul",{},[881,882,883,885],"li",{},[14,884,94],{}," 解決「啟動慢被 Liveness 誤判」的問題",[881,887,888,890],{},[14,889,59],{}," 只保證容器能自我療癒（卡死時重啟），檢查邏輯必須極輕量",[881,892,893,895],{},[14,894,76],{}," 控制流量路由，決定 Pod 準備好了沒、要不要收新請求",[881,897,898],{},"關閉服務時，Readiness 要先轉 Unhealthy 再等 drain，而不是直接關閉程序",[900,901,902],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":123,"searchDepth":138,"depth":138,"links":904},[905,906,907,908],{"id":29,"depth":138,"text":29},{"id":110,"depth":138,"text":111},{"id":759,"depth":138,"text":760},{"id":876,"depth":138,"text":876},"2025-11-23","Kubernetes 的三種探針（Liveness、Readiness、Startup）機制本身不難，官方文件\n也寫得很清楚。真正常出問題的地方，是把不該放進某個探針的檢查放了進去——\n最常見的就是把「檢查資料庫連線」這種較重的邏輯放進 Liveness，結果資料庫一慢，\n整個服務被 Kubernetes 誤判成掛掉，開始無限重啟，把本來只是慢的問題直接搞成中\n斷。完整可執行的範例程式碼放在\ndotnet-k8s-health-checks。",null,"md",{},"\u002Farticles\u002Fdotnet-k8s-health-checks",{"title":5,"description":910},"articles\u002Fdotnet-k8s-health-checks",[918,919,920,921],"Kubernetes","ASP.NET Core","Health Check","SRE","HbgtK1pXqCNl6Xw96bT_y4d8aeYqTwUPubiXvkuZN3Y",1792959150208]