<?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>Systemd on Tarragon</title><link>https://tarrragon.github.io/blog/tags/systemd/</link><description>Recent content in Systemd on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 02 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/systemd/index.xml" rel="self" type="application/rss+xml"/><item><title>程序、服務與狀態怎麼判</title><link>https://tarrragon.github.io/blog/linux/debug/process-service-state-diagnosis/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/debug/process-service-state-diagnosis/</guid><description>&lt;p>判斷「某個東西現在是什麼狀態」——程式活著沒、服務由誰提供、螢幕鎖了沒、session 還在不在——是除錯裡最常做、也最常判錯的一步。判錯多半不是工具不對，是問錯了來源：用一個猜的名字去掃行程、用畫面有沒有反應去推服務狀態、用畫面上有沒有某個元素去斷定 session 狀態。這篇把幾個常見的狀態判斷，對到它們各自的權威來源與正確工具。&lt;/p>
&lt;p>底層的心法（讀權威狀態、不靠肉眼）見 &lt;a href="../diagnosis-read-authoritative-state/">診斷心法&lt;/a>，這篇是它在「程序 / 服務 / 狀態」這一類的具體招式。&lt;/p>
&lt;h2 id="程式活著沒比對正確的行程名">程式活著沒：比對正確的行程名&lt;/h2>
&lt;p>判斷一個程式在不在，行程表是權威來源，&lt;code>pgrep&lt;/code> / &lt;code>ps&lt;/code> 是對的工具，但成敗在於&lt;strong>比對正確的行程名&lt;/strong>（comm，行程表裡記的執行檔短名，可從 &lt;code>/proc/&amp;lt;pid&amp;gt;/comm&lt;/code> 看）。一個實際的坑：某個桌面 shell（畫桌面 UI 的圖形程式，不是 bash/zsh 那種命令列 shell）的可執行檔叫 &lt;code>quickshell&lt;/code>，但透過名為 &lt;code>qs&lt;/code> 的 symlink 啟動時，它在行程表裡的 comm 是 &lt;code>qs&lt;/code>。這時 &lt;code>pgrep quickshell&lt;/code> 找不到，很容易誤判成程式掛了、甚至誤觸「重啟」而引發更大的問題，實際上它以 &lt;code>qs&lt;/code> 這個名字好好跑著。&lt;/p>
&lt;p>可靠的做法：&lt;/p>
&lt;ul>
&lt;li>先確認實際的 comm 名：&lt;code>ps -eo pid,comm | grep -i &amp;lt;關鍵字&amp;gt;&lt;/code>，或看你啟動它的實際指令。&lt;/li>
&lt;li>用精確比對：&lt;code>pgrep -x &amp;lt;comm&amp;gt;&lt;/code>（&lt;code>-x&lt;/code> 要求完全相符），或 &lt;code>pgrep -af &amp;lt;pattern&amp;gt;&lt;/code> 連完整命令列一起比對，避免被 symlink 名 / 縮寫名騙。&lt;/li>
&lt;li>另一個 comm 的坑：kernel 把 comm 截在 15 字元（&lt;code>TASK_COMM_LEN&lt;/code>），名字超過 15 字的程式用 &lt;code>pgrep -x &amp;lt;完整長名&amp;gt;&lt;/code> 反而 miss——這時改用 &lt;code>pgrep -af &amp;lt;pattern&amp;gt;&lt;/code> 比對完整命令列。&lt;/li>
&lt;li>別用一個「你以為的名字」掃過去就下生死結論——行程表沒騙你，是查詢條件寫錯。&lt;/li>
&lt;/ul>
&lt;h3 id="進程活著--內部子系統活著">進程活著 ≠ 內部子系統活著&lt;/h3>
&lt;p>比對到了正確的 comm、&lt;code>pgrep&lt;/code> 也有輸出，只證明「這個進程存在」，不證明「它內部在正常運作」。有一類故障是進程好端端活著（&lt;code>pgrep&lt;/code> 找得到、STAT 是正常的 &lt;code>S&lt;/code>、在 &lt;code>poll&lt;/code> 等事件、CPU 不高），但它內部某個子系統已經 wedged——例如一個圖形 shell 的 QML scene 因為上游錯誤（渲染 pipeline 建失敗之類）某個物件沒建起來變 null，於是負責互動的模組全部失效。表現是 bar 還畫得出來、卻點不動，keybind 叫不出東西，但焦點視窗打字正常。這時 &lt;code>pgrep&lt;/code> 會騙你說「在跑」。&lt;/p>
&lt;p>這種情況權威來源不是行程表，是&lt;strong>程式自己的 log&lt;/strong>，而且這種 log 常常不在 &lt;code>journalctl&lt;/code>、也不在你猜的路徑，要用該程式專屬的 log 指令（例如某桌面 shell 的 &lt;code>&amp;lt;shell&amp;gt; -l&lt;/code>）。log 裡的 &lt;code>TypeError: Cannot read property 'X' of null&lt;/code> 這類訊息，才是「進程活著但子系統死了」的定案證據。另一個更精準的活性探針是程式的 &lt;strong>IPC 回不回真實狀態&lt;/strong>：正常時查詢會回傳資料、子系統死掉時回空——這比「進程在不在」可靠得多。判「進程活著到底有沒有在運作」時，讀它自己的 log 與 IPC，不是看 &lt;code>pgrep&lt;/code> 有沒有輸出。桌面 shell 的具體案例與恢復（讀 &lt;code>caelestia shell -l&lt;/code> 抓到 null 根因、重啟重建 scene）見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作&lt;/a> 的「畫得出來但互動死掉」場景。&lt;/p>
&lt;h2 id="服務由誰提供問註冊表">服務由誰提供：問註冊表&lt;/h2>
&lt;p>「某個系統服務現在由哪個程式在提供」，權威來源是服務註冊，不是畫面。桌面服務多半註冊在 &lt;strong>D-Bus&lt;/strong>（Linux 桌面的行程間訊息匯流排）上：一個服務用一個名字掛在上面，而&lt;strong>同一個名字同一時間只能被一個行程擁有&lt;/strong>。以桌面通知為例，&lt;code>org.freedesktop.Notifications&lt;/code> 這個 D-Bus 名同一時間只有一個擁有者——兩個通知 daemon（例如 mako 跟某個桌面 shell 內建的通知服務）不能共存，誰先註冊誰佔著，後者只能等前者退出。&lt;/p>
&lt;p>想知道現在是誰接管，查註冊表而不是送一則通知看畫面：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 查 org.freedesktop.Notifications 目前被哪個連線擁有&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nv">owner&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> org.freedesktop.DBus GetNameOwner s org.freedesktop.Notifications &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print $2}&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> tr -d &lt;span class="s1">&amp;#39;&amp;#34;&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 把那個連線換算成 PID，再看行程名&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nv">pid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> org.freedesktop.DBus GetConnectionUnixProcessID s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$owner&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print $2}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">ps -o &lt;span class="nv">comm&lt;/span>&lt;span class="o">=&lt;/span> -p &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$pid&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>停掉舊 daemon 前擁有者是舊的、停掉後換成新的，就確認接管成功。這比「送通知看畫面有沒有跳」可靠——畫面沒跳可能是勿擾模式吃掉、可能根本沒送出，畫面反應不等於服務歸屬。切換兩個搶同一服務名的 daemon 時，這也解釋了為什麼「新的裝了卻沒作用」：舊的還佔著名字，新的靜默註冊失敗（通常只在它的 log 留一行 warning），得先停掉舊的。&lt;/p></description><content:encoded><![CDATA[<p>判斷「某個東西現在是什麼狀態」——程式活著沒、服務由誰提供、螢幕鎖了沒、session 還在不在——是除錯裡最常做、也最常判錯的一步。判錯多半不是工具不對，是問錯了來源：用一個猜的名字去掃行程、用畫面有沒有反應去推服務狀態、用畫面上有沒有某個元素去斷定 session 狀態。這篇把幾個常見的狀態判斷，對到它們各自的權威來源與正確工具。</p>
<p>底層的心法（讀權威狀態、不靠肉眼）見 <a href="../diagnosis-read-authoritative-state/">診斷心法</a>，這篇是它在「程序 / 服務 / 狀態」這一類的具體招式。</p>
<h2 id="程式活著沒比對正確的行程名">程式活著沒：比對正確的行程名</h2>
<p>判斷一個程式在不在，行程表是權威來源，<code>pgrep</code> / <code>ps</code> 是對的工具，但成敗在於<strong>比對正確的行程名</strong>（comm，行程表裡記的執行檔短名，可從 <code>/proc/&lt;pid&gt;/comm</code> 看）。一個實際的坑：某個桌面 shell（畫桌面 UI 的圖形程式，不是 bash/zsh 那種命令列 shell）的可執行檔叫 <code>quickshell</code>，但透過名為 <code>qs</code> 的 symlink 啟動時，它在行程表裡的 comm 是 <code>qs</code>。這時 <code>pgrep quickshell</code> 找不到，很容易誤判成程式掛了、甚至誤觸「重啟」而引發更大的問題，實際上它以 <code>qs</code> 這個名字好好跑著。</p>
<p>可靠的做法：</p>
<ul>
<li>先確認實際的 comm 名：<code>ps -eo pid,comm | grep -i &lt;關鍵字&gt;</code>，或看你啟動它的實際指令。</li>
<li>用精確比對：<code>pgrep -x &lt;comm&gt;</code>（<code>-x</code> 要求完全相符），或 <code>pgrep -af &lt;pattern&gt;</code> 連完整命令列一起比對，避免被 symlink 名 / 縮寫名騙。</li>
<li>另一個 comm 的坑：kernel 把 comm 截在 15 字元（<code>TASK_COMM_LEN</code>），名字超過 15 字的程式用 <code>pgrep -x &lt;完整長名&gt;</code> 反而 miss——這時改用 <code>pgrep -af &lt;pattern&gt;</code> 比對完整命令列。</li>
<li>別用一個「你以為的名字」掃過去就下生死結論——行程表沒騙你，是查詢條件寫錯。</li>
</ul>
<h3 id="進程活著--內部子系統活著">進程活著 ≠ 內部子系統活著</h3>
<p>比對到了正確的 comm、<code>pgrep</code> 也有輸出，只證明「這個進程存在」，不證明「它內部在正常運作」。有一類故障是進程好端端活著（<code>pgrep</code> 找得到、STAT 是正常的 <code>S</code>、在 <code>poll</code> 等事件、CPU 不高），但它內部某個子系統已經 wedged——例如一個圖形 shell 的 QML scene 因為上游錯誤（渲染 pipeline 建失敗之類）某個物件沒建起來變 null，於是負責互動的模組全部失效。表現是 bar 還畫得出來、卻點不動，keybind 叫不出東西，但焦點視窗打字正常。這時 <code>pgrep</code> 會騙你說「在跑」。</p>
<p>這種情況權威來源不是行程表，是<strong>程式自己的 log</strong>，而且這種 log 常常不在 <code>journalctl</code>、也不在你猜的路徑，要用該程式專屬的 log 指令（例如某桌面 shell 的 <code>&lt;shell&gt; -l</code>）。log 裡的 <code>TypeError: Cannot read property 'X' of null</code> 這類訊息，才是「進程活著但子系統死了」的定案證據。另一個更精準的活性探針是程式的 <strong>IPC 回不回真實狀態</strong>：正常時查詢會回傳資料、子系統死掉時回空——這比「進程在不在」可靠得多。判「進程活著到底有沒有在運作」時，讀它自己的 log 與 IPC，不是看 <code>pgrep</code> 有沒有輸出。桌面 shell 的具體案例與恢復（讀 <code>caelestia shell -l</code> 抓到 null 根因、重啟重建 scene）見 <a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作</a> 的「畫得出來但互動死掉」場景。</p>
<h2 id="服務由誰提供問註冊表">服務由誰提供：問註冊表</h2>
<p>「某個系統服務現在由哪個程式在提供」，權威來源是服務註冊，不是畫面。桌面服務多半註冊在 <strong>D-Bus</strong>（Linux 桌面的行程間訊息匯流排）上：一個服務用一個名字掛在上面，而<strong>同一個名字同一時間只能被一個行程擁有</strong>。以桌面通知為例，<code>org.freedesktop.Notifications</code> 這個 D-Bus 名同一時間只有一個擁有者——兩個通知 daemon（例如 mako 跟某個桌面 shell 內建的通知服務）不能共存，誰先註冊誰佔著，後者只能等前者退出。</p>
<p>想知道現在是誰接管，查註冊表而不是送一則通知看畫面：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 查 org.freedesktop.Notifications 目前被哪個連線擁有</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">owner</span><span class="o">=</span><span class="k">$(</span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  org.freedesktop.DBus GetNameOwner s org.freedesktop.Notifications <span class="p">|</span> awk <span class="s1">&#39;{print $2}&#39;</span> <span class="p">|</span> tr -d <span class="s1">&#39;&#34;&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 把那個連線換算成 PID，再看行程名</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nv">pid</span><span class="o">=</span><span class="k">$(</span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  org.freedesktop.DBus GetConnectionUnixProcessID s <span class="s2">&#34;</span><span class="nv">$owner</span><span class="s2">&#34;</span> <span class="p">|</span> awk <span class="s1">&#39;{print $2}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">ps -o <span class="nv">comm</span><span class="o">=</span> -p <span class="s2">&#34;</span><span class="nv">$pid</span><span class="s2">&#34;</span></span></span></code></pre></div><p>停掉舊 daemon 前擁有者是舊的、停掉後換成新的，就確認接管成功。這比「送通知看畫面有沒有跳」可靠——畫面沒跳可能是勿擾模式吃掉、可能根本沒送出，畫面反應不等於服務歸屬。切換兩個搶同一服務名的 daemon 時，這也解釋了為什麼「新的裝了卻沒作用」：舊的還佔著名字，新的靜默註冊失敗（通常只在它的 log 留一行 warning），得先停掉舊的。</p>
<h2 id="桌面-session-有沒有被鎖認清是哪一層的鎖">桌面 session 有沒有被鎖：認清是哪一層的鎖</h2>
<p>判斷一個圖形 session 有沒有被鎖，最容易被畫面帶偏，因為「畫面上有密碼框」很有說服力、卻不等於 session 真的被鎖（現代桌面 shell 的儀表板常內嵌鎖屏樣式的 widget）。而且鎖有不同層，查錯層會得到誤導的答案。</p>
<p>關鍵是分清兩種鎖：</p>
<ul>
<li><strong>logind 層的鎖</strong>：systemd 登入管理的 session 鎖，權威狀態是 <code>loginctl show-session &lt;id&gt; -p LockedHint</code>。</li>
<li><strong>Wayland 合成器層的鎖</strong>：走 <code>ext-session-lock</code> 協議、由<strong>合成器</strong>（compositor，Wayland 下負責把各視窗合成到螢幕、管輸入輸出的核心程式，約當 X11 時代的視窗管理器加顯示伺服器；Hyprland、Sway 等都是）管的鎖，跟 logind 是獨立機制。這種鎖 <code>loginctl</code> 的 <code>LockedHint</code> <strong>查不到</strong>——不是沒鎖，是查錯層。（用 GNOME / KDE 的鎖屏走的機制不同，以下的 <code>ext-session-lock</code> 判法與復原針對 wlroots 系的 Wayland 合成器。）</li>
</ul>
<p>所以「<code>loginctl</code> 沒有 <code>LockedHint</code>、<code>pgrep</code> 找不到獨立鎖屏程式」不足以斷定「沒鎖」：合成器層的鎖不歸 logind、而鎖屏畫面可能由 shell 主程式在自己行程內畫（沒有獨立可執行檔可抓）。這種情況真正的權威來源是那個 shell 自己的 log（有沒有載入鎖屏模組、idle 計時器有沒有觸發鎖定），或直接看 compositor 的 session-lock 狀態。判鎖看合成器 / shell 的 log，不是 <code>loginctl</code>、更不是畫面有沒有密碼框。</p>
<h3 id="鎖屏程式死掉造成的死局與復原">鎖屏程式死掉造成的死局與復原</h3>
<p><code>ext-session-lock</code> 有一個安全設計：持鎖的鎖屏程式若在鎖定狀態下崩潰 / 被中止，compositor <strong>會保持鎖定</strong>、不會因為鎖屏程式沒了就解鎖（否則殺掉鎖屏程式就成了繞過鎖的漏洞）。表現是畫面卡在「鎖屏程式已死」的安全提示。復原要從另一個 VT 或 SSH 用 <code>hyprctl keyword misc:allow_session_lock_restore 1</code> 允許新鎖屏 client 接管、再 <code>hyprctl dispatch exec hyprlock</code> 起一個接管後輸密碼解鎖。完整機制、兩層鎖的關係、各 compositor 的差異，見 <a href="/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Wayland Session Lock 卡</a>。</p>
<p>診斷紀律：<strong>測鎖屏、或 <code>pkill</code> 一個持鎖的鎖屏程式時，要預期它把 session 卡在鎖定——這是協議的安全設計，不是 bug。</strong> 自動化 / 無人值守流程尤其要避免在持鎖狀態下殺鎖屏程式。</p>
<h2 id="終端機多工器的-session-還在不在">終端機多工器的 session 還在不在</h2>
<p>用 zellij / tmux 這類多工器跑遠端長任務時，判斷「重連後那個 session 還在不在」的權威來源是多工器自己的 session 列表，不是「我 SSH 斷了所以應該還在吧」的假設。<code>zellij ls</code>（或 <code>tmux ls</code>）會列出 session 與狀態：多工器是常駐在遠端的程序，SSH 斷不影響它，所以只要那台機器沒重開，<code>attach</code> 就能接回去；但如果機器重開過、或那個 session 因為資源不足（例如磁碟滿觸發的連鎖）被殺，列表會顯示它已 <code>EXITED</code> / 不存在，這種接不回去。</p>
<p>這裡有個順序上的紀律：<strong>當一個 session 可能已經死掉、而它裡面跑的任務有你在意的產出時，先確認產出有沒有被安全保存，再處理 session。</strong> 例如任務是在改 git repo，先 <code>git -C &lt;repo&gt; status</code> 跟 <code>git log @{u}..</code>（本地有、遠端沒有的 commit）確認有沒有沒推送的東西、把該推的推掉，再去 <code>zellij delete</code> 清死 session。搞反順序、先清了 session，可能連帶失去唯一還記得那些改動的地方。權威狀態（git 的推送狀態、多工器的 session 列表）先讀清楚，再動手。</p>
<h2 id="判讀路由">判讀路由</h2>
<ul>
<li>判程式活著 → <code>pgrep -x &lt;正確 comm&gt;</code> / <code>pgrep -af &lt;pattern&gt;</code>，先確認實際 comm 名，別用猜的名字。</li>
<li>判進程活著但「有沒有在運作」→ 讀程式自己的 log（可能要用它專屬的 log 指令、不在 journalctl）+ 它的 IPC 回不回真實狀態，不是看 <code>pgrep</code> 有輸出就當正常。</li>
<li>判服務歸誰 → <code>busctl</code> 查 D-Bus name 擁有者 → 換算 PID → comm，不看畫面反應。</li>
<li>判 session 鎖沒鎖 → 分清 logind 層（<code>loginctl LockedHint</code>）vs 合成器層（<code>ext-session-lock</code>，看 compositor / shell log），不看畫面有沒有密碼框。</li>
<li>鎖屏程式死掉卡住 → <code>allow_session_lock_restore</code> + 重起鎖屏程式接管解鎖。</li>
<li>判多工器 session 存活 → <code>zellij ls</code> / <code>tmux ls</code>；可能已死且有在意的產出時，先確認產出已保存 / 已推送再清 session。</li>
</ul>
<p>判不準時，<a href="../diagnosis-read-authoritative-state/">診斷心法</a> 的四步（描述症狀、定位權威來源、用對工具讀、矛盾時信權威）是通用的回退。</p>
]]></content:encoded></item><item><title>模組四：服務探活與自動恢復</title><link>https://tarrragon.github.io/blog/devops/04-service-health/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/04-service-health/</guid><description>&lt;p>回答「服務掛了怎麼知道、知道了怎麼自動恢復」。探活是所有自動恢復機制的前提。&lt;/p>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> Health check endpoint 設計（什麼算健康、什麼算不健康、check 的深度）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> Liveness vs Readiness（活著 vs 準備好接流量 — Kubernetes 的兩種 probe）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> systemd watchdog + 自動重啟（WatchdogSec + Restart=on-failure）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> Process supervisor 的選型（systemd / supervisord / Docker restart policy）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> Graceful shutdown（收到 SIGTERM 後的清理流程）&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/dashboard-devops/" data-link-title="DevOps Dashboard 設計" data-link-desc="Collector 和 SDK 是否健康 — 日常監控的服務狀態卡、吞吐量曲線、儲存用量，以及告警觸發後的排障視圖">monitoring 模組四 Dashboard DevOps&lt;/a>：DevOps dashboard 的服務狀態卡依賴 health check&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend 部署平台&lt;/a>：部署平台的 health check 整合&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「服務掛了怎麼知道、知道了怎麼自動恢復」。探活是所有自動恢復機制的前提。</p>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input disabled="" type="checkbox"> Health check endpoint 設計（什麼算健康、什麼算不健康、check 的深度）</li>
<li><input disabled="" type="checkbox"> Liveness vs Readiness（活著 vs 準備好接流量 — Kubernetes 的兩種 probe）</li>
<li><input disabled="" type="checkbox"> systemd watchdog + 自動重啟（WatchdogSec + Restart=on-failure）</li>
<li><input disabled="" type="checkbox"> Process supervisor 的選型（systemd / supervisord / Docker restart policy）</li>
<li><input disabled="" type="checkbox"> Graceful shutdown（收到 SIGTERM 後的清理流程）</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/monitoring/04-collector/dashboard-devops/" data-link-title="DevOps Dashboard 設計" data-link-desc="Collector 和 SDK 是否健康 — 日常監控的服務狀態卡、吞吐量曲線、儲存用量，以及告警觸發後的排障視圖">monitoring 模組四 Dashboard DevOps</a>：DevOps dashboard 的服務狀態卡依賴 health check</li>
<li>→ <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">backend 部署平台</a>：部署平台的 health check 整合</li>
</ul>
]]></content:encoded></item><item><title>服務掛了怎麼自動知道：從肉眼盯到主動告警</title><link>https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/</guid><description>&lt;p>服務掛了不需要用肉眼盯——systemd 本來就在追蹤每個 unit 的狀態，你要做的是把「讀權威狀態」這件事自動化，並在狀態變成失敗時主動推播給自己。這篇跟本系列其他篇的差別在時機：診斷是出事後回頭找根因，監控是讓系統在出事的當下就告訴你。兩者共用同一個地基——權威狀態。診斷是手動讀一次權威狀態，監控是訂閱權威狀態的變化、變壞就推播。&lt;/p>
&lt;p>理解這個框架後，監控就不是「裝一套很重的東西」，而是分層選擇：從 systemd 內建的失敗鉤子（不裝任何額外服務），到推播管道，到「整台機器死掉」的體外心跳，到完整的指標儀表板。多數人只需要前一兩層。&lt;/p>
&lt;h2 id="你現在手動在做的事要被取代的基線">你現在手動在做的事（要被取代的基線）&lt;/h2>
&lt;p>在自動化之前，先認清手動版本——這也是所有告警底層讀的同一個權威來源：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">systemctl --failed &lt;span class="c1"># 現在有哪些 unit 處於 failed（開機後系統怪怪的先掃這個）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">systemctl is-failed &amp;lt;unit&amp;gt; &lt;span class="c1"># 單一 unit 明確判失敗（比 is-active 直接）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">journalctl -u &amp;lt;unit&amp;gt; -f &lt;span class="c1"># 即時跟一個 unit 的 log&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>systemctl --failed&lt;/code> 就是「服務死活」的權威清單。手動版的問題不是不準，是你得記得去看。下面每一層都是把「記得去看」換成「壞了它來找你」。&lt;/p>
&lt;h2 id="第一層systemd-原生-onfailure-鉤子不裝額外服務">第一層：systemd 原生 &lt;code>OnFailure&lt;/code> 鉤子（不裝額外服務）&lt;/h2>
&lt;p>systemd 每個 unit 進入 failed 狀態時，可以自動觸發另一個 unit。這是最正統、零額外依賴的做法——告警邏輯就寫成一個普通的 systemd service。它由三塊組成：一個負責送通知的處理器 unit、一個實際送出的腳本、以及在你要監控的 unit 上掛一行 &lt;code>OnFailure=&lt;/code>。&lt;/p>
&lt;p>&lt;strong>通知處理器&lt;/strong>是一個 template unit（&lt;code>@&lt;/code> 表示可帶參數），參數 &lt;code>%i&lt;/code> 會是失敗的那個 unit 名：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># /etc/systemd/system/alert@.service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">Description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">Alert on failure of %i&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="na">Type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">oneshot&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="na">ExecStart&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">/usr/local/bin/notify-failure %i&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>送出腳本&lt;/strong>負責把「哪個 unit、在哪台機、什麼時候」推出去。這裡有個實測踩到的坑：在 systemd service 的執行環境下，&lt;code>hostname&lt;/code> 指令可能回傳空字串，要改用 &lt;code>uname -n&lt;/code> 或讀 &lt;code>/etc/hostname&lt;/code> 才穩：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="c1"># /usr/local/bin/notify-failure （記得 chmod +x）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nv">unit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只在「真正放棄」時告警：OnFailure 每次失敗都觸發（含 auto-restart 中途，見下節實測），&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># auto-restart 中途 ActiveState 是 activating、撞重試上限才進 failed。gate 掉中途避免洗告警。&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nv">state&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>systemctl show &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$unit&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -p ActiveState --value&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="o">[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$state&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> failed &lt;span class="o">]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">exit&lt;/span> &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="nv">host&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname -n&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="c1"># 不要用 hostname，systemd 環境下可能回空&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nv">ts&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date -Is&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nv">topic&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;你的私密topic&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">curl -fsS &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Title: &lt;/span>&lt;span class="nv">$host&lt;/span>&lt;span class="s2">: &lt;/span>&lt;span class="nv">$unit&lt;/span>&lt;span class="s2"> failed&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$unit&lt;/span>&lt;span class="s2"> 於 &lt;/span>&lt;span class="nv">$ts&lt;/span>&lt;span class="s2"> 進入 failed&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;https://ntfy.sh/&lt;/span>&lt;span class="nv">$topic&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>在要監控的 unit 掛上鉤子&lt;/strong>。針對單一 unit，加一行：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="na">OnFailure&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">alert@%n.service # %n 是本 unit 的全名，會展開成 alert@&amp;lt;本unit&amp;gt;.service&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>要&lt;strong>一次套用到所有 service&lt;/strong>，用 top-level drop-in（放在 &lt;code>service.d/&lt;/code> 這個型別目錄下的設定會套用到每個 &lt;code>.service&lt;/code>）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># /etc/systemd/system/service.d/onfailure.conf&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">OnFailure&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">alert@%n.service&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>改完 &lt;code>sudo systemctl daemon-reload&lt;/code>。&lt;strong>一個必須注意的遞迴陷阱&lt;/strong>：全域 drop-in 也會套到 &lt;code>alert@&lt;/code> 自己，它若失敗會觸發自己。給 &lt;code>alert@.service&lt;/code> 一個清空 &lt;code>OnFailure=&lt;/code> 的 override（&lt;code>[Unit]&lt;/code> 段寫 &lt;code>OnFailure=&lt;/code>）擋掉。&lt;/p>
&lt;p>這條鏈是實測驗證過的：故意讓一個 &lt;code>ExecStart=/bin/false&lt;/code> 的測試 service 失敗，systemd log 出現 &lt;code>Triggering OnFailure= dependencies&lt;/code>、&lt;code>alert@&lt;/code> 處理器被觸發跑完、&lt;code>curl&lt;/code> 推到 ntfy 回 HTTP 200——通知確實送出，全程沒有肉眼介入。&lt;/p>
&lt;h3 id="先自動重啟放棄了才吵你">先自動重啟、放棄了才吵你&lt;/h3>
&lt;p>多數暫時性失敗（一次連線抖動、一個 race）自己重試就好，不值得半夜叫醒你。把「自動復原」跟「告警」分兩段：讓 systemd 先重啟幾次，撐過重試上限才真的算放棄。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="na">Restart&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">on-failure&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">RestartSec&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="na">StartLimitBurst&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">3 # 重試 3 次&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="na">StartLimitIntervalSec&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">60 # 60 秒內都失敗才進 failed（start-limit-hit）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>這裡有個實測踩到、跟直覺相反的坑&lt;/strong>：&lt;code>OnFailure&lt;/code> 不是「放棄才觸發」，而是&lt;strong>每一次失敗都觸發&lt;/strong>——包含 &lt;code>Restart=on-failure&lt;/code> 的每次 auto-restart 中途。實測一個反覆 crash 的服務（重試 3 次後放棄）觸發了 &lt;strong>4 次&lt;/strong> &lt;code>OnFailure&lt;/code>（3 次 auto-restart + 1 次最終 &lt;code>start-limit-hit&lt;/code>）。所以只靠 &lt;code>Restart=&lt;/code> + &lt;code>StartLimit=&lt;/code> 這段 config，你會被每次瞬斷洗告警。&lt;/p></description><content:encoded><![CDATA[<p>服務掛了不需要用肉眼盯——systemd 本來就在追蹤每個 unit 的狀態，你要做的是把「讀權威狀態」這件事自動化，並在狀態變成失敗時主動推播給自己。這篇跟本系列其他篇的差別在時機：診斷是出事後回頭找根因，監控是讓系統在出事的當下就告訴你。兩者共用同一個地基——權威狀態。診斷是手動讀一次權威狀態，監控是訂閱權威狀態的變化、變壞就推播。</p>
<p>理解這個框架後，監控就不是「裝一套很重的東西」，而是分層選擇：從 systemd 內建的失敗鉤子（不裝任何額外服務），到推播管道，到「整台機器死掉」的體外心跳，到完整的指標儀表板。多數人只需要前一兩層。</p>
<h2 id="你現在手動在做的事要被取代的基線">你現在手動在做的事（要被取代的基線）</h2>
<p>在自動化之前，先認清手動版本——這也是所有告警底層讀的同一個權威來源：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">systemctl --failed          <span class="c1"># 現在有哪些 unit 處於 failed（開機後系統怪怪的先掃這個）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl is-failed &lt;unit&gt;  <span class="c1"># 單一 unit 明確判失敗（比 is-active 直接）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">journalctl -u &lt;unit&gt; -f     <span class="c1"># 即時跟一個 unit 的 log</span></span></span></code></pre></div><p><code>systemctl --failed</code> 就是「服務死活」的權威清單。手動版的問題不是不準，是你得記得去看。下面每一層都是把「記得去看」換成「壞了它來找你」。</p>
<h2 id="第一層systemd-原生-onfailure-鉤子不裝額外服務">第一層：systemd 原生 <code>OnFailure</code> 鉤子（不裝額外服務）</h2>
<p>systemd 每個 unit 進入 failed 狀態時，可以自動觸發另一個 unit。這是最正統、零額外依賴的做法——告警邏輯就寫成一個普通的 systemd service。它由三塊組成：一個負責送通知的處理器 unit、一個實際送出的腳本、以及在你要監控的 unit 上掛一行 <code>OnFailure=</code>。</p>
<p><strong>通知處理器</strong>是一個 template unit（<code>@</code> 表示可帶參數），參數 <code>%i</code> 會是失敗的那個 unit 名：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/systemd/system/alert@.service</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Alert on failure of %i</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/local/bin/notify-failure %i</span></span></span></code></pre></div><p><strong>送出腳本</strong>負責把「哪個 unit、在哪台機、什麼時候」推出去。這裡有個實測踩到的坑：在 systemd service 的執行環境下，<code>hostname</code> 指令可能回傳空字串，要改用 <code>uname -n</code> 或讀 <code>/etc/hostname</code> 才穩：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cp"></span><span class="c1"># /usr/local/bin/notify-failure   （記得 chmod +x）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nv">unit</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 只在「真正放棄」時告警：OnFailure 每次失敗都觸發（含 auto-restart 中途，見下節實測），</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># auto-restart 中途 ActiveState 是 activating、撞重試上限才進 failed。gate 掉中途避免洗告警。</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nv">state</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>systemctl show <span class="s2">&#34;</span><span class="nv">$unit</span><span class="s2">&#34;</span> -p ActiveState --value<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$state</span><span class="s2">&#34;</span> <span class="o">=</span> failed <span class="o">]</span> <span class="o">||</span> <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nv">host</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>uname -n<span class="k">)</span><span class="s2">&#34;</span>                     <span class="c1"># 不要用 hostname，systemd 環境下可能回空</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nv">ts</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>date -Is<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">topic</span><span class="o">=</span><span class="s2">&#34;你的私密topic&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">curl -fsS <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  -H <span class="s2">&#34;Title: </span><span class="nv">$host</span><span class="s2">: </span><span class="nv">$unit</span><span class="s2"> failed&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  -d <span class="s2">&#34;</span><span class="nv">$unit</span><span class="s2"> 於 </span><span class="nv">$ts</span><span class="s2"> 進入 failed&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  <span class="s2">&#34;https://ntfy.sh/</span><span class="nv">$topic</span><span class="s2">&#34;</span></span></span></code></pre></div><p><strong>在要監控的 unit 掛上鉤子</strong>。針對單一 unit，加一行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">OnFailure</span><span class="o">=</span><span class="s">alert@%n.service    # %n 是本 unit 的全名，會展開成 alert@&lt;本unit&gt;.service</span></span></span></code></pre></div><p>要<strong>一次套用到所有 service</strong>，用 top-level drop-in（放在 <code>service.d/</code> 這個型別目錄下的設定會套用到每個 <code>.service</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/systemd/system/service.d/onfailure.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">OnFailure</span><span class="o">=</span><span class="s">alert@%n.service</span></span></span></code></pre></div><p>改完 <code>sudo systemctl daemon-reload</code>。<strong>一個必須注意的遞迴陷阱</strong>：全域 drop-in 也會套到 <code>alert@</code> 自己，它若失敗會觸發自己。給 <code>alert@.service</code> 一個清空 <code>OnFailure=</code> 的 override（<code>[Unit]</code> 段寫 <code>OnFailure=</code>）擋掉。</p>
<p>這條鏈是實測驗證過的：故意讓一個 <code>ExecStart=/bin/false</code> 的測試 service 失敗，systemd log 出現 <code>Triggering OnFailure= dependencies</code>、<code>alert@</code> 處理器被觸發跑完、<code>curl</code> 推到 ntfy 回 HTTP 200——通知確實送出，全程沒有肉眼介入。</p>
<h3 id="先自動重啟放棄了才吵你">先自動重啟、放棄了才吵你</h3>
<p>多數暫時性失敗（一次連線抖動、一個 race）自己重試就好，不值得半夜叫醒你。把「自動復原」跟「告警」分兩段：讓 systemd 先重啟幾次，撐過重試上限才真的算放棄。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">RestartSec</span><span class="o">=</span><span class="s">5</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">StartLimitBurst</span><span class="o">=</span><span class="s">3          # 重試 3 次</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">StartLimitIntervalSec</span><span class="o">=</span><span class="s">60   # 60 秒內都失敗才進 failed（start-limit-hit）</span></span></span></code></pre></div><p><strong>這裡有個實測踩到、跟直覺相反的坑</strong>：<code>OnFailure</code> 不是「放棄才觸發」，而是<strong>每一次失敗都觸發</strong>——包含 <code>Restart=on-failure</code> 的每次 auto-restart 中途。實測一個反覆 crash 的服務（重試 3 次後放棄）觸發了 <strong>4 次</strong> <code>OnFailure</code>（3 次 auto-restart + 1 次最終 <code>start-limit-hit</code>）。所以只靠 <code>Restart=</code> + <code>StartLimit=</code> 這段 config，你會被每次瞬斷洗告警。</p>
<p>真正做到「只在放棄才吵」，靠的是上面送出腳本開頭那道 gate：<code>systemctl show &lt;unit&gt; -p ActiveState</code> 在 auto-restart 中途是 <code>activating</code>、撞上限進 failed 才是 <code>failed</code>，腳本只在 <code>failed</code> 才送。加上 gate 後同一個 crash 測試從 4 次告警降到 1 次（只剩最終放棄那次）。config 負責「重試幾次」，handler 的 gate 負責「只在終局告警」——兩段合起來才是完整的「先重啟、放棄才吵」。</p>
<h3 id="抓進程活著但沒在做事外部健康探針">抓「進程活著但沒在做事」：外部健康探針</h3>
<p><code>OnFailure</code> 抓的是「進程狀態變了」——crash、exit、被 kill。但服務可能<strong>進程還在、卻沒在做事</strong>：hung、deadlock、內部子系統壞掉。這種 systemd 看它還 <code>active</code>、不會觸發任何告警——正是<a href="../process-service-state-diagnosis/">「進程活著 ≠ 在運作」</a>那條，搬到監控場景。</p>
<p>要抓這種，得從外面<strong>主動戳它、看它回不回應</strong>：一個 timer 定時對服務發一個健康請求（HTTP 服務就 curl 它的 <code>/health</code>）並設逾時；戳不動、逾時失敗，就讓「那個檢查」自己 failed，一樣走 <code>OnFailure</code> 告警。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># health-check.service（oneshot）+ 一個每 2 分鐘跑的 .timer</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/curl -fsS --max-time 5 http://127.0.0.1:8899/health</span></span></span></code></pre></div><p>實測對照最清楚：讓一個健康服務卡在 <code>sleep</code>（進程還在、單執行緒不再回應），<code>systemctl is-active</code> 仍顯示 <code>active</code>——systemd 沒察覺；但這個外部探針 curl <code>/health</code> 5 秒逾時、check 失敗、告警發出。<strong>systemd 抓進程死、外部探針抓進程活著但 hung，兩層互補、缺一漏一種。</strong></p>
<h3 id="canary先證明告警管線本身是好的">canary：先證明告警管線本身是好的</h3>
<p>監控最怕的失效模式是「出事時才發現它早就不會叫了」。防這個的辦法是養一隻 <strong>canary</strong>——一個你可控的假服務，專門用來確認整條管線是活的。它一物兩用：</p>
<ul>
<li><strong>驗證管線</strong>：故意弄掛它，看「失敗 → OnFailure → 推送」真的一路通到你手機，不必拿 sshd 這種真服務去冒險。</li>
<li><strong>當活性訊號</strong>：它自己若無故失敗告警，等於告訴你告警系統本身還在運作。</li>
</ul>
<p>做法是一個極簡 HTTP 服務（stdlib 就夠、不必框架），留幾個測試入口：<code>/health</code> 正常回、<code>/crash</code> 故意退出（測 <code>OnFailure</code>）、<code>/hang</code> 進程活著但不回應（測外部探針）。這樣任何時候都能一鍵重驗監控沒有默默失效。</p>
<h2 id="第二層推去哪裡關鍵是能離開這台機器">第二層：推去哪裡（關鍵是能離開這台機器）</h2>
<p>處理器腳本裡那一段 <code>curl</code> 可以換成任何管道：</p>
<ul>
<li><strong>ntfy</strong>（<code>ntfy.sh</code> 或自架）：一行 <code>curl</code> 推到手機，最省事，上面的例子就是。它怎麼運作、公共站 vs 自架、以及「topic 名稱就是唯一的密碼」這個安全模型，見 <a href="../ntfy-push-notification-service/">ntfy：推送通知服務</a>。</li>
<li><strong>email</strong>：要先設好一個 MTA（如 <code>msmtp</code>），腳本改成 <code>mail</code> / <code>sendmail</code>。</li>
<li><strong>Telegram bot、Apprise</strong>（一個工具打多個目標）等。</li>
</ul>
<p>判準只有一條：<strong>告警要送到機器外</strong>。送桌面 <code>notify-send</code> 只有你正盯著螢幕時才有用；送手機或 email，離開座位、人在外面也收得到。一台跑正事的機器，告警管道應該落在它之外。</p>
<h2 id="第三層整台機器死掉怎麼辦監控自己的盲點">第三層：整台機器死掉怎麼辦（監控自己的盲點）</h2>
<p><code>OnFailure</code> 有個根本限制：<strong>它靠 systemd 觸發，機器整台掛了（當機、斷電、kernel panic），systemd 自己都沒了，發不出任何告警。</strong> 這是所有「機器自己監控自己」方案的共同盲點——它報得了服務的死，報不了自己這台的死。</p>
<p>覆蓋這一層要反過來做：讓機器定時對一個<strong>體外</strong>的服務「報平安」，平安訊號一停，由那個體外服務替你告警。這叫 dead-man&rsquo;s switch（心跳監控）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/systemd/system/heartbeat.service</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">curl -fsS https://hc-ping.com/&lt;你的-uuid&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 搭配一個 heartbeat.timer，OnUnitActiveSec=5min 定時打</span></span></span></code></pre></div><p>心跳超過設定時間沒到，healthchecks.io（或自架的 Uptime Kuma）就通知你。<strong>體內的監控管不了自己這台的死亡，一定要有體外的一隻眼睛</strong>——這跟本系列 <a href="../machine-unreachable/">機器連不到或起不來</a> 是同一個問題的兩面：那篇是機器已經不回應時從外面怎麼查，心跳是讓「不回應」這件事本身自動觸發告警。</p>
<h2 id="第四層要指標趨勢門檻不只是-updown">第四層：要指標、趨勢、門檻（不只是 up/down）</h2>
<p>當你要的不只是「掛了沒」，而是 CPU、記憶體、磁碟、延遲的趨勢與門檻告警（例如磁碟用量超過 80% 就先警告，接上本系列反覆出現的「磁碟滿連鎖」），就進到完整監控堆疊：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>定位</th>
          <th>什麼時候選它</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Netdata</td>
          <td>開箱即用、自帶大量預設告警</td>
          <td>單機、想要圖表 + 門檻告警、最不想設定</td>
      </tr>
      <tr>
          <td>Monit</td>
          <td>輕量、每服務健康檢查 + 自動動作</td>
          <td>要「掛了自動跑一段修復腳本」、超出 systemd <code>Restart=</code> 能表達的邏輯</td>
      </tr>
      <tr>
          <td>Prometheus + Alertmanager</td>
          <td>指標抓取 + 告警規則引擎</td>
          <td>多台機器、要歷史數據與可擴展的告警規則</td>
      </tr>
      <tr>
          <td>Uptime Kuma</td>
          <td>自架的 up/down + 心跳面板</td>
          <td>想要一個面板統一看多台/多服務、也能當第三層的心跳接收端</td>
      </tr>
  </tbody>
</table>
<p>這一層不是每個人都需要。單機、只想知道某個服務死活，第一層就夠；要看趨勢、跨機、設門檻，才值得付這層的設定與維運成本。</p>
<h2 id="先確認有沒有沒有就從最簡單開始">先確認有沒有，沒有就從最簡單開始</h2>
<p>監控最好在出事之前就建好，不是等第一次沒人發現的當機才想到。有兩個時機該主動確認這台機器有沒有在監控自己：<strong>裝好一台新機器時</strong>，跟<strong>發現自己反覆在除同一個服務的失敗時</strong>。確認的方式就是讀權威狀態：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">systemctl --failed                      <span class="c1"># 現在有沒有 failed 的</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl show sshd -p OnFailure        <span class="c1"># 關鍵服務有沒有掛告警鉤子</span></span></span></code></pre></div><p>沒有任何監控的話，<strong>從最簡單那層開始建，別一開始就上重的</strong>：第一層的 <code>OnFailure</code> + ntfy 就能讓「服務掛了」主動找上你，零額外 daemon、幾個檔案就設好。遠端機器至少把 sshd 掛上——它掛了你就失聯，是最該先監控的一個。等你真的需要趨勢圖、跨機、或告警內容不能經過第三方時，再往自架 ntfy（帳號 + ACL）跟完整監控堆疊爬。多數單機、個人用的情境，停在第一層就夠。</p>
<h2 id="依情境選">依情境選</h2>
<p>把上面四層對回你實際要監控的東西：</p>
<ul>
<li><strong>某個 service 掛了想被通知</strong> → 第一層 <code>OnFailure</code> drop-in + ntfy。不裝額外 daemon，最貼近 systemd。</li>
<li><strong>希望先自動重啟、救不回來才告警</strong> → 第一層再加 <code>Restart=on-failure</code> + <code>StartLimit*</code>。</li>
<li><strong>怕整台機器當掉沒人知道</strong> → 第三層心跳 / dead-man switch。這層體內方案覆蓋不到，必須體外。</li>
<li><strong>要看資源趨勢、跨多台、設門檻告警</strong> → 第四層，單機用 Netdata、多機用 Prometheus 堆疊。</li>
</ul>
<p>判準是先分清你要監控的層級：<strong>單一 service 的死活、整台機器的死活、還是資源的趨勢</strong>——三種對應不同層，別拿其中一種去蓋另一種。最常見的誤區是以為體內的 <code>OnFailure</code> 能報自己這台的當機，那正是它的盲點。</p>
<h2 id="下一步">下一步</h2>
<ul>
<li>告警把你叫來之後，怎麼判那個服務到底是什麼狀態（failed、restart loop、還是活著但子系統 wedged）→ <a href="../process-service-state-diagnosis/">程序、服務與狀態怎麼判</a>。</li>
<li>機器完全不回應、心跳斷掉之後從外面怎麼查 → <a href="../machine-unreachable/">機器連不到或起不來</a>。</li>
<li>底層那套「讀權威狀態、不靠肉眼猜」的判讀紀律 → <a href="../diagnosis-read-authoritative-state/">診斷心法</a>。</li>
</ul>
]]></content:encoded></item></channel></rss>