SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-10-17

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-api

Requires=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.conf

network.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.target

network-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.service

verify 會以 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/start

systemd 只能刪除啟動要求不需要的 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 '...'。

#systemd#units#dependencies#ordering#troubleshooting