<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>模組四：服務探活與自動恢復 on Tarragon</title><link>https://tarrragon.github.io/blog/operations/04-service-health/</link><description>Recent content in 模組四：服務探活與自動恢復 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/operations/04-service-health/index.xml" rel="self" type="application/rss+xml"/><item><title>Health check endpoint 設計</title><link>https://tarrragon.github.io/blog/operations/04-service-health/health-check-endpoint/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/04-service-health/health-check-endpoint/</guid><description>&lt;p>Health check endpoint 的責任是讓外部用一個標準請求就問到服務的權威健康狀態，取代肉眼登入機器猜死活。它是這個模組所有自動恢復機制的共同輸入：重啟靠它判斷該不該重啟、負載平衡靠它決定要不要送流量、告警靠它知道什麼時候該叫人。端點回什麼、探多深，是每個服務要自己設計的決策，不是框架給的預設值。&lt;/p>
&lt;p>設計的核心問題只有一個：「這個端點回 200 的時候，到底保證了什麼」。答得越含糊，下游依賴這個訊號做的每個決策就越不可靠。&lt;/p>
&lt;h2 id="淺檢查只證明-http-handler-活著">淺檢查只證明 HTTP handler 活著&lt;/h2>
&lt;p>最簡單的 health endpoint 是收到請求就回 200。這種檢查探到的深度只到「HTTP server 還在接受連線、路由還在運作」，不保證服務真的能做事。進程活著不等於內部子系統活著——一個服務可以進程好端端跑著、&lt;code>pgrep&lt;/code> 找得到、CPU 不高，但它內部某個關鍵子系統已經 wedged，這種狀態下淺檢查照樣回 200。這正是 &lt;a href="https://tarrragon.github.io/blog/linux/debug/process-service-state-diagnosis/" data-link-title="程序、服務與狀態怎麼判" data-link-desc="要判斷一個程式活著沒、某個系統服務現在由誰提供、桌面 session 有沒有被鎖、或終端機多工器的 session 還在不在時，用對的權威來源而不是靠畫面或猜的名字">程序、服務與狀態怎麼判&lt;/a> 裡「進程活著 ≠ 在運作」那條，搬到服務自我回報的場景。&lt;/p>
&lt;p>一個資料寫入服務的 handler 執行緒還在監聽、但它背後的資料庫連線池已經全部逾時，淺檢查會回 200，負載平衡繼續把寫入請求送進來，每一筆都在下游卡死。訊號說健康、實際在掉資料——淺檢查的盲點就在這裡：它證明的比它看起來保證的少很多。&lt;/p>
&lt;h2 id="深檢查探到服務真正依賴的子系統">深檢查探到服務真正依賴的子系統&lt;/h2>
&lt;p>深檢查在回答健康之前，先確認服務賴以運作的關鍵子系統確實可用。一個寫入服務的深檢查會實際碰一下儲存層——取一個連線、確認 pool 沒枯竭就夠，不必完整寫一筆；一個查詢服務會確認索引載入完成、快取連得上。深檢查回 200 的保證比淺檢查強：不只 handler 活著，服務真正做事需要的那條路徑也通。&lt;/p>
&lt;p>深檢查的成本是它要花時間、也可能引入自己的失敗。檢查本身若把每個下游依賴都同步 ping 一遍，就製造了依賴放大：一個非關鍵的下游慢一點，health check 跟著逾時，服務被判成不健康、被摘掉流量或被重啟，但服務本身其實還能服務核心請求。所以深檢查的設計要先切開「關鍵依賴」跟「非關鍵依賴」——關鍵依賴掛了服務確實無法運作，值得檢查；非關鍵依賴（一個可以降級的推薦引擎、一個非同步的分析上報）掛了不該讓整個服務被判死。這條切分沒有通則，取決於這個服務缺了哪個子系統就真的做不了事。&lt;/p>
&lt;h2 id="帶診斷資訊的回應把活著跟有沒有在做事分開">帶診斷資訊的回應把「活著」跟「有沒有在做事」分開&lt;/h2>
&lt;p>health endpoint 回的不只有狀態碼，還可以帶回讓外部判讀健康程度的欄位。本站 collector 的 &lt;code>/health&lt;/code> 回 200 的同時附帶「最後一次成功寫入的時間」——這個時間戳把兩件事分開了：狀態碼 200 說進程活著，最後寫入時間說它有沒有在做事。進程活著但最後寫入停在十分鐘前，是「活著但卡住」的定案訊號，比單看狀態碼可靠。&lt;/p>
&lt;p>更細的健康程度來自處理管線的內部計數。collector 在 endpoint 暴露 &lt;code>channel.depth&lt;/code>（待寫入事件的積壓數）跟 &lt;code>storage.errors&lt;/code>（寫入錯誤累計）這類計數器；積壓持續變高、錯誤持續累加，代表服務還活著、但已經降級——處理速度跟不上進來的量。這是二元的「掛了沒」之外的中間態訊號。&lt;/p>
&lt;p>中間態不一定要暴露給最終呈現層。同一套 collector 的 DevOps 儀表板服務狀態卡刻意設計成純二元——綠色正常、紅色異常，不讓看板的人去解讀積壓數字。中間態的量化欄位存在於 endpoint、供機器判讀與告警規則收斂用；呈現層則收斂成二元，讓人一眼就懂。健康程度探得多細，跟最終要呈現多細，是兩個獨立的決策。&lt;/p>
&lt;h2 id="啟動期的健康是另一種狀態">啟動期的健康是另一種狀態&lt;/h2>
&lt;p>服務剛起來、還在載入索引或重建狀態時，它既不是掛了、也還不能服務——這是啟動自檢要覆蓋的一段。collector 啟動時會檢查儲存完整性、重建索引，這段期間服務進程活著，但還沒準備好接流量。淺檢查在這時就會回 200，若外部據此開始送流量，請求會打在還沒就緒的服務上。啟動期的健康狀態要跟「就緒可服務」分開回報——啟動中、就緒、存活是三種不同狀態，對應 &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">liveness、readiness 與 startup 三種 probe&lt;/a> 要分開回答的問題。&lt;/p>
&lt;h2 id="誰來戳這個端點">誰來戳這個端點&lt;/h2>
&lt;p>設計好回什麼之後，還要決定誰來讀它。服務自己不會主動回報，要有外部的探測者定期戳。最輕量的做法是一個定時器對 endpoint 發健康請求並設逾時，戳不動就讓那次檢查失敗、走既有的告警鏈——&lt;a href="https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/" data-link-title="服務掛了怎麼自動知道：從肉眼盯到主動告警" data-link-desc="不想每次都手動 systemctl 檢查服務死活、想讓機器在 service 掛掉時主動推播通知、或擔心整台機器當掉沒人知道時回來讀">服務掛了怎麼自動知道&lt;/a> 的外部探針段有單機 systemd 上的完整實作（curl &lt;code>/health&lt;/code> 設 5 秒逾時、逾時就 failed）。跑在編排平台上時，這個角色由平台的 probe 機制接手，探測的語意、失敗後的動作，是 &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">liveness 與 readiness&lt;/a> 的主題。&lt;/p>
&lt;p>外部探測比服務自我回報可靠的地方在於：服務內部卡死時，它可能連「我不健康」都報不出來——是外部戳它、發現戳不動，才抓得到這種失效。自我回報只能覆蓋服務還有能力回應的那些失敗。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>活著、準備好接流量、啟動中是三種健康、對應三種 probe → &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">Liveness 與 Readiness&lt;/a>&lt;/li>
&lt;li>端點設計好之後，怎麼在單機用 systemd 定時探測並告警 → &lt;a href="https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/" data-link-title="服務掛了怎麼自動知道：從肉眼盯到主動告警" data-link-desc="不想每次都手動 systemctl 檢查服務死活、想讓機器在 service 掛掉時主動推播通知、或擔心整台機器當掉沒人知道時回來讀">服務掛了怎麼自動知道&lt;/a>&lt;/li>
&lt;li>探到不健康之後怎麼自動重啟、放棄了才告警 → &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/systemd-watchdog-restart/" data-link-title="systemd watchdog 與自動重啟" data-link-desc="在單機 systemd 上做服務自動恢復時，釐清 watchdog 主動報活與 restart policy 被動拉起是兩套機制、以及為什麼要先重啟幾次才放棄">systemd watchdog 與自動重啟&lt;/a>&lt;/li>
&lt;li>health signal 怎麼收斂成 DevOps 儀表板的服務狀態卡 → &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/dashboard-devops/" data-link-title="DevOps Dashboard 設計" data-link-desc="Collector 和 SDK 是否健康 — 日常監控的服務狀態卡、吞吐量曲線、儲存用量，以及告警觸發後的排障視圖">Monitoring DevOps 儀表板&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Health check endpoint 的責任是讓外部用一個標準請求就問到服務的權威健康狀態，取代肉眼登入機器猜死活。它是這個模組所有自動恢復機制的共同輸入：重啟靠它判斷該不該重啟、負載平衡靠它決定要不要送流量、告警靠它知道什麼時候該叫人。端點回什麼、探多深，是每個服務要自己設計的決策，不是框架給的預設值。</p>
<p>設計的核心問題只有一個：「這個端點回 200 的時候，到底保證了什麼」。答得越含糊，下游依賴這個訊號做的每個決策就越不可靠。</p>
<h2 id="淺檢查只證明-http-handler-活著">淺檢查只證明 HTTP handler 活著</h2>
<p>最簡單的 health endpoint 是收到請求就回 200。這種檢查探到的深度只到「HTTP server 還在接受連線、路由還在運作」，不保證服務真的能做事。進程活著不等於內部子系統活著——一個服務可以進程好端端跑著、<code>pgrep</code> 找得到、CPU 不高，但它內部某個關鍵子系統已經 wedged，這種狀態下淺檢查照樣回 200。這正是 <a href="/blog/linux/debug/process-service-state-diagnosis/" data-link-title="程序、服務與狀態怎麼判" data-link-desc="要判斷一個程式活著沒、某個系統服務現在由誰提供、桌面 session 有沒有被鎖、或終端機多工器的 session 還在不在時，用對的權威來源而不是靠畫面或猜的名字">程序、服務與狀態怎麼判</a> 裡「進程活著 ≠ 在運作」那條，搬到服務自我回報的場景。</p>
<p>一個資料寫入服務的 handler 執行緒還在監聽、但它背後的資料庫連線池已經全部逾時，淺檢查會回 200，負載平衡繼續把寫入請求送進來，每一筆都在下游卡死。訊號說健康、實際在掉資料——淺檢查的盲點就在這裡：它證明的比它看起來保證的少很多。</p>
<h2 id="深檢查探到服務真正依賴的子系統">深檢查探到服務真正依賴的子系統</h2>
<p>深檢查在回答健康之前，先確認服務賴以運作的關鍵子系統確實可用。一個寫入服務的深檢查會實際碰一下儲存層——取一個連線、確認 pool 沒枯竭就夠，不必完整寫一筆；一個查詢服務會確認索引載入完成、快取連得上。深檢查回 200 的保證比淺檢查強：不只 handler 活著，服務真正做事需要的那條路徑也通。</p>
<p>深檢查的成本是它要花時間、也可能引入自己的失敗。檢查本身若把每個下游依賴都同步 ping 一遍，就製造了依賴放大：一個非關鍵的下游慢一點，health check 跟著逾時，服務被判成不健康、被摘掉流量或被重啟，但服務本身其實還能服務核心請求。所以深檢查的設計要先切開「關鍵依賴」跟「非關鍵依賴」——關鍵依賴掛了服務確實無法運作，值得檢查；非關鍵依賴（一個可以降級的推薦引擎、一個非同步的分析上報）掛了不該讓整個服務被判死。這條切分沒有通則，取決於這個服務缺了哪個子系統就真的做不了事。</p>
<h2 id="帶診斷資訊的回應把活著跟有沒有在做事分開">帶診斷資訊的回應把「活著」跟「有沒有在做事」分開</h2>
<p>health endpoint 回的不只有狀態碼，還可以帶回讓外部判讀健康程度的欄位。本站 collector 的 <code>/health</code> 回 200 的同時附帶「最後一次成功寫入的時間」——這個時間戳把兩件事分開了：狀態碼 200 說進程活著，最後寫入時間說它有沒有在做事。進程活著但最後寫入停在十分鐘前，是「活著但卡住」的定案訊號，比單看狀態碼可靠。</p>
<p>更細的健康程度來自處理管線的內部計數。collector 在 endpoint 暴露 <code>channel.depth</code>（待寫入事件的積壓數）跟 <code>storage.errors</code>（寫入錯誤累計）這類計數器；積壓持續變高、錯誤持續累加，代表服務還活著、但已經降級——處理速度跟不上進來的量。這是二元的「掛了沒」之外的中間態訊號。</p>
<p>中間態不一定要暴露給最終呈現層。同一套 collector 的 DevOps 儀表板服務狀態卡刻意設計成純二元——綠色正常、紅色異常，不讓看板的人去解讀積壓數字。中間態的量化欄位存在於 endpoint、供機器判讀與告警規則收斂用；呈現層則收斂成二元，讓人一眼就懂。健康程度探得多細，跟最終要呈現多細，是兩個獨立的決策。</p>
<h2 id="啟動期的健康是另一種狀態">啟動期的健康是另一種狀態</h2>
<p>服務剛起來、還在載入索引或重建狀態時，它既不是掛了、也還不能服務——這是啟動自檢要覆蓋的一段。collector 啟動時會檢查儲存完整性、重建索引，這段期間服務進程活著，但還沒準備好接流量。淺檢查在這時就會回 200，若外部據此開始送流量，請求會打在還沒就緒的服務上。啟動期的健康狀態要跟「就緒可服務」分開回報——啟動中、就緒、存活是三種不同狀態，對應 <a href="/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">liveness、readiness 與 startup 三種 probe</a> 要分開回答的問題。</p>
<h2 id="誰來戳這個端點">誰來戳這個端點</h2>
<p>設計好回什麼之後，還要決定誰來讀它。服務自己不會主動回報，要有外部的探測者定期戳。最輕量的做法是一個定時器對 endpoint 發健康請求並設逾時，戳不動就讓那次檢查失敗、走既有的告警鏈——<a href="/blog/linux/debug/service-failure-monitoring/" data-link-title="服務掛了怎麼自動知道：從肉眼盯到主動告警" data-link-desc="不想每次都手動 systemctl 檢查服務死活、想讓機器在 service 掛掉時主動推播通知、或擔心整台機器當掉沒人知道時回來讀">服務掛了怎麼自動知道</a> 的外部探針段有單機 systemd 上的完整實作（curl <code>/health</code> 設 5 秒逾時、逾時就 failed）。跑在編排平台上時，這個角色由平台的 probe 機制接手，探測的語意、失敗後的動作，是 <a href="/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">liveness 與 readiness</a> 的主題。</p>
<p>外部探測比服務自我回報可靠的地方在於：服務內部卡死時，它可能連「我不健康」都報不出來——是外部戳它、發現戳不動，才抓得到這種失效。自我回報只能覆蓋服務還有能力回應的那些失敗。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>活著、準備好接流量、啟動中是三種健康、對應三種 probe → <a href="/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">Liveness 與 Readiness</a></li>
<li>端點設計好之後，怎麼在單機用 systemd 定時探測並告警 → <a href="/blog/linux/debug/service-failure-monitoring/" data-link-title="服務掛了怎麼自動知道：從肉眼盯到主動告警" data-link-desc="不想每次都手動 systemctl 檢查服務死活、想讓機器在 service 掛掉時主動推播通知、或擔心整台機器當掉沒人知道時回來讀">服務掛了怎麼自動知道</a></li>
<li>探到不健康之後怎麼自動重啟、放棄了才告警 → <a href="/blog/operations/04-service-health/systemd-watchdog-restart/" data-link-title="systemd watchdog 與自動重啟" data-link-desc="在單機 systemd 上做服務自動恢復時，釐清 watchdog 主動報活與 restart policy 被動拉起是兩套機制、以及為什麼要先重啟幾次才放棄">systemd watchdog 與自動重啟</a></li>
<li>health signal 怎麼收斂成 DevOps 儀表板的服務狀態卡 → <a href="/blog/monitoring/04-collector/dashboard-devops/" data-link-title="DevOps Dashboard 設計" data-link-desc="Collector 和 SDK 是否健康 — 日常監控的服務狀態卡、吞吐量曲線、儲存用量，以及告警觸發後的排障視圖">Monitoring DevOps 儀表板</a></li>
</ul>
]]></content:encoded></item><item><title>Liveness 與 Readiness</title><link>https://tarrragon.github.io/blog/operations/04-service-health/liveness-vs-readiness/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/04-service-health/liveness-vs-readiness/</guid><description>&lt;p>Liveness 跟 readiness 探測失敗時，平台採取的動作完全不同：readiness 失敗，平台把這個實例從流量目標裡摘掉、但讓它繼續跑；liveness 失敗，平台判定這個實例壞到無法恢復、直接重建它。兩者混淆的代價是用錯恢復動作——把一個只是暫時不能接流量的服務給重啟了，或把一個真的死掉的服務一直留在流量池裡。分清這兩種健康，是設計自動恢復的前提。&lt;/p>
&lt;p>差別的根源是它們回答的問題不同。Readiness 問「現在可以安全地把流量送給它嗎」，liveness 問「它還有基本運作能力、還是壞到只能砍掉重來」。同一個服務在同一時刻，readiness 可能為否、liveness 卻為是——它活著、只是還沒準備好。&lt;/p>
&lt;h2 id="readiness宣告可以安全接流量">Readiness：宣告可以安全接流量&lt;/h2>
&lt;p>Readiness 是服務對平台的宣告：我現在可以安全承接流量了。平台只把流量導向 readiness 通過的實例；一旦某個實例 readiness 轉為否，平台停止送新流量給它，但不動它的進程。這個機制讓服務有辦法在「還活著、但暫時不該接流量」的狀態下把自己隔離起來——正在載入大型快取、下游必要依賴短暫斷線、或正在收束準備關閉。&lt;/p>
&lt;p>Readiness 設計的核心決策是「哪些依賴要納入判斷」。這條線劃錯，readiness 不是太緊就是太鬆。依賴大致分三類，各有不同的納入原則：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>必要依賴&lt;/strong>：資料庫、認證服務這種，不可用時服務完全無法處理請求。這類納入 readiness——它們斷了，服務就該停止接流量。&lt;/li>
&lt;li>&lt;strong>可降級依賴&lt;/strong>：推薦引擎、非關鍵快取這種，不可用時服務仍能服務核心請求、只是功能打折。這類不該納入 readiness，改用熔斷或 fallback 在應用層處理；把它們納入，等於讓一個可以降級的功能故障拖垮整個服務的接流量能力。&lt;/li>
&lt;li>&lt;strong>觀測依賴&lt;/strong>：metrics collector、log shipper 這種。把它們納入 readiness 是常見的誤判——觀測基建掛掉本身不影響服務處理請求的能力，卻會讓整個服務被判成不 ready、被摘光流量。監控自己的健康不該是服務能否服務的前提。&lt;/li>
&lt;/ul>
&lt;h2 id="liveness宣告基本運作能力還在">Liveness：宣告基本運作能力還在&lt;/h2>
&lt;p>Liveness 是服務宣告自己還有基本運作能力；它失敗時平台的動作是重建實例，因為判斷已經是「這個實例壞到不可恢復」。正因為後果是重啟，liveness 只該用來偵測那些「重啟真的能修好」的失敗。&lt;/p>
&lt;p>適合 liveness 偵測的是進程回不了頭的內部壞死。一個典型是死結——把 liveness 端點設在跟主要工作獨立的執行緒上，主工作 deadlock 時這個端點回應不了，平台重建實例確實能解。另一個是記憶體洩漏逼近 OOM 的前兆：服務主動回報 unhealthy、讓平台在還來得及的時候有序重建，好過等平台的 OOM kill 硬砍（OOM kill 不走 graceful shutdown、在途請求直接中斷）。還有一類是關鍵背景任務永久停止——像憑證續期、session 清理這種停了服務會慢慢壞掉的任務，值得反映進 liveness。&lt;/p>
&lt;p>不適合 liveness 偵測的是下游的暫時性問題。下游資料庫短暫不可用、外部 API 逾時、快取命中率升高——這些用 liveness 去重建實例毫無幫助，重啟這個服務不會修好它的下游，只會用重啟放大問題：實例重建期間容量更少、下游壓力反而更大。這類屬於 readiness（暫時摘流量）或熔斷（快速失敗）的範疇，不是 liveness。把下游故障接進 liveness，是製造 restart loop 最常見的方式。&lt;/p>
&lt;h2 id="startup啟動期是第三種狀態">Startup：啟動期是第三種狀態&lt;/h2>
&lt;p>服務剛起來、還在初始化的那段時間，既不是死、也還不能服務，這是第三種探針要覆蓋的狀態。Startup 探針在啟動期間持續探測，一旦成功就把健康判斷交棒給 liveness 跟 readiness。它存在的理由是啟動期的容忍窗口跟穩定運行期不同——一個冷啟動要拉 image、連依賴、重建索引的服務，可能要幾分鐘才就緒，但這不代表穩定運行時也該容忍幾分鐘不回應。Startup 探針的總容忍時間由探測失敗門檻乘上探測間隔決定（例如門檻 30 次、間隔 10 秒，就是 300 秒窗口），設計時量測最差情境的啟動時間（冷啟動加 image 拉取加依賴連線）再加兩三成 headroom。&lt;/p>
&lt;p>這帶出一個反模式：把 startup、readiness、liveness 全設成同一個 &lt;code>/health&lt;/code> 端點。三種探針問的是不同問題——初始化完成不等於依賴就緒，依賴暫時不可達不等於服務本身壞了。同一個端點無法同時給出三種語意不同的答案，混用的結果是平台在錯的時機做錯的動作。&lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/health-check-endpoint/" data-link-title="Health check endpoint 設計" data-link-desc="設計服務的健康檢查端點時，判斷什麼回應算健康、check 要探到多深、依賴要不要一起檢查時回來讀">Health check endpoint 設計&lt;/a> 講的檢查深度，在這裡對應到「哪一種探針該探到哪一層」。&lt;/p>
&lt;h2 id="探針靈敏度是雙向的取捨">探針靈敏度是雙向的取捨&lt;/h2>
&lt;p>Liveness 設太敏感，服務會陷入 restart loop——一點抖動就被判死、重建、重建期間更不穩、又被判死。設太寬鬆，壞掉的實例會長期留在線上、持續吃流量卻服務不了。Readiness 同理，只是後果換成流量抖動而非重啟迴圈。&lt;/p>
&lt;p>Liveness 觸發的重啟從來不是零成本，設計靈敏度時要把代價算進去：在途請求被中斷、連線要重建（資料庫連線池、快取、佇列消費者都要重新建立）、實例重建期間出現容量缺口。最危險的是 thundering herd——多個實例因為同一個下游問題同時被判失敗、同時重啟、同時回來搶資源，瞬間把下游壓力放大。這也是為什麼 liveness 不該接下游故障：下游一抖，所有實例一起重啟，比原本的問題更糟。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>在單機 systemd 上，liveness 對應 watchdog、重啟對應 restart policy → &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/systemd-watchdog-restart/" data-link-title="systemd watchdog 與自動重啟" data-link-desc="在單機 systemd 上做服務自動恢復時，釐清 watchdog 主動報活與 restart policy 被動拉起是兩套機制、以及為什麼要先重啟幾次才放棄">systemd watchdog 與自動重啟&lt;/a>&lt;/li>
&lt;li>探針該探到哪一層、回什麼才算健康 → &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/health-check-endpoint/" data-link-title="Health check endpoint 設計" data-link-desc="設計服務的健康檢查端點時，判斷什麼回應算健康、check 要探到多深、依賴要不要一起檢查時回來讀">Health check endpoint 設計&lt;/a>&lt;/li>
&lt;li>不同平台能不能分開表達這三種探針，是選型的關鍵 → &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/process-supervisor-selection/" data-link-title="Process supervisor 選型" data-link-desc="在 systemd、supervisord、Docker restart policy、Kubernetes 之間選服務監管方式時，用平台能不能分開表達 startup、readiness、liveness、drain 當判準">Process supervisor 選型&lt;/a>&lt;/li>
&lt;li>Readiness 轉為否、先摘流量再收束，是關閉流程的第一步 → &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/graceful-shutdown/" data-link-title="Graceful shutdown" data-link-desc="設計服務收到停止信號後的收束流程時，釐清 SIGTERM 到 SIGKILL 的 grace period、退場的固定順序、以及不同 workload 的 drain 窗口要留多長">Graceful shutdown&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Liveness 跟 readiness 探測失敗時，平台採取的動作完全不同：readiness 失敗，平台把這個實例從流量目標裡摘掉、但讓它繼續跑；liveness 失敗，平台判定這個實例壞到無法恢復、直接重建它。兩者混淆的代價是用錯恢復動作——把一個只是暫時不能接流量的服務給重啟了，或把一個真的死掉的服務一直留在流量池裡。分清這兩種健康，是設計自動恢復的前提。</p>
<p>差別的根源是它們回答的問題不同。Readiness 問「現在可以安全地把流量送給它嗎」，liveness 問「它還有基本運作能力、還是壞到只能砍掉重來」。同一個服務在同一時刻，readiness 可能為否、liveness 卻為是——它活著、只是還沒準備好。</p>
<h2 id="readiness宣告可以安全接流量">Readiness：宣告可以安全接流量</h2>
<p>Readiness 是服務對平台的宣告：我現在可以安全承接流量了。平台只把流量導向 readiness 通過的實例；一旦某個實例 readiness 轉為否，平台停止送新流量給它，但不動它的進程。這個機制讓服務有辦法在「還活著、但暫時不該接流量」的狀態下把自己隔離起來——正在載入大型快取、下游必要依賴短暫斷線、或正在收束準備關閉。</p>
<p>Readiness 設計的核心決策是「哪些依賴要納入判斷」。這條線劃錯，readiness 不是太緊就是太鬆。依賴大致分三類，各有不同的納入原則：</p>
<ul>
<li><strong>必要依賴</strong>：資料庫、認證服務這種，不可用時服務完全無法處理請求。這類納入 readiness——它們斷了，服務就該停止接流量。</li>
<li><strong>可降級依賴</strong>：推薦引擎、非關鍵快取這種，不可用時服務仍能服務核心請求、只是功能打折。這類不該納入 readiness，改用熔斷或 fallback 在應用層處理；把它們納入，等於讓一個可以降級的功能故障拖垮整個服務的接流量能力。</li>
<li><strong>觀測依賴</strong>：metrics collector、log shipper 這種。把它們納入 readiness 是常見的誤判——觀測基建掛掉本身不影響服務處理請求的能力，卻會讓整個服務被判成不 ready、被摘光流量。監控自己的健康不該是服務能否服務的前提。</li>
</ul>
<h2 id="liveness宣告基本運作能力還在">Liveness：宣告基本運作能力還在</h2>
<p>Liveness 是服務宣告自己還有基本運作能力；它失敗時平台的動作是重建實例，因為判斷已經是「這個實例壞到不可恢復」。正因為後果是重啟，liveness 只該用來偵測那些「重啟真的能修好」的失敗。</p>
<p>適合 liveness 偵測的是進程回不了頭的內部壞死。一個典型是死結——把 liveness 端點設在跟主要工作獨立的執行緒上，主工作 deadlock 時這個端點回應不了，平台重建實例確實能解。另一個是記憶體洩漏逼近 OOM 的前兆：服務主動回報 unhealthy、讓平台在還來得及的時候有序重建，好過等平台的 OOM kill 硬砍（OOM kill 不走 graceful shutdown、在途請求直接中斷）。還有一類是關鍵背景任務永久停止——像憑證續期、session 清理這種停了服務會慢慢壞掉的任務，值得反映進 liveness。</p>
<p>不適合 liveness 偵測的是下游的暫時性問題。下游資料庫短暫不可用、外部 API 逾時、快取命中率升高——這些用 liveness 去重建實例毫無幫助，重啟這個服務不會修好它的下游，只會用重啟放大問題：實例重建期間容量更少、下游壓力反而更大。這類屬於 readiness（暫時摘流量）或熔斷（快速失敗）的範疇，不是 liveness。把下游故障接進 liveness，是製造 restart loop 最常見的方式。</p>
<h2 id="startup啟動期是第三種狀態">Startup：啟動期是第三種狀態</h2>
<p>服務剛起來、還在初始化的那段時間，既不是死、也還不能服務，這是第三種探針要覆蓋的狀態。Startup 探針在啟動期間持續探測，一旦成功就把健康判斷交棒給 liveness 跟 readiness。它存在的理由是啟動期的容忍窗口跟穩定運行期不同——一個冷啟動要拉 image、連依賴、重建索引的服務，可能要幾分鐘才就緒，但這不代表穩定運行時也該容忍幾分鐘不回應。Startup 探針的總容忍時間由探測失敗門檻乘上探測間隔決定（例如門檻 30 次、間隔 10 秒，就是 300 秒窗口），設計時量測最差情境的啟動時間（冷啟動加 image 拉取加依賴連線）再加兩三成 headroom。</p>
<p>這帶出一個反模式：把 startup、readiness、liveness 全設成同一個 <code>/health</code> 端點。三種探針問的是不同問題——初始化完成不等於依賴就緒，依賴暫時不可達不等於服務本身壞了。同一個端點無法同時給出三種語意不同的答案，混用的結果是平台在錯的時機做錯的動作。<a href="/blog/operations/04-service-health/health-check-endpoint/" data-link-title="Health check endpoint 設計" data-link-desc="設計服務的健康檢查端點時，判斷什麼回應算健康、check 要探到多深、依賴要不要一起檢查時回來讀">Health check endpoint 設計</a> 講的檢查深度，在這裡對應到「哪一種探針該探到哪一層」。</p>
<h2 id="探針靈敏度是雙向的取捨">探針靈敏度是雙向的取捨</h2>
<p>Liveness 設太敏感，服務會陷入 restart loop——一點抖動就被判死、重建、重建期間更不穩、又被判死。設太寬鬆，壞掉的實例會長期留在線上、持續吃流量卻服務不了。Readiness 同理，只是後果換成流量抖動而非重啟迴圈。</p>
<p>Liveness 觸發的重啟從來不是零成本，設計靈敏度時要把代價算進去：在途請求被中斷、連線要重建（資料庫連線池、快取、佇列消費者都要重新建立）、實例重建期間出現容量缺口。最危險的是 thundering herd——多個實例因為同一個下游問題同時被判失敗、同時重啟、同時回來搶資源，瞬間把下游壓力放大。這也是為什麼 liveness 不該接下游故障：下游一抖，所有實例一起重啟，比原本的問題更糟。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>在單機 systemd 上，liveness 對應 watchdog、重啟對應 restart policy → <a href="/blog/operations/04-service-health/systemd-watchdog-restart/" data-link-title="systemd watchdog 與自動重啟" data-link-desc="在單機 systemd 上做服務自動恢復時，釐清 watchdog 主動報活與 restart policy 被動拉起是兩套機制、以及為什麼要先重啟幾次才放棄">systemd watchdog 與自動重啟</a></li>
<li>探針該探到哪一層、回什麼才算健康 → <a href="/blog/operations/04-service-health/health-check-endpoint/" data-link-title="Health check endpoint 設計" data-link-desc="設計服務的健康檢查端點時，判斷什麼回應算健康、check 要探到多深、依賴要不要一起檢查時回來讀">Health check endpoint 設計</a></li>
<li>不同平台能不能分開表達這三種探針，是選型的關鍵 → <a href="/blog/operations/04-service-health/process-supervisor-selection/" data-link-title="Process supervisor 選型" data-link-desc="在 systemd、supervisord、Docker restart policy、Kubernetes 之間選服務監管方式時，用平台能不能分開表達 startup、readiness、liveness、drain 當判準">Process supervisor 選型</a></li>
<li>Readiness 轉為否、先摘流量再收束，是關閉流程的第一步 → <a href="/blog/operations/04-service-health/graceful-shutdown/" data-link-title="Graceful shutdown" data-link-desc="設計服務收到停止信號後的收束流程時，釐清 SIGTERM 到 SIGKILL 的 grace period、退場的固定順序、以及不同 workload 的 drain 窗口要留多長">Graceful shutdown</a></li>
</ul>
]]></content:encoded></item><item><title>systemd watchdog 與自動重啟</title><link>https://tarrragon.github.io/blog/operations/04-service-health/systemd-watchdog-restart/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/04-service-health/systemd-watchdog-restart/</guid><description>&lt;p>單機 systemd 上的自動恢復由兩套獨立機制組成：watchdog 讓服務定期主動報活、超時沒報就被 systemd 重建；restart policy 在進程退出後把它重新拉起。前者對應 &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">liveness 探針&lt;/a> 的語意——服務宣告自己還在運作；後者對應進程層的崩潰恢復。兩者觸發條件不同、覆蓋的失效模式也不同，一個服務常常兩套都要。&lt;/p>
&lt;p>在編排平台上，這兩件事由 probe 機制表達；在單機部署，systemd 靠 &lt;code>sd_notify&lt;/code> 協議讓服務明確宣告狀態，反而比 probe 更直接——服務主動說「我還活著」「我準備好了」，不必外部反覆猜測。&lt;/p>
&lt;h2 id="watchdog服務主動報活超時就被重建">Watchdog：服務主動報活，超時就被重建&lt;/h2>
&lt;p>Watchdog 是主動式的 liveness：服務端定期呼叫 &lt;code>sd_notify(WATCHDOG=1)&lt;/code> 告訴 systemd 「我還在正常運作」，systemd 設定一個 &lt;code>WatchdogSec=&lt;/code> 逾時；服務在時限內沒報，systemd 判定它卡死、自動 kill 加 restart。本站 collector 用的是 &lt;code>WatchdogSec=30s&lt;/code>——服務要在 30 秒內報一次，逾時就被重啟。&lt;/p>
&lt;p>Watchdog 抓的正是 &lt;a href="https://tarrragon.github.io/blog/linux/debug/process-service-state-diagnosis/" data-link-title="程序、服務與狀態怎麼判" data-link-desc="要判斷一個程式活著沒、某個系統服務現在由誰提供、桌面 session 有沒有被鎖、或終端機多工器的 session 還在不在時，用對的權威來源而不是靠畫面或猜的名字">進程活著但子系統死掉&lt;/a> 那類失效：進程還在、&lt;code>systemctl is-active&lt;/code> 還顯示 active，但內部某個關鍵迴圈已經 hung。這種狀態下崩潰恢復幫不上忙（進程根本沒退出），但 watchdog 抓得到——因為卡死的服務報不出那一次心跳。前提是服務改得動：&lt;code>sd_notify(WATCHDOG=1)&lt;/code> 要寫進服務自己的碼，報活的位置要放在「真的有在做事才會經過」的路徑上，才不會變成一個進程活著就自動報活的假訊號。&lt;/p>
&lt;p>服務改不動的情況（閉源程式、別人的服務），watchdog 這條用不上，改從外部主動戳它——一個定時器對服務發健康請求並設逾時，戳不動就讓那次檢查失敗。這個外部探針的完整單機實作在 &lt;a href="https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/" data-link-title="服務掛了怎麼自動知道：從肉眼盯到主動告警" data-link-desc="不想每次都手動 systemctl 檢查服務死活、想讓機器在 service 掛掉時主動推播通知、或擔心整台機器當掉沒人知道時回來讀">服務掛了怎麼自動知道&lt;/a> 的外部健康探針段。Watchdog 跟外部探針的分界就是「控不控制得了那個服務」：控制得了用 watchdog（零額外依賴、服務自己報），控制不了用外部探針（從體外戳）。&lt;/p>
&lt;h2 id="restart-policy進程退出後被拉起">Restart policy：進程退出後被拉起&lt;/h2>
&lt;p>Restart policy 覆蓋的是進程真的退出的情況——崩潰、被 kill、非正常結束。&lt;code>Restart=on-failure&lt;/code> 讓 systemd 在服務以失敗狀態退出時自動重啟，&lt;code>RestartSec=5&lt;/code> 設定重啟前等幾秒。這是最基本的崩潰恢復：多數暫時性失敗（一次連線抖動、一個 race）重試一下就好，不值得驚動人。&lt;/p>
&lt;p>但無限重啟會掩蓋真正壞掉的服務——一個因為配置錯誤永遠起不來的服務，會被 restart policy 反覆拉起、反覆失敗。所以要配重試上限：&lt;code>StartLimitBurst=3&lt;/code> 加 &lt;code>StartLimitIntervalSec=60&lt;/code> 表示 60 秒內失敗 3 次才真的進 failed 狀態、停止重試。本站 collector 用的門檻是 10 分鐘內重啟 3 次以上就停止自動重啟、改發告警要求人工介入——這條門檻是「機器自動恢復」跟「人工恢復」的交界：撞上限之前交給機器重試，撞上限之後承認自動恢復無效、升級成人的問題。&lt;/p>
&lt;h2 id="先自動重啟放棄了才告警">先自動重啟，放棄了才告警&lt;/h2>
&lt;p>自動恢復跟告警是兩段，要分開。理由是多數失敗自己重試就會過，每次瞬斷都吵人會把告警洗到沒人看。正確的分段是：restart policy 負責「重試幾次」，告警只在「重試上限撞到、真的放棄」時才發。&lt;/p>
&lt;p>這裡有個實測踩到、跟直覺相反的意外：systemd 的 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/systemd-onfailure/" data-link-title="systemd OnFailure（失敗觸發鉤子）" data-link-desc="想讓某個服務進 failed 時自動觸發告警或修復動作、或發現 OnFailure 每次重啟中途都觸發把告警洗爆時讀">&lt;code>OnFailure&lt;/code>&lt;/a> 鉤子不是「放棄才觸發」，而是每一次失敗都觸發——包含 &lt;code>Restart=on-failure&lt;/code> 的每次 auto-restart 中途。&lt;a href="https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/" data-link-title="服務掛了怎麼自動知道：從肉眼盯到主動告警" data-link-desc="不想每次都手動 systemctl 檢查服務死活、想讓機器在 service 掛掉時主動推播通知、或擔心整台機器當掉沒人知道時回來讀">服務掛了怎麼自動知道&lt;/a> 實測一個反覆崩潰、重試 3 次後放棄的服務，&lt;code>OnFailure&lt;/code> 觸發了 4 次（3 次 auto-restart 加 1 次最終放棄）。這個觸發次數是特定 systemd 版本的實測，&lt;code>OnFailure&lt;/code> 與 &lt;code>Restart=&lt;/code> 的互動跨版本調整過，換一個版本可能量到不同次數，但「每次失敗都觸發、不是只在放棄時」這個機制本身跨版本成立。所以只靠 restart 加 start-limit 的 config，每次瞬斷都會發告警。真正做到「只在放棄才吵」，要在告警處理器裡加一道狀態閘門——auto-restart 中途服務的 &lt;code>ActiveState&lt;/code> 是 &lt;code>activating&lt;/code>、撞上限進 failed 才是 &lt;code>failed&lt;/code>，處理器只在 &lt;code>failed&lt;/code> 才送，加上這道閘門後同一個崩潰測試從 4 則告警降到 1 則。config 管重試次數、handler 的閘門管只在終局告警，兩段合起來才是完整的「先重啟、放棄才吵」。這道告警鏈的完整設定（&lt;code>OnFailure&lt;/code> 鉤子、送出腳本、遞迴陷阱）在那篇有實測驗證過的完整版，本模組不重述。&lt;/p>
&lt;h2 id="恢復閉環要回饋到狀態呈現">恢復閉環要回饋到狀態呈現&lt;/h2>
&lt;p>自動恢復做完之後，狀態呈現要跟著收斂。collector 在自動恢復成功後送一個 &lt;code>collector.started&lt;/code> 事件，讓 DevOps 儀表板的服務狀態卡從紅轉綠；撞到重啟上限、升級成人工告警時，狀態卡維持紅色等人處理。恢復機制跟呈現層的這個回饋迴路，讓「服務掉了又自己回來」這件事在看板上留下可追溯的痕跡，而不是靜默地掉了又靜默地回來。收斂成儀表板的細節見 &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/dashboard-devops/" data-link-title="DevOps Dashboard 設計" data-link-desc="Collector 和 SDK 是否健康 — 日常監控的服務狀態卡、吞吐量曲線、儲存用量，以及告警觸發後的排障視圖">Monitoring DevOps 儀表板&lt;/a>。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Watchdog 對應的概念層 liveness、restart 對應的重啟代價 → &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">Liveness 與 Readiness&lt;/a>&lt;/li>
&lt;li>單機 systemd 之外，supervisord、Docker、Kubernetes 怎麼表達重啟與重建 → &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/process-supervisor-selection/" data-link-title="Process supervisor 選型" data-link-desc="在 systemd、supervisord、Docker restart policy、Kubernetes 之間選服務監管方式時，用平台能不能分開表達 startup、readiness、liveness、drain 當判準">Process supervisor 選型&lt;/a>&lt;/li>
&lt;li>告警鏈的完整單機實作（OnFailure、送出腳本、心跳、canary）→ &lt;a href="https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/" data-link-title="服務掛了怎麼自動知道：從肉眼盯到主動告警" data-link-desc="不想每次都手動 systemctl 檢查服務死活、想讓機器在 service 掛掉時主動推播通知、或擔心整台機器當掉沒人知道時回來讀">服務掛了怎麼自動知道&lt;/a>&lt;/li>
&lt;li>重啟前先讓服務有序收束在途工作 → &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/graceful-shutdown/" data-link-title="Graceful shutdown" data-link-desc="設計服務收到停止信號後的收束流程時，釐清 SIGTERM 到 SIGKILL 的 grace period、退場的固定順序、以及不同 workload 的 drain 窗口要留多長">Graceful shutdown&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>單機 systemd 上的自動恢復由兩套獨立機制組成：watchdog 讓服務定期主動報活、超時沒報就被 systemd 重建；restart policy 在進程退出後把它重新拉起。前者對應 <a href="/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">liveness 探針</a> 的語意——服務宣告自己還在運作；後者對應進程層的崩潰恢復。兩者觸發條件不同、覆蓋的失效模式也不同，一個服務常常兩套都要。</p>
<p>在編排平台上，這兩件事由 probe 機制表達；在單機部署，systemd 靠 <code>sd_notify</code> 協議讓服務明確宣告狀態，反而比 probe 更直接——服務主動說「我還活著」「我準備好了」，不必外部反覆猜測。</p>
<h2 id="watchdog服務主動報活超時就被重建">Watchdog：服務主動報活，超時就被重建</h2>
<p>Watchdog 是主動式的 liveness：服務端定期呼叫 <code>sd_notify(WATCHDOG=1)</code> 告訴 systemd 「我還在正常運作」，systemd 設定一個 <code>WatchdogSec=</code> 逾時；服務在時限內沒報，systemd 判定它卡死、自動 kill 加 restart。本站 collector 用的是 <code>WatchdogSec=30s</code>——服務要在 30 秒內報一次，逾時就被重啟。</p>
<p>Watchdog 抓的正是 <a href="/blog/linux/debug/process-service-state-diagnosis/" data-link-title="程序、服務與狀態怎麼判" data-link-desc="要判斷一個程式活著沒、某個系統服務現在由誰提供、桌面 session 有沒有被鎖、或終端機多工器的 session 還在不在時，用對的權威來源而不是靠畫面或猜的名字">進程活著但子系統死掉</a> 那類失效：進程還在、<code>systemctl is-active</code> 還顯示 active，但內部某個關鍵迴圈已經 hung。這種狀態下崩潰恢復幫不上忙（進程根本沒退出），但 watchdog 抓得到——因為卡死的服務報不出那一次心跳。前提是服務改得動：<code>sd_notify(WATCHDOG=1)</code> 要寫進服務自己的碼，報活的位置要放在「真的有在做事才會經過」的路徑上，才不會變成一個進程活著就自動報活的假訊號。</p>
<p>服務改不動的情況（閉源程式、別人的服務），watchdog 這條用不上，改從外部主動戳它——一個定時器對服務發健康請求並設逾時，戳不動就讓那次檢查失敗。這個外部探針的完整單機實作在 <a href="/blog/linux/debug/service-failure-monitoring/" data-link-title="服務掛了怎麼自動知道：從肉眼盯到主動告警" data-link-desc="不想每次都手動 systemctl 檢查服務死活、想讓機器在 service 掛掉時主動推播通知、或擔心整台機器當掉沒人知道時回來讀">服務掛了怎麼自動知道</a> 的外部健康探針段。Watchdog 跟外部探針的分界就是「控不控制得了那個服務」：控制得了用 watchdog（零額外依賴、服務自己報），控制不了用外部探針（從體外戳）。</p>
<h2 id="restart-policy進程退出後被拉起">Restart policy：進程退出後被拉起</h2>
<p>Restart policy 覆蓋的是進程真的退出的情況——崩潰、被 kill、非正常結束。<code>Restart=on-failure</code> 讓 systemd 在服務以失敗狀態退出時自動重啟，<code>RestartSec=5</code> 設定重啟前等幾秒。這是最基本的崩潰恢復：多數暫時性失敗（一次連線抖動、一個 race）重試一下就好，不值得驚動人。</p>
<p>但無限重啟會掩蓋真正壞掉的服務——一個因為配置錯誤永遠起不來的服務，會被 restart policy 反覆拉起、反覆失敗。所以要配重試上限：<code>StartLimitBurst=3</code> 加 <code>StartLimitIntervalSec=60</code> 表示 60 秒內失敗 3 次才真的進 failed 狀態、停止重試。本站 collector 用的門檻是 10 分鐘內重啟 3 次以上就停止自動重啟、改發告警要求人工介入——這條門檻是「機器自動恢復」跟「人工恢復」的交界：撞上限之前交給機器重試，撞上限之後承認自動恢復無效、升級成人的問題。</p>
<h2 id="先自動重啟放棄了才告警">先自動重啟，放棄了才告警</h2>
<p>自動恢復跟告警是兩段，要分開。理由是多數失敗自己重試就會過，每次瞬斷都吵人會把告警洗到沒人看。正確的分段是：restart policy 負責「重試幾次」，告警只在「重試上限撞到、真的放棄」時才發。</p>
<p>這裡有個實測踩到、跟直覺相反的意外：systemd 的 <a href="/blog/linux/dotfile/knowledge-cards/systemd-onfailure/" data-link-title="systemd OnFailure（失敗觸發鉤子）" data-link-desc="想讓某個服務進 failed 時自動觸發告警或修復動作、或發現 OnFailure 每次重啟中途都觸發把告警洗爆時讀"><code>OnFailure</code></a> 鉤子不是「放棄才觸發」，而是每一次失敗都觸發——包含 <code>Restart=on-failure</code> 的每次 auto-restart 中途。<a href="/blog/linux/debug/service-failure-monitoring/" data-link-title="服務掛了怎麼自動知道：從肉眼盯到主動告警" data-link-desc="不想每次都手動 systemctl 檢查服務死活、想讓機器在 service 掛掉時主動推播通知、或擔心整台機器當掉沒人知道時回來讀">服務掛了怎麼自動知道</a> 實測一個反覆崩潰、重試 3 次後放棄的服務，<code>OnFailure</code> 觸發了 4 次（3 次 auto-restart 加 1 次最終放棄）。這個觸發次數是特定 systemd 版本的實測，<code>OnFailure</code> 與 <code>Restart=</code> 的互動跨版本調整過，換一個版本可能量到不同次數，但「每次失敗都觸發、不是只在放棄時」這個機制本身跨版本成立。所以只靠 restart 加 start-limit 的 config，每次瞬斷都會發告警。真正做到「只在放棄才吵」，要在告警處理器裡加一道狀態閘門——auto-restart 中途服務的 <code>ActiveState</code> 是 <code>activating</code>、撞上限進 failed 才是 <code>failed</code>，處理器只在 <code>failed</code> 才送，加上這道閘門後同一個崩潰測試從 4 則告警降到 1 則。config 管重試次數、handler 的閘門管只在終局告警，兩段合起來才是完整的「先重啟、放棄才吵」。這道告警鏈的完整設定（<code>OnFailure</code> 鉤子、送出腳本、遞迴陷阱）在那篇有實測驗證過的完整版，本模組不重述。</p>
<h2 id="恢復閉環要回饋到狀態呈現">恢復閉環要回饋到狀態呈現</h2>
<p>自動恢復做完之後，狀態呈現要跟著收斂。collector 在自動恢復成功後送一個 <code>collector.started</code> 事件，讓 DevOps 儀表板的服務狀態卡從紅轉綠；撞到重啟上限、升級成人工告警時，狀態卡維持紅色等人處理。恢復機制跟呈現層的這個回饋迴路，讓「服務掉了又自己回來」這件事在看板上留下可追溯的痕跡，而不是靜默地掉了又靜默地回來。收斂成儀表板的細節見 <a href="/blog/monitoring/04-collector/dashboard-devops/" data-link-title="DevOps Dashboard 設計" data-link-desc="Collector 和 SDK 是否健康 — 日常監控的服務狀態卡、吞吐量曲線、儲存用量，以及告警觸發後的排障視圖">Monitoring DevOps 儀表板</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Watchdog 對應的概念層 liveness、restart 對應的重啟代價 → <a href="/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">Liveness 與 Readiness</a></li>
<li>單機 systemd 之外，supervisord、Docker、Kubernetes 怎麼表達重啟與重建 → <a href="/blog/operations/04-service-health/process-supervisor-selection/" data-link-title="Process supervisor 選型" data-link-desc="在 systemd、supervisord、Docker restart policy、Kubernetes 之間選服務監管方式時，用平台能不能分開表達 startup、readiness、liveness、drain 當判準">Process supervisor 選型</a></li>
<li>告警鏈的完整單機實作（OnFailure、送出腳本、心跳、canary）→ <a href="/blog/linux/debug/service-failure-monitoring/" data-link-title="服務掛了怎麼自動知道：從肉眼盯到主動告警" data-link-desc="不想每次都手動 systemctl 檢查服務死活、想讓機器在 service 掛掉時主動推播通知、或擔心整台機器當掉沒人知道時回來讀">服務掛了怎麼自動知道</a></li>
<li>重啟前先讓服務有序收束在途工作 → <a href="/blog/operations/04-service-health/graceful-shutdown/" data-link-title="Graceful shutdown" data-link-desc="設計服務收到停止信號後的收束流程時，釐清 SIGTERM 到 SIGKILL 的 grace period、退場的固定順序、以及不同 workload 的 drain 窗口要留多長">Graceful shutdown</a></li>
</ul>
]]></content:encoded></item><item><title>Process supervisor 選型</title><link>https://tarrragon.github.io/blog/operations/04-service-health/process-supervisor-selection/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/04-service-health/process-supervisor-selection/</guid><description>&lt;p>選 process supervisor 的判準是這個平台能不能分別表達服務生命週期的四個階段：啟動（startup）、就緒（readiness）、存活（liveness）、收束（drain）。表達力越完整，越能讓平台在對的時機做對的動作；表達力有缺，缺的那部分邏輯就要在應用層自己補，複雜度從平台設定轉移到程式碼裡。選型不是比誰功能多，是比這個服務需要的生命週期粒度，跟平台能表達的粒度對不對得上。&lt;/p>
&lt;p>在動手比較之前，先問服務四個問題：啟動要多久、哪些依賴是就緒條件；失敗時該自己恢復還是交平台重建；停止時有哪些在途請求、連線、背景工作要收束；以及平台能不能把 startup、readiness、liveness、drain 分開表達。這四個問題的答案決定了要往哪個方向選。&lt;/p>
&lt;h2 id="各平台的生命週期表達力">各平台的生命週期表達力&lt;/h2>
&lt;p>各平台對這四階段的支援程度不同，這張對照是選型的骨架：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>平台&lt;/th>
 &lt;th>啟動 gate&lt;/th>
 &lt;th>就緒與存活&lt;/th>
 &lt;th>收束&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>systemd&lt;/td>
 &lt;td>無原生 startup gate&lt;/td>
 &lt;td>&lt;code>sd_notify(READY=1)&lt;/code> 宣告就緒&lt;/td>
 &lt;td>&lt;code>ExecStop&lt;/code> + &lt;code>KillSignal&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>supervisord&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>只有 RUNNING 狀態、不分離就緒與存活&lt;/td>
 &lt;td>&lt;code>stopsignal&lt;/code> + &lt;code>stopwaitsecs&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Docker&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>&lt;code>HEALTHCHECK&lt;/code> 不分離就緒與存活&lt;/td>
 &lt;td>&lt;code>stop_grace_period&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Kubernetes&lt;/td>
 &lt;td>&lt;code>startupProbe&lt;/code>&lt;/td>
 &lt;td>readiness 與 liveness 獨立探針&lt;/td>
 &lt;td>&lt;code>preStop&lt;/code> hook + endpoint 摘除&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ECS&lt;/td>
 &lt;td>startup health check&lt;/td>
 &lt;td>依 health check 設定&lt;/td>
 &lt;td>deregistration delay&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Kubernetes 的表達力最完整——三種探針獨立、收束有 preStop hook 加 endpoint 摘除，能精確表達每個階段。代價是參數最多、也最容易配錯：探針門檻、間隔、grace period 任何一個設歪，行為就跟預期不符。systemd 在單機場景反而直接，&lt;code>sd_notify&lt;/code> 讓服務主動宣告狀態，不必外部反覆探測，但它沒有原生的 startup gate 概念，啟動期的健康要自己用就緒宣告的時機表達。&lt;/p>
&lt;p>supervisord 是單機上的經典應用監管者，比 systemd 更輕、跨發行版一致，適合不想綁 systemd 的環境；限制是它只有一個 RUNNING 狀態，不像 systemd 有 &lt;code>sd_notify&lt;/code> 可以宣告就緒，也就沒辦法區分就緒與存活。這個「不分離就緒與存活」的限制在 Docker 跟 ECS 上一樣存在——&lt;code>HEALTHCHECK&lt;/code> 只有一個健康概念，無法同時回答「可以接流量嗎」跟「還活著嗎」。服務若真的需要把這兩者分開（例如依賴斷線時要摘流量但不要重啟），這段差距就得在應用層補：自己維護就緒狀態、自己在健康端點裡分辨這次探測該回答哪個問題。這一段邏輯搬進程式碼是可行的，代價是本來平台該表達的職責變成應用自己扛。&lt;/p>
&lt;h2 id="restart-policy-是恢復動作的表達">Restart policy 是恢復動作的表達&lt;/h2>
&lt;p>除了生命週期階段，各平台對「進程退出後怎麼辦」也有各自的表達。Docker 的 restart policy 有 &lt;code>no&lt;/code>（不重啟）、&lt;code>on-failure&lt;/code>（非零退出才重啟，可設次數上限）、&lt;code>always&lt;/code>（永遠重啟，含手動停止後 daemon 重啟也拉起）、&lt;code>unless-stopped&lt;/code>（類似 always 但尊重手動停止）。Kubernetes 的 Pod &lt;code>restartPolicy&lt;/code> 有 &lt;code>Always&lt;/code>、&lt;code>OnFailure&lt;/code>、&lt;code>Never&lt;/code>，語意對應到 Pod 層的容器重啟。&lt;/p>
&lt;p>這些選項對應的決策跟 systemd 的 &lt;code>Restart=on-failure&lt;/code> 是同一件事：這個服務退出時，是該無條件拉回、只在異常時拉回、還是不動它交給更上層處理。選 &lt;code>always&lt;/code> 類的策略要搭配重試上限或退避，否則一個永遠起不來的服務會陷入無限重啟迴圈——這條跟 systemd 的 &lt;code>StartLimitBurst&lt;/code> 是同一個問題，&lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/systemd-watchdog-restart/" data-link-title="systemd watchdog 與自動重啟" data-link-desc="在單機 systemd 上做服務自動恢復時，釐清 watchdog 主動報活與 restart policy 被動拉起是兩套機制、以及為什麼要先重啟幾次才放棄">systemd watchdog 與自動重啟&lt;/a> 有單機上的完整設定。&lt;/p>
&lt;h2 id="容器裡的-pid-1-是另一層選型">容器裡的 PID 1 是另一層選型&lt;/h2>
&lt;p>跑在容器裡時，還有一個容易漏掉的選型：誰當 PID 1。容器的 PID 1 是 init process，除了跑服務，還負責接收 &lt;code>SIGTERM&lt;/code>／&lt;code>SIGINT&lt;/code> 並轉發給子進程、以及回收結束的子進程（zombie reaping）。這個責任交給誰，直接影響服務收不收得到關閉信號、以及會不會累積殭屍進程。&lt;/p>
&lt;p>解法看容器裡跑幾個進程，兩種修法對應兩種情況、不是互斥的競爭方案。單一主進程的情況，用 exec form（或啟動腳本裡 &lt;code>exec&lt;/code>）讓服務直接取代 shell 當 PID 1、自己接手信號就夠。多進程容器還多一個問題：若 PID 1 不做 &lt;code>wait()&lt;/code>，結束的子進程會變殭屍累積，這時要用 tini 或 dumb-init 這類輕量 init 當 PID 1，由它負責信號轉發跟殭屍回收，或在 Kubernetes 設 &lt;code>shareProcessNamespace&lt;/code> 讓 kubelet 接手。一句話分工：exec form 解單進程的信號傳遞，tini／dumb-init 解多進程的信號傳遞加殭屍回收。殭屍為什麼會因為 PID 1 不做 &lt;code>wait()&lt;/code> 而累積、以及累積到什麼程度會讓整個 uid 開不了新進程，機制見 &lt;a href="https://tarrragon.github.io/blog/macos/macos_process_limit_zombie_reaping/" data-link-title="殭屍程序與使用者程序上限：程序表格位的配置與回收" data-link-desc="fork 失敗報 Resource temporarily unavailable、或想確認程序結束後格位何時才釋放時回來看。殭屍佔的是程序表格位、不是記憶體，父程序收屍前它一直計入 per-user 額度。">殭屍程序與使用者程序上限&lt;/a>（該篇量的是 macOS 的參數，exit 與 wait 兩階段的機制本身是 POSIX 通用的）。信號傳不到服務造成的關閉失敗，是 &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/graceful-shutdown/" data-link-title="Graceful shutdown" data-link-desc="設計服務收到停止信號後的收束流程時，釐清 SIGTERM 到 SIGKILL 的 grace period、退場的固定順序、以及不同 workload 的 drain 窗口要留多長">graceful shutdown&lt;/a> 章最常見的失效模式，這裡是它的選型根因。&lt;/p></description><content:encoded><![CDATA[<p>選 process supervisor 的判準是這個平台能不能分別表達服務生命週期的四個階段：啟動（startup）、就緒（readiness）、存活（liveness）、收束（drain）。表達力越完整，越能讓平台在對的時機做對的動作；表達力有缺，缺的那部分邏輯就要在應用層自己補，複雜度從平台設定轉移到程式碼裡。選型不是比誰功能多，是比這個服務需要的生命週期粒度，跟平台能表達的粒度對不對得上。</p>
<p>在動手比較之前，先問服務四個問題：啟動要多久、哪些依賴是就緒條件；失敗時該自己恢復還是交平台重建；停止時有哪些在途請求、連線、背景工作要收束；以及平台能不能把 startup、readiness、liveness、drain 分開表達。這四個問題的答案決定了要往哪個方向選。</p>
<h2 id="各平台的生命週期表達力">各平台的生命週期表達力</h2>
<p>各平台對這四階段的支援程度不同，這張對照是選型的骨架：</p>
<table>
  <thead>
      <tr>
          <th>平台</th>
          <th>啟動 gate</th>
          <th>就緒與存活</th>
          <th>收束</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>systemd</td>
          <td>無原生 startup gate</td>
          <td><code>sd_notify(READY=1)</code> 宣告就緒</td>
          <td><code>ExecStop</code> + <code>KillSignal</code></td>
      </tr>
      <tr>
          <td>supervisord</td>
          <td>無</td>
          <td>只有 RUNNING 狀態、不分離就緒與存活</td>
          <td><code>stopsignal</code> + <code>stopwaitsecs</code></td>
      </tr>
      <tr>
          <td>Docker</td>
          <td>無</td>
          <td><code>HEALTHCHECK</code> 不分離就緒與存活</td>
          <td><code>stop_grace_period</code></td>
      </tr>
      <tr>
          <td>Kubernetes</td>
          <td><code>startupProbe</code></td>
          <td>readiness 與 liveness 獨立探針</td>
          <td><code>preStop</code> hook + endpoint 摘除</td>
      </tr>
      <tr>
          <td>ECS</td>
          <td>startup health check</td>
          <td>依 health check 設定</td>
          <td>deregistration delay</td>
      </tr>
  </tbody>
</table>
<p>Kubernetes 的表達力最完整——三種探針獨立、收束有 preStop hook 加 endpoint 摘除，能精確表達每個階段。代價是參數最多、也最容易配錯：探針門檻、間隔、grace period 任何一個設歪，行為就跟預期不符。systemd 在單機場景反而直接，<code>sd_notify</code> 讓服務主動宣告狀態，不必外部反覆探測，但它沒有原生的 startup gate 概念，啟動期的健康要自己用就緒宣告的時機表達。</p>
<p>supervisord 是單機上的經典應用監管者，比 systemd 更輕、跨發行版一致，適合不想綁 systemd 的環境；限制是它只有一個 RUNNING 狀態，不像 systemd 有 <code>sd_notify</code> 可以宣告就緒，也就沒辦法區分就緒與存活。這個「不分離就緒與存活」的限制在 Docker 跟 ECS 上一樣存在——<code>HEALTHCHECK</code> 只有一個健康概念，無法同時回答「可以接流量嗎」跟「還活著嗎」。服務若真的需要把這兩者分開（例如依賴斷線時要摘流量但不要重啟），這段差距就得在應用層補：自己維護就緒狀態、自己在健康端點裡分辨這次探測該回答哪個問題。這一段邏輯搬進程式碼是可行的，代價是本來平台該表達的職責變成應用自己扛。</p>
<h2 id="restart-policy-是恢復動作的表達">Restart policy 是恢復動作的表達</h2>
<p>除了生命週期階段，各平台對「進程退出後怎麼辦」也有各自的表達。Docker 的 restart policy 有 <code>no</code>（不重啟）、<code>on-failure</code>（非零退出才重啟，可設次數上限）、<code>always</code>（永遠重啟，含手動停止後 daemon 重啟也拉起）、<code>unless-stopped</code>（類似 always 但尊重手動停止）。Kubernetes 的 Pod <code>restartPolicy</code> 有 <code>Always</code>、<code>OnFailure</code>、<code>Never</code>，語意對應到 Pod 層的容器重啟。</p>
<p>這些選項對應的決策跟 systemd 的 <code>Restart=on-failure</code> 是同一件事：這個服務退出時，是該無條件拉回、只在異常時拉回、還是不動它交給更上層處理。選 <code>always</code> 類的策略要搭配重試上限或退避，否則一個永遠起不來的服務會陷入無限重啟迴圈——這條跟 systemd 的 <code>StartLimitBurst</code> 是同一個問題，<a href="/blog/operations/04-service-health/systemd-watchdog-restart/" data-link-title="systemd watchdog 與自動重啟" data-link-desc="在單機 systemd 上做服務自動恢復時，釐清 watchdog 主動報活與 restart policy 被動拉起是兩套機制、以及為什麼要先重啟幾次才放棄">systemd watchdog 與自動重啟</a> 有單機上的完整設定。</p>
<h2 id="容器裡的-pid-1-是另一層選型">容器裡的 PID 1 是另一層選型</h2>
<p>跑在容器裡時，還有一個容易漏掉的選型：誰當 PID 1。容器的 PID 1 是 init process，除了跑服務，還負責接收 <code>SIGTERM</code>／<code>SIGINT</code> 並轉發給子進程、以及回收結束的子進程（zombie reaping）。這個責任交給誰，直接影響服務收不收得到關閉信號、以及會不會累積殭屍進程。</p>
<p>解法看容器裡跑幾個進程，兩種修法對應兩種情況、不是互斥的競爭方案。單一主進程的情況，用 exec form（或啟動腳本裡 <code>exec</code>）讓服務直接取代 shell 當 PID 1、自己接手信號就夠。多進程容器還多一個問題：若 PID 1 不做 <code>wait()</code>，結束的子進程會變殭屍累積，這時要用 tini 或 dumb-init 這類輕量 init 當 PID 1，由它負責信號轉發跟殭屍回收，或在 Kubernetes 設 <code>shareProcessNamespace</code> 讓 kubelet 接手。一句話分工：exec form 解單進程的信號傳遞，tini／dumb-init 解多進程的信號傳遞加殭屍回收。殭屍為什麼會因為 PID 1 不做 <code>wait()</code> 而累積、以及累積到什麼程度會讓整個 uid 開不了新進程，機制見 <a href="/blog/macos/macos_process_limit_zombie_reaping/" data-link-title="殭屍程序與使用者程序上限：程序表格位的配置與回收" data-link-desc="fork 失敗報 Resource temporarily unavailable、或想確認程序結束後格位何時才釋放時回來看。殭屍佔的是程序表格位、不是記憶體，父程序收屍前它一直計入 per-user 額度。">殭屍程序與使用者程序上限</a>（該篇量的是 macOS 的參數，exit 與 wait 兩階段的機制本身是 POSIX 通用的）。信號傳不到服務造成的關閉失敗，是 <a href="/blog/operations/04-service-health/graceful-shutdown/" data-link-title="Graceful shutdown" data-link-desc="設計服務收到停止信號後的收束流程時，釐清 SIGTERM 到 SIGKILL 的 grace period、退場的固定順序、以及不同 workload 的 drain 窗口要留多長">graceful shutdown</a> 章最常見的失效模式，這裡是它的選型根因。</p>
<h2 id="選型收斂">選型收斂</h2>
<p>單機、服務自己寫得動、要零額外依賴且需要區分就緒與存活 → systemd，用 <code>sd_notify</code> 宣告就緒與報活。單機但不想綁 systemd、只要基本的拉起與重啟 → supervisord。多機、需要 startup、readiness、liveness、drain 全部分開表達、能吃下配置複雜度 → Kubernetes。容器化但生命週期需求簡單、不需要分離就緒與存活 → Docker restart policy 加 <code>HEALTHCHECK</code>，不足的部分在應用層補。判準始終是同一條：服務需要的生命週期粒度，跟平台能表達的粒度對不對得上——需求簡單卻上最複雜的平台，付的是配置成本；需求複雜卻用表達力不足的平台，付的是應用層補洞的成本。</p>
<h2 id="要不要上-kubernetes">要不要上 Kubernetes</h2>
<p>「要不要引入編排層」是這個選型裡最大的一個決策，值得單獨判。上 Kubernetes 的成本是配置複雜度與一整套維運（叢集升級、網路、儲存、權限），這筆成本是固定的、不隨服務數量攤薄到很小。值得付的訊號是三個同時成立：跑在多台機器上、需要 startup/readiness/liveness/drain 全部分開精確表達、而且有多個服務要統一調度與擴縮。這三個都成立時，Kubernetes 把「本來要自己拼的調度、健康、擴縮」收進一個平台，複雜度換到了值得的地方。</p>
<p>反過來，單機或少數幾台、生命週期需求簡單、服務數量不多時，上 Kubernetes 是拿一大筆配置與維運複雜度、換一套用不到的能力——systemd 或 Docker 加 restart policy 就足夠，省下的複雜度是實打實的。常見的誤區是把 Kubernetes 當成「正規」的預設起點，結果一個兩台機器的服務背上了整個叢集的維運負擔。判準回到同一條：需求的粒度配不配得上平台的粒度，不是「業界都用所以我也要用」。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平台要表達的 startup、readiness、liveness 各是什麼語意 → <a href="/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">Liveness 與 Readiness</a></li>
<li>systemd 上 restart policy 與 watchdog 的完整設定 → <a href="/blog/operations/04-service-health/systemd-watchdog-restart/" data-link-title="systemd watchdog 與自動重啟" data-link-desc="在單機 systemd 上做服務自動恢復時，釐清 watchdog 主動報活與 restart policy 被動拉起是兩套機制、以及為什麼要先重啟幾次才放棄">systemd watchdog 與自動重啟</a></li>
<li>收束階段的信號傳遞與 grace period 設計 → <a href="/blog/operations/04-service-health/graceful-shutdown/" data-link-title="Graceful shutdown" data-link-desc="設計服務收到停止信號後的收束流程時，釐清 SIGTERM 到 SIGKILL 的 grace period、退場的固定順序、以及不同 workload 的 drain 窗口要留多長">Graceful shutdown</a></li>
<li>部署平台的完整生命週期契約 → <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Backend 部署平台生命週期契約</a></li>
</ul>
]]></content:encoded></item><item><title>Graceful shutdown</title><link>https://tarrragon.github.io/blog/operations/04-service-health/graceful-shutdown/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/04-service-health/graceful-shutdown/</guid><description>&lt;p>服務收到停止信號時，graceful shutdown 決定它是有序收束、還是被硬砍中斷。有序收束的責任分兩層：shutdown 是服務停止接受新工作、釋放自己持有的資源；drain 是平台在真正移除這個實例之前，讓已經在處理的請求、連線、背景工作有時間收完。這兩層都做對，一次正常的部署替換或縮容才不會掉掉在途的工作；做錯，使用者會在每次部署時撞到中斷的請求。&lt;/p>
&lt;p>收束的相對面是硬砍。平台給的收束時間是有上限的，超過上限服務就被 &lt;code>SIGKILL&lt;/code> 強制結束、不走任何清理。所以 graceful shutdown 的成敗判準是清理邏輯能不能在 grace period 內跑完——跑不完，清理邏輯寫得再完整也等於沒有。&lt;/p>
&lt;h2 id="信號路徑與-grace-period">信號路徑與 grace period&lt;/h2>
&lt;p>關閉從一個信號開始，設計的第一件事是確認這個信號真的到得了服務、以及服務有足夠時間反應。在 Kubernetes 上，平台先執行 preStop hook、再送 &lt;code>SIGTERM&lt;/code>；&lt;code>terminationGracePeriodSeconds&lt;/code> 是平台願意等的最長時間，超過就 &lt;code>SIGKILL&lt;/code>。這個值要覆蓋 preStop、drain、資源釋放的總時間——設太短，收束到一半被硬砍。&lt;/p>
&lt;p>驗證信號到不到得了服務，靠實際觸發一次關閉看紀錄：在 staging 觸發實例刪除，看 log 有沒有出現關閉處理器的紀錄。沒看到，代表信號根本沒傳到服務，要先修傳遞路徑、再談清理邏輯——清理邏輯寫得再完整，信號收不到就一行都不會跑。&lt;/p>
&lt;h2 id="退場的固定順序">退場的固定順序&lt;/h2>
&lt;p>實例退場的四個步驟要固定順序：平台先把這個實例從流量目標摘掉、服務停止接受新請求、服務完成手上的在途請求、實例退出。順序穩定，rollback 這種「反向操作」才能在同一套機制下運作。這條順序的第一步對應 &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">readiness&lt;/a>——關閉的起手式是把 readiness 轉為否，讓平台停止送新流量，而不是直接關進程。先把流量停掉再收束在途工作，跟先關進程再期待流量自己不來，是完全不同的結果。&lt;/p>
&lt;p>這裡有一個單機環境不會遇到、多機才有的細節：readiness 轉為否，到平台真的停止送流量之間，有一段傳播延遲。Kubernetes 把 endpoint 跟 readiness 綁定，readiness 轉否要先傳到 endpoint controller、再傳到每個節點的 kube-proxy 或 envoy，這段期間客戶端仍可能打到已經標記為 not-ready 的實例。穩定的做法是在 preStop hook 加一段短暫等待（5 到 15 秒），讓摘除的狀態傳播到所有轉發層，再開始真正的收束。這段等待是 drain 總窗口的一個子區間，不是浪費——它填的正是「服務說我不 ready」跟「流量真的不再進來」之間的空隙。&lt;/p>
&lt;h2 id="drain-窗口按-workload-決定">drain 窗口按 workload 決定&lt;/h2>
&lt;p>Drain 要留多久，取決於服務跑的是哪種 workload，沒有通用值：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>短請求 API&lt;/strong>（HTTP REST、gRPC unary）：窗口通常 5 到 30 秒，收束條件是在途請求數歸零。主要風險是負載平衡的 deregistration delay 仍會送幾秒流量進來，drain 窗口要覆蓋這段。&lt;/li>
&lt;li>&lt;strong>長連線&lt;/strong>（WebSocket、gRPC streaming、SSE）：窗口從 30 秒到數分鐘，收束條件是現有連線收斂、且重連的波形穩定。主要風險是 reconnect storm——一堆連線同時被斷、同時重連，把接手的實例壓垮。&lt;/li>
&lt;li>&lt;strong>背景 worker&lt;/strong>：窗口取決於單一 job 的最長執行時間，收束條件是不可中斷的 job 跑完。風險是被強制結束的 job 留下不一致狀態。&lt;/li>
&lt;/ul>
&lt;p>服務若混合了多種 workload，drain 窗口取最嚴格（最長）的那個——短請求 5 秒就收完，但同一個服務還有一個要跑兩分鐘的 job，總窗口就得容納兩分鐘。用短請求的窗口去砍一個長 job，等於每次部署都中斷它。&lt;/p>
&lt;h2 id="信號收不到收束就變硬砍">信號收不到，收束就變硬砍&lt;/h2>
&lt;p>清理邏輯的前提是收得到信號，容器環境有三個常見的信號傳不到陷阱，都跟 PID 1 有關。第一個是用 shell 當 PID 1 又不轉發——&lt;code>ENTRYPOINT [&amp;quot;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;java -jar app.jar&amp;quot;]&lt;/code> 這種寫法，&lt;code>SIGTERM&lt;/code> 送到 sh，sh 預設不轉發給 java，java 一直收不到、等 grace period 到期被 &lt;code>SIGKILL&lt;/code> 強殺；修法是用 exec form 或在腳本裡 &lt;code>exec&lt;/code>，讓服務直接當 PID 1。第二個是多進程容器的殭屍回收——PID 1 不做 &lt;code>wait()&lt;/code>，結束的子進程累積成殭屍，這屬於 &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/process-supervisor-selection/" data-link-title="Process supervisor 選型" data-link-desc="在 systemd、supervisord、Docker restart policy、Kubernetes 之間選服務監管方式時，用平台能不能分開表達 startup、readiness、liveness、drain 當判準">supervisor 選型&lt;/a> 裡 init process 的職責。第三個是啟動腳本的 trap handler 卡住，把本來 graceful 的關閉拖成 ungraceful 的 hang——trap handler 本身要設逾時，不能無限等。&lt;/p>
&lt;p>這三個陷阱的共同表現是一樣的：log 裡看不到關閉處理器跑過的紀錄、服務每次都撐到 grace period 上限才消失。看到這個表現，先查 PID 1 是誰、信號有沒有轉發，而不是先懷疑清理邏輯。&lt;/p>
&lt;h2 id="收束要保護的是已承諾未完成的工作">收束要保護的是已承諾未完成的工作&lt;/h2>
&lt;p>Graceful shutdown 真正要保護的，是那些「已經對外承諾、但還沒真正完成」的工作。本站 collector 是個具體例子：它收到事件先回 202、事件進 channel buffer、再非同步寫入儲存。從回 202 到真正寫入之間有一個窗口，這段期間若被 &lt;code>SIGKILL&lt;/code> 硬砍，這些已承諾但未持久化的事件就遺失了。graceful shutdown 的收束序列要 flush 這些 pending write——把 buffer 裡還沒寫的先寫完，再退出。&lt;/p>
&lt;p>哪些關閉保護得了、哪些保護不了，看退出走不走 graceful。走 &lt;code>SIGTERM&lt;/code> 加 grace period 的正常關閉，收束序列有機會 flush；但 OOM kill、硬體故障這種非 graceful 的結束，不走任何清理、在途工作直接中斷——這也是 &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">liveness&lt;/a> 要在記憶體逼近上限時主動回報 unhealthy 的理由：主動回報讓平台在還能 graceful 的時候有序重建，好過等 OOM kill 硬砍中斷在途工作。這條「有沒有走 graceful」的分界在監控上也留得下痕跡——collector 正常關閉會送一個 &lt;code>collector.shutdown&lt;/code> 事件，這個事件的有無，就是區分有序退場跟異常中斷的訊號。&lt;/p></description><content:encoded><![CDATA[<p>服務收到停止信號時，graceful shutdown 決定它是有序收束、還是被硬砍中斷。有序收束的責任分兩層：shutdown 是服務停止接受新工作、釋放自己持有的資源；drain 是平台在真正移除這個實例之前，讓已經在處理的請求、連線、背景工作有時間收完。這兩層都做對，一次正常的部署替換或縮容才不會掉掉在途的工作；做錯，使用者會在每次部署時撞到中斷的請求。</p>
<p>收束的相對面是硬砍。平台給的收束時間是有上限的，超過上限服務就被 <code>SIGKILL</code> 強制結束、不走任何清理。所以 graceful shutdown 的成敗判準是清理邏輯能不能在 grace period 內跑完——跑不完，清理邏輯寫得再完整也等於沒有。</p>
<h2 id="信號路徑與-grace-period">信號路徑與 grace period</h2>
<p>關閉從一個信號開始，設計的第一件事是確認這個信號真的到得了服務、以及服務有足夠時間反應。在 Kubernetes 上，平台先執行 preStop hook、再送 <code>SIGTERM</code>；<code>terminationGracePeriodSeconds</code> 是平台願意等的最長時間，超過就 <code>SIGKILL</code>。這個值要覆蓋 preStop、drain、資源釋放的總時間——設太短，收束到一半被硬砍。</p>
<p>驗證信號到不到得了服務，靠實際觸發一次關閉看紀錄：在 staging 觸發實例刪除，看 log 有沒有出現關閉處理器的紀錄。沒看到，代表信號根本沒傳到服務，要先修傳遞路徑、再談清理邏輯——清理邏輯寫得再完整，信號收不到就一行都不會跑。</p>
<h2 id="退場的固定順序">退場的固定順序</h2>
<p>實例退場的四個步驟要固定順序：平台先把這個實例從流量目標摘掉、服務停止接受新請求、服務完成手上的在途請求、實例退出。順序穩定，rollback 這種「反向操作」才能在同一套機制下運作。這條順序的第一步對應 <a href="/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">readiness</a>——關閉的起手式是把 readiness 轉為否，讓平台停止送新流量，而不是直接關進程。先把流量停掉再收束在途工作，跟先關進程再期待流量自己不來，是完全不同的結果。</p>
<p>這裡有一個單機環境不會遇到、多機才有的細節：readiness 轉為否，到平台真的停止送流量之間，有一段傳播延遲。Kubernetes 把 endpoint 跟 readiness 綁定，readiness 轉否要先傳到 endpoint controller、再傳到每個節點的 kube-proxy 或 envoy，這段期間客戶端仍可能打到已經標記為 not-ready 的實例。穩定的做法是在 preStop hook 加一段短暫等待（5 到 15 秒），讓摘除的狀態傳播到所有轉發層，再開始真正的收束。這段等待是 drain 總窗口的一個子區間，不是浪費——它填的正是「服務說我不 ready」跟「流量真的不再進來」之間的空隙。</p>
<h2 id="drain-窗口按-workload-決定">drain 窗口按 workload 決定</h2>
<p>Drain 要留多久，取決於服務跑的是哪種 workload，沒有通用值：</p>
<ul>
<li><strong>短請求 API</strong>（HTTP REST、gRPC unary）：窗口通常 5 到 30 秒，收束條件是在途請求數歸零。主要風險是負載平衡的 deregistration delay 仍會送幾秒流量進來，drain 窗口要覆蓋這段。</li>
<li><strong>長連線</strong>（WebSocket、gRPC streaming、SSE）：窗口從 30 秒到數分鐘，收束條件是現有連線收斂、且重連的波形穩定。主要風險是 reconnect storm——一堆連線同時被斷、同時重連，把接手的實例壓垮。</li>
<li><strong>背景 worker</strong>：窗口取決於單一 job 的最長執行時間，收束條件是不可中斷的 job 跑完。風險是被強制結束的 job 留下不一致狀態。</li>
</ul>
<p>服務若混合了多種 workload，drain 窗口取最嚴格（最長）的那個——短請求 5 秒就收完，但同一個服務還有一個要跑兩分鐘的 job，總窗口就得容納兩分鐘。用短請求的窗口去砍一個長 job，等於每次部署都中斷它。</p>
<h2 id="信號收不到收束就變硬砍">信號收不到，收束就變硬砍</h2>
<p>清理邏輯的前提是收得到信號，容器環境有三個常見的信號傳不到陷阱，都跟 PID 1 有關。第一個是用 shell 當 PID 1 又不轉發——<code>ENTRYPOINT [&quot;sh&quot;, &quot;-c&quot;, &quot;java -jar app.jar&quot;]</code> 這種寫法，<code>SIGTERM</code> 送到 sh，sh 預設不轉發給 java，java 一直收不到、等 grace period 到期被 <code>SIGKILL</code> 強殺；修法是用 exec form 或在腳本裡 <code>exec</code>，讓服務直接當 PID 1。第二個是多進程容器的殭屍回收——PID 1 不做 <code>wait()</code>，結束的子進程累積成殭屍，這屬於 <a href="/blog/operations/04-service-health/process-supervisor-selection/" data-link-title="Process supervisor 選型" data-link-desc="在 systemd、supervisord、Docker restart policy、Kubernetes 之間選服務監管方式時，用平台能不能分開表達 startup、readiness、liveness、drain 當判準">supervisor 選型</a> 裡 init process 的職責。第三個是啟動腳本的 trap handler 卡住，把本來 graceful 的關閉拖成 ungraceful 的 hang——trap handler 本身要設逾時，不能無限等。</p>
<p>這三個陷阱的共同表現是一樣的：log 裡看不到關閉處理器跑過的紀錄、服務每次都撐到 grace period 上限才消失。看到這個表現，先查 PID 1 是誰、信號有沒有轉發，而不是先懷疑清理邏輯。</p>
<h2 id="收束要保護的是已承諾未完成的工作">收束要保護的是已承諾未完成的工作</h2>
<p>Graceful shutdown 真正要保護的，是那些「已經對外承諾、但還沒真正完成」的工作。本站 collector 是個具體例子：它收到事件先回 202、事件進 channel buffer、再非同步寫入儲存。從回 202 到真正寫入之間有一個窗口，這段期間若被 <code>SIGKILL</code> 硬砍，這些已承諾但未持久化的事件就遺失了。graceful shutdown 的收束序列要 flush 這些 pending write——把 buffer 裡還沒寫的先寫完，再退出。</p>
<p>哪些關閉保護得了、哪些保護不了，看退出走不走 graceful。走 <code>SIGTERM</code> 加 grace period 的正常關閉，收束序列有機會 flush；但 OOM kill、硬體故障這種非 graceful 的結束，不走任何清理、在途工作直接中斷——這也是 <a href="/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">liveness</a> 要在記憶體逼近上限時主動回報 unhealthy 的理由：主動回報讓平台在還能 graceful 的時候有序重建，好過等 OOM kill 硬砍中斷在途工作。這條「有沒有走 graceful」的分界在監控上也留得下痕跡——collector 正常關閉會送一個 <code>collector.shutdown</code> 事件，這個事件的有無，就是區分有序退場跟異常中斷的訊號。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>收束的第一步是 readiness 轉否、停止接流量 → <a href="/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">Liveness 與 Readiness</a></li>
<li>信號傳不到的 PID 1 選型問題 → <a href="/blog/operations/04-service-health/process-supervisor-selection/" data-link-title="Process supervisor 選型" data-link-desc="在 systemd、supervisord、Docker restart policy、Kubernetes 之間選服務監管方式時，用平台能不能分開表達 startup、readiness、liveness、drain 當判準">Process supervisor 選型</a></li>
<li>部署替換時 drain 與 rollback 的完整流程 → <a href="/blog/backend/05-deployment-platform/deployment-rollout-drain-rollback/" data-link-title="5.8 Deployment Rollout with Drain and Rollback（實作示範）" data-link-desc="以 checkout service 示範部署切換如何交付 canary evidence、drain signal、release gate 與 incident decision log。">Backend 部署替換、drain 與 rollback</a></li>
<li>端到端資料完整性：已承諾未持久化窗口的更多場景 → <a href="/blog/monitoring/04-collector/data-integrity/" data-link-title="端到端資料完整性" data-link-desc="從 SDK 到 storage 的資料損失地圖 — 每個環節的損失類型、控制策略、完整性指標、被自己 SDK DDoS 的防護">端到端資料完整性</a></li>
</ul>
]]></content:encoded></item></channel></rss>