systemd unit 無法啟動?先看 exit code
先執行 systemctl status,查看 status=203/EXEC 與 226/NAMESPACE 的差異,並了解 unit 為何可能正常啟動後,僅一秒就結束。
systemd unit 無法啟動的原因
無法啟動的 systemd unit 會在一個欄位中說明原因。執行 systemctl status <unit>,並在回報失敗的那一行尋找 code= 與 status=。200 多的狀態碼表示 systemd 尚未執行到您的程式:它在建立 unit file 要求的執行環境時就已失敗。低於 200 的狀態碼表示程式確實已執行,並自行結束,因此 unit file 可能沒有問題,問題通常出在應用程式。
這個區分就是判斷路徑。以下內容會依照狀態碼出現的順序說明後續處理方式。
依序回答問題的 3 個命令
systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.servicesystemctl status 提供判定結果。先讀取 Loaded: 行,因為其中會指出 systemd 實際解析的檔案,並說明該 unit 是否已啟用、被遮罩,或根本找不到。接著讀取 Active: 行,以及下方的 code= 與 status= 配對結果。
journalctl -u myapp.service -b --no-pager 提供詳細資訊。-u 將結果限制為該 unit,-b 將輸出限制在目前的開機工作階段,避免讀到上週的失敗紀錄,而 --no-pager 會直接輸出至終端機,讓你可以將結果透過 pipe 傳給 grep。status 只顯示最後幾行日誌,並截短過長的行。journal 會顯示程式在終止前輸出的所有內容,通常其中就有真正的錯誤原因。加入 -n 100 可取得更多歷史紀錄,或在第二個終端機中搭配 -f 執行,再重新啟動該 unit。
systemd-analyze verify 會載入 unit file,但不會執行它。此命令會警告未知的 section 和 directive,也會標示 ExecStart= 中無法執行的命令。這能捕捉 2 類不易察覺的錯誤:拼錯的 key,以及不存在的路徑。systemd 載入時會忽略拼錯的 key,並發出大多數人不會查看的警告。
編輯任何 unit file 後,請執行 sudo systemctl daemon-reload。在執行前,systemd 會繼續使用先前載入的副本,而 systemctl status 會額外警告磁碟上的檔案已變更。修正「沒有作用」時,常見原因是 systemd 尚未重新讀取該修正。
另外還有 2 個值得使用的命令。systemctl cat myapp.service 會輸出有效的 unit,也就是主要檔案以及 /etc/systemd/system/myapp.service.d/ 下的所有 drop-in。systemctl show myapp.service -p ExecStart -p User -p WorkingDirectory 會以 systemd 解析後的形式輸出這些值,這正是實際執行時會採用的設定。
status=203/EXEC 代表什麼?
203/EXEC 表示 systemd 已完成設定、呼叫了 execve(),但核心拒絕執行。您的程式完全沒有執行任何一行自己的程式碼。幾乎所有情況都可歸納為以下 4 個原因。
ExecStart=中的路徑錯誤,或不是絕對路徑。使用ls -l檢查,並與 unit 檔案中的完整字串比對。- 檔案沒有執行位元。
sudo chmod +x /opt/myapp/run.sh可修正此問題。從封存檔解開或從其他機器複製的檔案,常會遺失這個位元。 - shebang 行損壞。核心會讀取指令碼的第一行,並執行其中指定的直譯器。因此,當服務的 PATH 沒有
python3時,#!/usr/bin/env python3會失敗;如果檔案使用 Windows 換行格式儲存,核心會尋找名為/bin/bash\r的直譯器,但該檔案不存在。 - 檔案不是這台機器可執行的格式,例如架構錯誤,或是完全沒有 shebang 的文字檔案。
在變更任何設定前,先以服務帳號手動重現問題。
sudo -u appuser /opt/myapp/run.sh
file /opt/myapp/run.sh
head -1 /opt/myapp/run.sh | cat -Afile 會顯示架構;如果換行格式造成問題,也會顯示「with CRLF line terminators」。cat -A 會以結尾的 ^M 顯示相同問題。使用 sed -i 's/\r$//' /opt/myapp/run.sh 移除這些字元。
關於這個範圍,有一點必須說明:200 以上的狀態碼只是慣例,不是保證。您自己的程式也可以以 203 結束,而 systemd 無法區分這兩種情況。systemd-analyze exit-status 203 會顯示任何程式碼的名稱與類別,有助於閱讀這份表格;但如果您的應用程式使用 199 以上的結束碼,請改用其他數值。
為什麼會出現 217/USER 或 216/GROUP?
217/USER 表示服務啟動時,User= 中指定的帳號不存在。216/GROUP 則表示 Group= 或 SupplementaryGroups= 發生相同問題。每項各執行一個指令即可確認。
getent passwd appuser
getent group appgroup每個指令都會輸出一行,或不輸出內容並以非零狀態碼結束。沒有輸出表示系統找不到該名稱,因此 systemd 無法切換至該帳號,並會在執行 exec 前停止。修正方式是建立該帳號,而不是設定 User=root。讓服務以 具最低權限的專用系統帳號執行,正是這項指令的用途。
sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuserDynamicUser=yes 會讓 systemd 在每次啟動時配置暫時帳號,藉此避開這個問題。這適合不保存狀態的服務。任何會寫入檔案的服務,都必須同時設定 StateDirectory=,因為每次啟動時使用者 ID 都會變更;若使用一般路徑,檔案最後會由已不存在的帳號擁有。
什麼是 226/NAMESPACE?
226/NAMESPACE 來自沙箱化指令。當 unit 設定 ProtectSystem=、ProtectHome=、PrivateTmp=、ReadWritePaths= 或類似設定時,systemd 會在執行程式前,為該服務建立私有掛載 namespace。這裡的 namespace 是單一程序所看到的檔案系統私有視圖。如果該配置中的任何掛載失敗,啟動就會以 226 失敗,程式也不會執行。
最常見的原因是 ReadWritePaths= 中指定的路徑不存在。ProtectSystem=strict 會以唯讀方式掛載整個檔案系統,而 ReadWritePaths= 會重新開放指定路徑供寫入。systemd 無法重新開放不存在的目錄。有兩種適當的修正方式。使用 StateDirectory= 讓 systemd 建立目錄;這會在每次啟動時建立 /var/lib/<name>,並將目錄交給服務使用者。或者在路徑前加上 -,告知 systemd 在來源不存在時忽略該項目。不要刪除強化設定,因為這只是把 5 分鐘的問題換成永久性的問題。
[Service]
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=-/srv/uploads如果無法判斷是哪一行造成問題,請先移除整個強化設定區塊,重新載入設定,然後啟動服務。如果服務成功啟動,請逐行加回設定,每加一行就重新啟動一次。此類錯誤中另外兩個相近的錯誤是 233/RUNTIME_DIRECTORY 和 238/STATE_DIRECTORY。這表示 systemd 無法建立 RuntimeDirectory= 或 StateDirectory= 中指定的目錄,或無法取得該目錄的所有權。通常是因為該路徑已存在,且歸屬於其他使用者。
為什麼 WorkingDirectory 看似正確,卻出現 200/CHDIR?
200/CHDIR 表示進入 WorkingDirectory= 的 chdir() 失敗。目錄可能不存在,或服務使用者無法進入該目錄。進入目錄需要該目錄及其所有上層父目錄具備 execute 權限。因此,即使 /home/deploy/app 可讀,只要 /home/deploy 的模式為 700,而服務以 appuser 執行,仍然無法存取該目錄。
sudo -u appuser test -x /srv/myapp && echo ok
namei -l /srv/myappnamei -l 會列出路徑中每個元件的擁有者與模式。這是找出哪個目錄阻擋後續存取的最快方法。將 WorkingDirectory=-/srv/myapp 設為可讓缺少目錄不致造成致命錯誤。這適合不在意從哪個目錄開始執行的程式;若程式會依相對路徑開啟檔案,則不適合這樣設定。
服務為什麼啟動後,1 秒後就停止?
這裡沒有任何 200 系列狀態碼,通常也完全沒有錯誤文字。單元在啟動後立即顯示 inactive (dead),或持續循環顯示 activating (auto-restart)。systemd 已正確建立執行環境。問題在於程式的實際行為,與 Type= 所宣告的行為不一致。
Type=simple 是預設類型,表示程式會留在前景執行。如果讓它啟動一個 fork 到背景執行後便結束的 daemon,systemd 會判定主程序已結束,並將服務視為完成。大多數 daemon 都提供保持前景執行的旗標,例如 nginx -g 'daemon off;'。
Type=forking 表示第一個程序會在子程序就緒後結束。若提供前景程式,啟動工作會持續等待,直到 TimeoutStartSec= 用盡;預設為 90 秒。之後 systemd 會終止該程式,並記錄逾時。
Type=notify 表示程式會呼叫 sd_notify() 宣告已就緒。不支援這項功能的程式不會發出任何就緒通知,因此啟動會逾時,journal 會將結果記錄為通訊協定失敗。
請根據程式的實際行為選擇類型。simple、forking、oneshot 與 notify 的差異 是解決這整類故障的關鍵判斷。
服務反覆結束時,systemd 會停止重試,並回報啟動要求重複過於頻繁。此時單元會維持 failed 狀態,直到速率限制的時間窗結束,或執行 sudo systemctl reset-failed myapp.service。提高限制只會掩蓋症狀。請從第一次失敗開始查看 journal,而不是只看最後一次,並在修改前參閱 Restart=on-failure 實際會重試什麼。
為什麼 unit 會在完全沒有錯誤的情況下保持 inactive?
unit 可能會被略過,而不是啟動。Condition* 指令的設計就是不顯示錯誤:檢查失敗時,systemd 會將該工作標記為成功,然後不執行任何動作。含有 ConditionPathExists=/etc/myapp/config.yml 的 unit 在該檔案不存在時永遠不會啟動,也不會回報錯誤。
systemctl show myapp.service -p ConditionResult -p ConditionTimestamp
journalctl -u myapp.service -b --no-pager | grep -i conditionConditionResult=no 會確認該 unit 已被略過,而 journal 會指出未通過的檢查。當缺少必要條件時應明確失敗,請改用 Assert* 指令。Conditions、assert 與 unit 排序說明不同檢查應放在哪個位置。
附近還有幾種不會顯示錯誤的情況。「could not be found」錯誤通常表示檔案位於錯誤的目錄,或尚未重新載入:自行建立的 unit 檔案應放在 /etc/systemd/system/。被 mask 的 unit 在 sudo systemctl unmask myapp.service 清除 mask 前會拒絕所有啟動要求。此外,沒有 [Install] 區段的 unit 執行 systemctl enable 會失敗,因此請為它加入 WantedBy=multi-user.target。
如果程序是被終止,而不是啟動失敗,該怎麼辦?
code=killed 與 code=exited 的情況不同。程序是由外部因素終止的。status=9/KILL 通常指向 out of memory (OOM) killer,journal 會記錄它選定的程序。你自行設定的限制也會在 cgroup (control group) 內執行相同的終止動作,因此請使用 free -m 檢查主機的可用記憶體,並檢查 unit 是否設定了 MemoryMax=。MemoryMax、CPUQuota 與其他 cgroup 限制說明哪些限制會終止程序,哪些限制只會降低程序的執行速度。
如果在嘗試啟動後立即出現 status=15/TERM,通常表示 systemd 等待啟動逾時並終止了程序,因此請回到 Type=。
避免大多數這類故障的 2 個習慣
所有地方都使用絕對路徑。 systemd 不會執行您的登入 shell,因此沒有 .bashrc、沒有 .profile,也沒有已啟用的虛擬環境。系統服務的 $PATH 只包含一份簡短的內建清單,不會包含 /opt 或語言版本管理器的 shim。請完整寫出 /usr/bin/python3 或 /opt/myapp/venv/bin/python。在 shell 中執行 command -v myapp,即可列出要貼上的路徑。同樣的規則也適用於 WorkingDirectory=、EnvironmentFile=,以及 ReadWritePaths= 中的所有路徑。
ExecStart= 不是 shell。 systemd 會將該行拆成多個字詞,然後直接呼叫 execve()。管線、重新導向、萬用字元、&&、反引號與 ~ 都沒有特殊意義,會以字面值引數傳給您的程式。ExecStart=/usr/bin/myapp --flag > /tmp/out.log 會將 > 和 /tmp/out.log 傳給 myapp,接著該程式會因使用方式錯誤而結束,顯示的訊息看起來完全不像 systemd 問題。需要使用 shell 功能時,請明確呼叫 shell。
ExecStart=/bin/sh -c '/usr/bin/myapp --flag | /usr/bin/tee -a /var/log/myapp.log'如果只需要輸出,則不需要這樣做。服務輸出預設會寫入 journal,而 StandardOutput=append:/var/log/myapp.log 不需要透過 shell 即可將輸出寫入檔案。
變數展開也受到相同限制。$MYVAR 和 ${MYVAR} 會從 Environment= 和 EnvironmentFile= 取代,除此之外不會展開任何內容。除非您加以設定,否則系統服務不會設定 $HOME。EnvironmentFile= 也不是 shell script:其中不能使用 export,其引號規則與 bash 不同;如果檔案不存在,除非在路徑前加上 -,否則會視為致命錯誤。
在正式運作中的伺服器上逐步處理
讀取程式碼,確認根因,修改一項設定,然後重新啟動。這個順序比記住所有編號更重要,因為可以避免同時進行 3 項推測性的修改,也不會失去對哪一項修改有效的追蹤。這套方法同樣適用於不是由你撰寫的 unit。永遠不會觸發的 timer,代表對應的 service 從未啟動,因此應先偵錯 service:systemd timer 與其觸發的 service 會以完全相同的方式失敗,而 timer 會隱藏輸出,直到你要求 journal 顯示相關內容。
FAQ
systemctl status 中的 status=203/EXEC 代表什麼?
systemd 已完成 unit 要求的設定,但 execve() 呼叫失敗,因此程式從未啟動。請依序檢查 4 項:ExecStart= 中的路徑是否存在且為絕對路徑、檔案是否具備執行位元、shebang 指定的直譯器是否存在於服務的 PATH,以及檔案是否使用 Unix 換行格式。最後一項會由 file 回報「with CRLF line terminators」,使直譯器名稱變成 /bin/bash\r,導致 kernel 拒絕執行。
為什麼我的服務啟動後立即停止?
unit 檔案宣告的行為與程式實際支援的行為不符。使用 Type=simple 時,systemd 預期程式持續在前景執行,因此會將 fork 到背景的 daemon 視為已完成。使用 Type=forking 時,systemd 會等待第一個程序結束,因此前景程式會讓啟動工作持續等待,直到 TimeoutStartSec= 逾時。請讓 Type= 符合程式的執行方式;如果程式提供前景執行旗標,請搭配預設的 Type=simple 使用該旗標。
如何查看完整錯誤,而不是簡短的狀態輸出?
systemctl status 只會顯示 journal 的最後幾行,並截斷過長的內容。執行 journalctl -u myapp.service -b --no-pager 以取得此開機期間 unit 記錄的所有內容;加入 -n 200 可擴大顯示範圍,或將輸出傳送至 grep。如果應用程式會寫入自己的 log 檔案,也請一併查看,因為 systemd 只會擷取程式傳送至標準輸出與標準錯誤的內容。
為什麼我的 unit 處於 inactive,卻沒有錯誤訊息?
最常見的原因是某個 Condition* 指令略過了它。這些檢查不會顯示錯誤:條件失敗時,啟動工作仍會標記為成功。執行 systemctl show myapp.service -p ConditionResult 並尋找 ConditionResult=no,再查看指出該檢查的 journal 記錄。另一個常見原因是 unit 已被 mask;在 sudo systemctl unmask 清除之前,它會拒絕所有啟動要求。
每次修改 unit 檔案後都需要執行 daemon-reload 嗎?
需要。只要修改 unit 檔案或 drop-in,就必須執行。sudo systemctl daemon-reload 會讓 systemd 從磁碟重新讀取檔案,接著 sudo systemctl restart myapp.service 將變更套用至執行中的服務。執行 systemctl edit 後不需要另外執行,因為該命令會自動重新載入;如果修改的是應用程式所屬的設定檔,而不是 systemd 的設定檔,也不需要執行。