systemd 相依性與條件:Requires、After 怎麼分
釐清 Requires、Wants、After、Before、ExecStartPre 與 Condition 的差異,掌握 unit 為何未執行,並用正確順序修正開機時失敗問題。
Requires 不代表 After
systemd 的相依性與條件是 4 種獨立機制,但許多 unit file 會把它們當成同一件事使用。Requires= 與 Wants= 決定要拉入哪些其他 unit。After= 與 Before= 決定 unit 的啟動順序。ExecStartPre= 會執行檢查,檢查失敗時可使 unit 失敗。Condition 與 Assert 系列則決定 unit 是否會執行。每種機制都與其他機制獨立,因此某個 unit 即使要求另一個 unit,仍可能與它在同一時間啟動。
最後這句話,是幾乎所有「手動啟動時正常,但開機時失敗」問題背後的原因。
[Unit]
Description=Inventory API
Requires=postgresql.service
[Service]
ExecStartPre=/usr/bin/pg_isready -h 127.0.0.1 -t 5
ExecStart=/usr/local/bin/inventory-apiRequires=postgresql.service 會將 PostgreSQL 拉入相同的啟動交易,但不會等待 PostgreSQL。systemd 會平行啟動這兩個工作,因此 pg_isready 執行時,PostgreSQL 仍在開啟其資料目錄。此時尚未有任何程序監聽,pg_isready 會以 2 結束,unit 也會在執行到 ExecStart 之前失敗。於 1 小時後執行 sudo systemctl start inventory-api 會成功,因為 PostgreSQL 到那時已經啟動。unit file 並沒有任何變更,因此檔案看起來沒有問題。
修正方式只需修改 1 行。
[Unit]
Requires=postgresql.service
After=postgresql.service同一處還有一個容易忽略的細節。只有在同時對 Requires= 設定 After= 時,失敗的相依 unit 才會阻止目前的 unit 啟動。若沒有設定啟動順序,systemd 可能早已啟動目前的 unit,等另一個 unit 失敗時,就沒有可取消的動作。單獨使用 Requires= 並不會提供使用者以為能取得的保護。除非有明確理由不這麼做,否則請在每個 Requires= 與每個 Wants= 旁邊都寫上 After=。
Requires、Wants、Requisite 與 BindsTo 的用途
這些都是相依性設定,不會建立任何啟動順序。
Wants=:拉入其他 unit。即使其他 unit 啟動失敗或不存在,這個 unit 仍會啟動。這是systemctl enable在.wants/目錄中建立符號連結所產生的效果。Requires=:拉入其他 unit。若其他 unit 啟動失敗,且你也對After=建立了順序,這個 unit 就不會啟動。若之後明確停止其他 unit,這個 unit 也會隨之停止。Requisite=:不拉入其他 unit。若其他 unit 尚未啟用,立即讓這個 unit 失敗。BindsTo=:作用類似Requires=,而且無論其他 unit 因任何原因停止,這個 unit 也會停止,包括硬體消失的情況。PartOf=:停止與重新啟動會從其他 unit 向下傳遞到這個 unit。啟動不會傳遞。Conflicts=:啟動這個 unit 時,會停止其他 unit。
對於會與另一個 daemon 通訊的 daemon,Wants= 加上 After= 通常是正確的組合。Requires= 會綁定兩者的生命週期:停止資料庫進行維護時,應用程式也會隨之停止;資料庫恢復後,應用程式不會自動恢復。Wants= 加上 After= 可提供開機順序,而不會綁定兩者的生命週期;重新啟動政策則能處理相依服務之後消失的情況。
你也會繼承未自行撰寫的相依性。使用預設值 DefaultDependencies=yes 時,一般服務會自動取得 Requires=sysinit.target、After=sysinit.target basic.target 與 Conflicts=shutdown.target。因此,即使服務的 [Unit] 區段幾乎是空的,它仍會在開機流程後段啟動,並在關機時正常停止。
After 與 Before 只決定交易順序,不負責其他事項
After= 與 Before= 純粹用於排序,完全不會建立任何需求。若在沒有其他單位會拉入 Redis 的 unit 中使用 After=redis.service,這不會產生任何效果:如果 redis.service 不在交易中,就沒有需要等待的對象,因此您的 unit 會立即啟動。
這點值得重複說明,因為這正是後文 network-online.target 錯誤的具體形式。排序只會等待同一筆交易中已經要啟動的 units。
這一對設定具有對稱性。在 a.service 中寫入 After=b.service,與在 b.service 中寫入 Before=a.service 的效果相同。因此請擇一使用,並將其放在由您管理的 unit 中。停止時會自動反轉排序,因此 After=b.service 也表示您的 unit 會在 b.service 之前停止。
After= 等待「啟動完成」,而 Type= 定義其意義
After= 會等待其他單元完成啟動。何謂「完成啟動」,完全由該單元的 Type= 決定。
Type=simple:systemd fork 出程序後立即視為啟動完成。程式可能尚未解析設定,更不用說開啟 socket。Type=exec:execve()成功後立即視為啟動完成。條件稍微嚴格,但仍未表示服務已就緒。Type=forking:原始父程序結束時視為啟動完成。Type=oneshot:程序結束時視為啟動完成。此時「啟動」確實代表工作已完成。Type=notify:服務在其通知 socket 上送出READY=1時視為啟動完成。只有此類型能回報真正的就緒狀態。
因此,對 Type=simple daemon 使用 After= 是一項薄弱的保證,這也是第一個範例中競態問題的另一半。如果相依單元以 Type=simple 提供,對它設定排序並不代表它已接受連線。有兩種可靠的做法。改為排在它的 socket 單元之後,讓 kernel 在 daemon 仍在啟動時佇列傳入連線。或讓自己的服務重試,並交由 restart policy 處理。單元使用的類型會顯示在 systemctl cat 中;在依賴排序之前,值得先閱讀 Type= 設定,以及各值會告訴 systemd 的內容。
ExecStartPre 是可能使單元失敗的閘門
ExecStartPre= 會在 ExecStart= 之前執行。如果其結束狀態為非零,啟動程序會中止,單元會進入 failed。ExecStart= 永遠不會執行。許多單元在實際程式沒有任何訊息的情況下失敗,原因就在於程式根本未啟動。
以下幾點容易造成誤解:
- 它不是 shell。不支援管線、重新導向、萬用字元或
&&。第一個 token 必須是絕對路徑。需要 shell 語法時,請將該行包在/bin/sh -c '...'中。 -前置詞會讓非零結束狀態不視為失敗:ExecStartPre=-/usr/bin/optional-check。- 每個
ExecStartPre=都必須在下一個執行前結束。它不能啟動長時間執行的程序。 - 所有
ExecStartPre=行都與ExecStart=共用TimeoutStartSec=。等待資料庫的前置檢查若陷入迴圈,會耗盡啟動逾時時間;在日誌出現start-pre operation timed out. Terminating.後,單元會以Result: timeout失敗。
失敗行列出的是控制程序,而非主要程序:
inventory-api.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
inventory-api.service: Failed with result 'exit-code'.請仔細讀取該符號名稱。systemd 會透過固定對照表轉換較小的結束代碼,因此無論程式原本想表達什麼,2 都一定會顯示為 INVALIDARGUMENT。只有 status=203/EXEC 能提供實際資訊:systemd 完全無法執行該二進位檔,原因可能是路徑錯誤或檔案沒有執行權限。
請勿使用 ExecStartPre= 建立目錄。RuntimeDirectory=、StateDirectory=、LogsDirectory= 和 CacheDirectory= 會以正確的擁有者與模式建立目錄,而 RuntimeDirectory= 會在服務停止時清理。它們在 DynamicUser= 下也能正確運作;手寫的 mkdir 則無法做到這點。
條件不成立時會靜默跳過。Assert 不成立時會明確失敗。
Condition 與 Assert 系列會執行相同的測試。兩者的差異只在於測試失敗時的處理方式。
Condition...= 失敗時會跳過該 unit。啟動工作會回報為成功。該 unit 會維持在 inactive (dead) 狀態,不會標記為失敗、不會觸發警示,而 journal 只會記錄一行訊息。訊息內容取決於 systemd 版本。例如,在 systemd 255 上,訊息會指出失敗的測試:
Inventory API was skipped because of an unmet condition check (ConditionPathExists=/etc/inventory/api.conf).systemd 260 及更新版本會將訊息縮短為 Inventory API skipped, unmet condition check ConditionPathExists=/etc/inventory/api.conf,systemd 249 及更舊版本則記錄 Condition check resulted in Inventory API being skipped.。在該 unit 的 journal 中搜尋 skipped,即可找到所有版本的訊息。
systemctl status 會直接顯示原因:
Active: inactive (dead)
Condition: start condition unmet at Thu 2026-08-20 09:14:02 UTC; 2min ago其下方的縮排行會指出失敗的確切 directive,例如 ConditionPathExists=/etc/inventory/api.conf was not met。systemd 252 及更舊版本會在標頭中顯示 start condition failed,而不是 start condition unmet。
Assert...= 失敗時,啟動工作也會失敗。systemctl start 會以非零狀態結束,並顯示 Assertion failed on job for inventory-api.service.;journal 會記錄 Assertion failed for Inventory API.,而 systemctl status 會顯示 Assert: start assertion failed 行。該 unit 本身仍不會進入 failed 狀態,因此 systemctl --failed 及任何只監控失敗 unit 的監控工具都不會列出它。錯誤訊息會傳給要求啟動該 unit 的使用者或程式。
請根據測試失敗的意義,在兩者之間做選擇。Condition 表示「此 unit 不適用於這台機器」。Assert 表示「這項條件必須成立;如果不成立,請通知相關人員」。大多數 unit 應使用 Condition。只有在靜默不執行比啟動工作失敗更糟時,才使用 Assert。
Condition 系列有兩個常見陷阱。
第一,條件失敗不會使相依於該 unit 的 unit 失敗。如果 a.service 設定了 Requires=b.service,而 b.service 因條件不成立而跳過,b.service 的啟動工作仍會被視為完成。因此 a.service 仍會正常啟動,但此時 b 並未執行。條件只會保護寫入該條件的 unit。
第二,條件會在 unit 每次啟動時、執行該工作當下重新評估。由 systemd timer 在 VPS 上觸發的 unit 可能連續跳過 100 次,卻從未顯示失敗。這與執行但不做任何事的 cron 工作屬於相同類型的靜默無操作。排查方式也相同:讀取該 unit 的 journal,不要只相信它的結束狀態。
伺服器上值得了解的條件如下:
ConditionPathExists=/etc/inventory/api.conf及其否定形式ConditionPathExists=!/etc/inventory/api.conf。ConditionFileNotEmpty=與ConditionDirectoryNotEmpty=,適用於套件建立但留為空的設定檔或資料目錄。ConditionVirtualization=,讓需要實際 kernel 介面的 unit 能夠設定ConditionVirtualization=!container。請使用systemd-detect-virt檢查這台主機回報的內容。ConditionHost=會比對主機名稱或 machine ID,讓同一份 unit 檔案在兩台伺服器上採用不同的行為。ConditionKernelCommandLine=與ConditionKernelVersion=,適用於繫結至開機參數或最低 kernel 版本的 unit。
空白指派會清除清單。因此,drop-in 可以移除套件提供的條件:
[Unit]
ConditionPathExists=
ConditionPathExists=/srv/inventory/api.confnetwork.target 不代表網路已就緒
network.target 是同步點,不是狀態。開機時,將服務排序在它之後,表示網路管理軟體已啟動。但這不代表介面已取得位址,也不代表已存在通往網際網路的路由。此 target 主要是為了反向情境而存在:排序在 After=network.target 之後的 unit,會在關機拆除網路前先停止。
network-online.target 才是負責等待的 target。它由所使用網路管理程式提供的 wait-online 服務支援:
systemd-networkd-wait-online.service:由 systemd-networkd 管理連結時使用。這是透過 netplan 設定的 Ubuntu 伺服器常見配置。NetworkManager-wait-online.service:由 NetworkManager 管理時使用。
較舊的 ifupdown 配置則使用 networking.service,效果相同。無論使用哪一個 target,正確配置都需要兩行,而不是一行。
[Unit]
Wants=network-online.target
After=network-online.targetnetwork-online.target 不在預設的開機 transaction 中,也不會自動被拉入。若只寫 After=,你排序所依據的是一個從未排入佇列的 unit,因此排序完全不會產生作用。這就是前文所述的 no-op,只是代價更高。Wants= 這一行會將 target 加入 transaction,讓 After= 有實際等待的對象。
第二點是,「online」由 wait-online 實作定義,不是由 systemd 定義。systemd-networkd-wait-online 會在它管理的連結達到已設定狀態後返回。它不會檢查 DNS 是否能解析,也不會檢查任何遠端主機是否可連線。
這項定義會造成常見的 VPS 問題。若主機有一個供私有網路使用的第二介面,該介面已在 netplan 宣告卻從未取得位址,wait 服務就會持續等待,直到逾時:
systemd-networkd-wait-online[612]: Timeout occurred while waiting for network connectivity.
systemd-networkd-wait-online.service: Failed with result 'exit-code'.由於預設逾時為 120 秒,開機會多花 2 分鐘。可採用兩種修正方式。第一,在 netplan 檔案中將未使用的介面標記為 optional: true,讓 networkd 不再等待該介面。第二,為 wait 服務加入 drop-in,使用 --interface= 指定要等待的連結,或傳入 --any,讓服務在任一連結上線後立即返回。
更好的作法是避免依賴這個 target。許多服務之所以排序在 network-online.target 之後,只是因為它們會繫結特定位址,並在開機時因以下訊息而失敗:
nginx: [emerg] bind() to 203.0.113.10:443 failed (99: Cannot assign requested address)核心拒絕繫結,因為該位址尚未上線。設定 net.ipv4.ip_nonlocal_bind=1 可讓程序繫結至主機目前尚未持有的位址,重啟原則則可處理後續狀況。為了通常只涉及一個 socket 的問題,延後整個開機流程直到網路就緒,是過於笨重的作法。
執行中系統如何讀取實際的 systemd 相依性
不要只根據 unit 檔案判斷。Drop-in、.wants/ 符號連結,以及隱含的預設相依性都會加入檔案中未顯示的關聯。
systemctl cat inventory-api.service此命令會依套用順序列出 unit 檔案與所有 drop-in,並在每個區塊上方顯示來源路徑。請先執行此命令。/etc/systemd/system/inventory-api.service.d/ 中的 5 行覆寫內容會優先於套件提供的檔案,否則不易發現。
systemctl show inventory-api.service -p Requires -p Wants -p After -p Before -p ConditionResult -p AssertResult此命令會列出套用 drop-in 並由 systemd 加入隱含相依性後的解析值。ConditionResult=no 直接回答了「unit 回報成功卻沒有執行任何動作」這個問題。
systemctl list-dependencies inventory-api.service
systemctl list-dependencies --reverse inventory-api.service
systemctl list-dependencies --after inventory-api.service
systemctl list-dependencies --before inventory-api.service不含其他選項的形式會向下展開 Requires= 與 Wants=。--reverse 顯示哪些 unit 拉入目前的 unit,可用來找出開機時啟動它的 target。--after 與 --before 顯示順序;若要確認是否真的有 unit 等待,應查看這兩項。
journalctl -b -u inventory-api.service --no-pager
journalctl -b -o short-precise -u inventory-api.service -u postgresql.service第二個命令會以毫秒時間戳交錯顯示 2 個 unit 的資訊。這能證明順序競態,而不是靠猜測判斷。pg_isready 的失敗發生在 PostgreSQL 記錄 database system is ready to accept connections 之前,兩者之間的時間差會直接顯示在輸出中。
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemd-analyze critical-chain inventory-api.serviceverify 會以 systemd 載入 unit 的方式載入它,並回報未知指令、對不存在 unit 的相依性、順序循環,以及無法解析的語法。此命令不會變更系統。critical-chain 會列出延遲該 unit 的順序鏈,以及每個步驟變為作用中狀態的時間;這只適用於本次開機期間已啟動的 unit。
編輯任何 unit 檔案後,請執行 sudo systemctl daemon-reload。若要變更套件提供的 unit,請使用 sudo systemctl edit inventory-api.service,它會代為建立 drop-in。直接編輯 /usr/lib/systemd/system/ 下的供應商檔案,只會在下一次套件升級取代該檔案前有效。相同的 drop-in 機制也能讓您為服務設定記憶體與 CPU 限制,而不必修改套件所管理的檔案。
Ordering 迴圈及其在 journal 中留下的訊息
在兩個方向都加入排序相依性時,systemd 會從 transaction 中刪除其中一個 job,嘗試解除迴圈:
systemd[1]: Found ordering cycle on inventory-api.service/start
systemd[1]: Job postgresql.service/start deleted to break ordering cycle starting with inventory-api.service/startsystemd 只能刪除啟動要求不需要的 job。當迴圈中的每個 job 都是必要項目時,例如各 unit 也互相指定 Requires=,systemd 便會放棄處理。journal 會記錄 Unable to break cycle starting with inventory-api.service/start,迴圈中的任何項目都不會啟動,而 systemctl start 會以 Transaction order is cyclic. See system logs for details. 失敗。
systemd 刪除 job 時會自行決定刪除哪一個,未必符合你的預期。結果可能表現為某個服務在部分重新開機後消失,其他重新開機後又恢復,從外部很難除錯。大多數迴圈來自已設定 DefaultDependencies=no 的 unit,之後卻仍針對 basic.target 設定排序;或是對已存在 After=、且反向指向你的 unit,再加入 Before=。systemd-analyze verify 可在不重新開機的情況下找出這些問題。
固定的 unit
[Unit]
Description=Inventory API
Wants=postgresql.service network-online.target
After=postgresql.service network-online.target
ConditionPathExists=/etc/inventory/api.conf
[Service]
Type=notify
StateDirectory=inventory
ExecStart=/usr/local/bin/inventory-api
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target每一行只負責一項工作。Wants= 會將兩個相依項目納入交易,但不會讓本 unit 的生命週期受它們影響。After= 負責等待,而且必須再次列出這兩個名稱,因為相依性與啟動順序是分開設定的。ConditionPathExists= 表示機器雖然已安裝套件,但缺少設定時,會安靜地略過此 unit,不會發出警示。對於由設定驅動的服務,這是正確行為。Type=notify 表示排在此 unit 之後的項目,會等待服務真正就緒,而不是只等待程序 fork。Restart=on-failure 處理開機很久之後資料庫才離線的情況,因為啟動順序只適用於第一次啟動。重試應該多積極,取決於 Restart= 與 RestartSec= 設定。
在信任它之前先進行檢查:
sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemctl list-dependencies --after inventory-api.service
sudo systemctl start inventory-api.service
systemctl show inventory-api.service -p ConditionResult -p ActiveState -p Result健康的 unit 會以 ConditionResult=yes 搭配 ActiveState=active 讀取,且 Result=success 會確認上次執行未失敗。ConditionResult=no 搭配 ActiveState=inactive 表示該 unit 被略過;journal 中列出條件的那一行,會告訴你哪項測試失敗。
FAQ
Requires= 會等待另一個 unit 啟動嗎?
不會。Requires= 與 After= 是分開的設定。Requires= 會將另一個 unit 納入相同的 transaction,接著 systemd 會平行啟動兩個工作。若要等待,請加入 After=,並指定相同的 unit。加入這項設定還有第二個原因:只有在同時設定 After= 時,失敗的 Requires= 相依性才會阻止本 unit 啟動;若未設定啟動順序,本 unit 早已在另一個 unit 失敗前啟動。
應該排在 network.target 還是 network-online.target 之後?
在開機期間,network.target 只表示網路管理軟體已啟動,因此不保證位址或路由已就緒。如果服務在啟動時需要可用的位址,請使用 network-online.target,並同時撰寫 Wants=network-online.target 與 After=network-online.target,因為該 target 不在預設的開機 transaction 中,單獨使用 After= 只會等待一個尚未排入佇列的 unit。如果服務只因繫結到某個特定 IP 而失敗,使用 net.ipv4.ip_nonlocal_bind=1 搭配 Restart=on-failure 會比延遲開機更輕量。
為什麼我的 unit 回報成功,卻從未執行?
Condition...= 測試失敗時,systemd 會略過該 unit,並將啟動工作回報為成功,因此不會標記任何失敗。請執行 systemctl show <unit> -p ConditionResult,再由 ConditionResult=no 加以確認。接著查看 journalctl -b -u <unit>,找出包含 skipped 的那一行;在目前的版本中,該行也會指出失敗的條件。systemctl status <unit> 也會指出未符合的確切指令。
Condition 與 Assert 有何差異?
兩者執行完全相同的測試。Condition 失敗時,systemd 會安靜地略過該 unit,工作仍會成功。Assert 失敗時,啟動工作會失敗:systemctl start 會回傳錯誤,且 journal 會記錄 Assertion failed for <description>.。兩者都不會讓 unit 進入 failed 狀態,因此都不會出現在 systemctl --failed 中。對於「本 unit 不適用於這台機器」的情況,請使用 Condition,這涵蓋幾乎所有實際案例。只有在啟動該 unit 的人員或元件必須得知前置條件缺失時,才使用 Assert。
為什麼 ExecStartPre 會以 status=203/EXEC 失敗?
203/EXEC 表示 systemd 完全無法執行該命令。常見原因包括路徑不是絕對路徑、該機器上不存在指定的 binary、檔案沒有 execute 權限,或 script 的 #! 行指向不存在的 interpreter。systemd 的其他小型代碼來自固定的對照表,因此 status=2/INVALIDARGUMENT 只表示命令以 2 結束,無法說明引數的問題。請記住,ExecStartPre= 不會經由 shell 執行,因此管線與萬用字元需要使用 /bin/sh -c '...'。