SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

systemd unitの終了コードを読む方法

systemctl statusを最初に確認し、203/EXECと226/NAMESPACEの意味を解説します。正常起動後、1秒で終了するunitの原因も確認できます。

systemd unit が起動しない理由

起動しない systemd unit では、1 つのフィールドに理由が示されます。systemctl status <unit> を実行し、失敗を報告している行の code=status= を確認します。200 番台の status 番号は、systemd がプログラムに到達していないことを示します。unit file で指定された環境の構築中に失敗しています。200 未満の status は、プログラム自体は実行されて終了したことを示します。この場合、unit file は正しく、問題はアプリケーション側にある可能性が高いです。

この区分が判断の起点になります。以下では、番号が現れる順に説明します。

質問に順番どおり答える3つのコマンド

systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
systemd-analyze verify /etc/systemd/system/myapp.service

systemctl statusで判定します。まずLoaded:行を読みます。この行には、systemd が実際に解析したファイル名と、unit が有効、mask 済み、または未検出のいずれかが示されます。次にActive:行を読み、その下にあるcode=status=の組み合わせを確認します。

journalctl -u myapp.service -b --no-pagerで詳細を確認します。-uは対象の unit だけに絞り、-bは今回のブート時の出力だけに限定します。これにより、先週の障害を読まずに済みます。--no-pagerは端末へ直接出力するため、grepへパイプできます。statusは最後の数行だけを表示し、長い行を短縮します。journal には、プログラムが終了する前に出力した内容がすべて記録されます。通常、そこに実際のエラーがあります。さらに履歴を確認するには-n 100を追加します。unit を再起動しながら別の端末で監視する場合は、-fを付けて実行します。

systemd-analyze verifyは、unit を実行せずに unit ファイルを読み込みます。認識できないセクションやディレクティブを警告し、ExecStart=内で実行できないコマンドも指摘します。これにより、見落としやすい2種類のミスを検出できます。1つはスペルミスです。systemd は読み込み時に警告を出して無視しますが、その警告は多くの場合確認されません。もう1つは、存在しないパスです。

unit ファイルを編集した後は、必ず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() を呼び出したものの、kernel に拒否されたことを示します。プログラムは自身のコードを1行も実行していません。原因のほとんどは、次の4つに分類できます。

  1. ExecStart= のパスが誤っているか、絶対パスではありません。unit ファイル内の正確な文字列と ls -l の結果を照合してください。
  2. ファイルに execute ビットがありません。sudo chmod +x /opt/myapp/run.sh で修正できます。アーカイブから展開したファイルや別のマシンからコピーしたファイルでは、このビットが失われていることがあります。
  3. shebang 行が壊れています。kernel はスクリプトの1行目を読み、そこに指定された interpreter を実行します。そのため、サービスの PATH に python3 がないと #!/usr/bin/env python3 は失敗します。また、Windows の改行コードで保存されたファイルは /bin/bash\r という名前の interpreter を要求しますが、その interpreter は存在しません。
  4. ファイルがこのマシンで実行できる形式ではありません。architecture が違うか、shebang のないテキストファイルです。

変更を加える前に、サービスユーザーとして手動で実行し、同じ状態を再現してください。

sudo -u appuser /opt/myapp/run.sh
file /opt/myapp/run.sh
head -1 /opt/myapp/run.sh | cat -A

file は architecture を示し、改行コードが問題の場合は「CRLF line terminators」と報告します。cat -A でも、末尾に ^M があることを確認できます。sed -i 's/\r$//' /opt/myapp/run.sh で削除してください。

終了コードの範囲については、1つ注意が必要です。200以上を systemd 用とするのは慣例であり、保証ではありません。プログラム自身が203で終了することもでき、systemd は両者を区別できません。systemd-analyze exit-status 203 は任意のコードの名前とクラスを表示するため、表の読み取りに役立ちます。ただし、アプリケーションが199を超える終了コードを使用する場合は、コードを変更してください。

217/USER または 216/GROUP になるのはなぜですか?

217/USER は、サービスの起動時点で User= に指定されたアカウントが存在しないことを示します。216/GROUP は、Group= または SupplementaryGroups= に指定されたアカウントで同じ問題が発生したことを示します。それぞれ、1 つのコマンドで確認できます。

getent passwd appuser
getent group appgroup

どちらも、該当する行を出力するか、何も出力せずに非ゼロの終了ステータスを返します。何も出力されない場合、その名前はシステムに存在しません。そのため systemd はそのアカウントに切り替えられず、exec の前に停止します。修正方法は、User=root を設定することではなく、アカウントを作成することです。1 つのサービスを 最小権限の専用システムアカウントで実行することが、このディレクティブの目的です。

sudo useradd --system --no-create-home --shell /usr/sbin/nologin appuser

DynamicUser=yes を使用すると、systemd が起動ごとに一時アカウントを割り当てるため、この問題を回避できます。状態を保持しないサービスに適しています。ファイルを書き込むサービスでは、これと併せて StateDirectory= も必要です。起動ごとにユーザー ID が変わるため、通常のパスに置いたファイルは、存在しなくなったアカウントの所有物になるからです。

226/NAMESPACE とは何ですか?

226/NAMESPACE は、サンドボックス化のディレクティブに起因します。unit で ProtectSystem=ProtectHome=PrivateTmp=ReadWritePaths= などを設定すると、systemd はプログラムを exec する前に、そのサービス用のプライベートな mount namespace を作成します。ここでいう namespace は、1 つのプロセスから見えるファイルシステムの専用ビューです。その構成に含まれる mount のいずれかが失敗すると、起動は 226 で失敗し、プログラムは実行されません。

通常の原因は、ReadWritePaths= 内のパスが存在しないことです。ProtectSystem=strict はファイルシステム全体を read only で mount し、ReadWritePaths= は指定したパスを再び write 用に開きます。systemd は存在しないディレクトリを再び開くことができません。適切な修正方法は 2 つあります。StateDirectory= で systemd にディレクトリを作成させます。これにより、systemd は起動のたびに /var/lib/<name> を作成し、サービスユーザーに割り当てます。または、パスの先頭に - を付けます。これにより、ソースが存在しない場合に systemd はそのエントリを無視します。ハードニングを削除するのは適切ではありません。5 分の問題と引き換えに、恒久的な問題を招くためです。

[Service]
ProtectSystem=strict
ProtectHome=yes
StateDirectory=myapp
ReadWritePaths=-/srv/uploads

どの行が原因か分からない場合は、ハードニングのブロック全体を削除し、reload してから start します。サービスが起動した場合は、行を 1 つずつ戻し、その都度 restart します。この系統で近いエラーには 233/RUNTIME_DIRECTORY238/STATE_DIRECTORY があります。これらは、RuntimeDirectory= または StateDirectory= に指定されたディレクトリを systemd が作成できなかったか、所有権を設定できなかったことを示します。通常は、そのパスがすでに存在し、別のユーザーに属していることが原因です。

WorkingDirectory が正しいように見えるのに、なぜ 200/CHDIR と表示されるのですか?

200/CHDIR は、WorkingDirectory= への chdir() に失敗したことを示します。ディレクトリが存在しないか、サービスユーザーがそのディレクトリに入れません。ディレクトリに入るには、そのディレクトリと、上位にあるすべての親ディレクトリに対する実行権限が必要です。そのため、/home/deploy/app が完全に読み取り可能でも、/home/deploy のモードが 700 で、サービスが appuser として実行されている場合は到達できません。

sudo -u appuser test -x /srv/myapp && echo ok
namei -l /srv/myapp

namei -l は、パスを構成する各要素の所有者とモードを表示します。どのディレクトリが後続の要素へのアクセスを妨げているかを、最も速く特定できます。WorkingDirectory=-/srv/myapp と記述すると、ディレクトリが存在しなくてもエラーになりません。開始場所を問わないプログラムには適していますが、相対パスでファイルを開くプログラムには適していません。

サービスが起動してから 1 秒後に停止するのはなぜですか?

ここでは 200 系のコードが返らず、エラーテキストも表示されないことがよくあります。unit は起動直後に inactive (dead) になったり、activating (auto-restart) を繰り返したりします。systemd は環境を正しく構築しています。問題は、プログラムの動作と Type= が示す動作の不一致です。

デフォルトの Type=simple は、プログラムがフォアグラウンドで動作し続けることを示します。バックグラウンドへ fork して終了する daemon を指定すると、systemd はメインプロセスの終了を検出し、サービスが完了したと判断します。多くの daemon には、フォアグラウンドに留まるための nginx -g 'daemon off;' のようなフラグがあります。

Type=forking は、子プロセスの準備が完了すると最初のプロセスが終了することを示します。フォアグラウンドで動作するプログラムを指定すると、start ジョブは TimeoutStartSec= が切れるまで待機します。デフォルトでは 90 秒後に systemd がプログラムを強制終了し、タイムアウトをログに記録します。

Type=notify は、プログラムが sd_notify() を呼び出して準備完了を通知することを示します。この機能に対応していないプログラムは何も通知しないため、start がタイムアウトし、journal にはプロトコルエラーとして結果が記録されます。

プログラムの実際の動作に合わせて type を選択してください。simple、forking、oneshot、notify の違いを確認すると、この種類の障害を解決する判断ができます。

サービスが何度も終了すると、systemd は試行を停止し、start リクエストが短時間に繰り返されすぎたと報告します。その後、rate limit の期間が過ぎるか sudo systemctl reset-failed myapp.service を実行するまで、unit は failed のままになります。制限値を引き上げても症状が隠れるだけです。最後の失敗ではなく、最初の失敗から 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 condition

ConditionResult=no でスキップを確認できます。journal には、成立しなかったチェックの内容が記録されます。前提条件がない場合に明示的に失敗させるには、代わりに Assert* ディレクティブを使用します。Conditions、asserts、unit の順序では、各チェックをどこに指定すべきかを説明しています。

この付近には、他にも静かに処理されるケースがあります。「could not be found」エラーは、通常、ファイルが誤ったディレクトリにあるか、reload を実行していないことを示します。作成した unit ファイルは /etc/systemd/system/ に配置します。masked unit は、sudo systemctl unmask myapp.service で解除するまで、どのような起動も拒否します。また、systemctl enable[Install] セクションがない unit では失敗します。そのため、WantedBy=multi-user.target を指定してください。

失敗ではなく強制終了された場合

code=killedcode=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 はログインシェルを実行しないため、.bashrc.profile もなく、仮想環境も有効化されていません。システムサービスの $PATH は短い組み込みリストであり、/opt や言語バージョンマネージャーの shim は含まれません。/usr/bin/python3 または /opt/myapp/venv/bin/python を完全なパスで記述します。シェルで command -v myapp を実行すると、貼り付けるパスが表示されます。同じルールは WorkingDirectory=EnvironmentFile=ReadWritePaths= 内のすべてのパスにも適用されます。

ExecStart= はシェルではありません。 systemd は行を単語に分割し、execve() 自体を呼び出します。パイプ、リダイレクト、glob、&&、バッククォート、~ には意味がなく、リテラルの引数としてプログラムに渡されます。ExecStart=/usr/bin/myapp --flag > /tmp/out.log>/tmp/out.logmyapp に渡します。その結果、myapp は使用方法エラーで終了し、systemd の問題とは思えない内容になります。シェル機能が必要な場合は、シェルを明示的に呼び出します。

ExecStart=/bin/sh -c '/usr/bin/myapp --flag | /usr/bin/tee -a /var/log/myapp.log'

出力だけが必要な場合は、その必要はありません。サービスの出力はデフォルトで journal に送られ、StandardOutput=append:/var/log/myapp.log はシェルを介さずにファイルへ書き込みます。

変数の展開にも同じ制限があります。$MYVAR${MYVAR}Environment=EnvironmentFile= から置換され、それ以外は展開されません。システムサービスでは、設定しない限り $HOME は設定されません。EnvironmentFile= もシェルスクリプトではありません。export はその中では使用できず、引用符の規則も bash とは異なります。また、- をパスの先頭に付けない限り、存在しないファイルは致命的なエラーになります。

稼働中のサーバーで切り分ける

コードを読み、原因を実証し、1 つだけ変更して、再起動します。この順序は、すべての数値を把握していることより重要です。推測に基づく変更を 3 つ重ねて、どの変更が効果をもたらしたのか分からなくなる事態を防げるためです。自分で作成していない unit にも、同じ手順を適用できます。動作しない timer は、起動していない service です。まず service をデバッグしてください。systemd timer と、それが起動する service は、上記とまったく同じ原因で失敗します。timer が出力を隠すため、journal に出力を要求するまで内容を確認できません。

FAQ

systemctl status で status=203/EXEC と表示されるのはなぜですか?

systemd は unit が要求した設定をすべて適用しましたが、execve() の呼び出しに失敗したため、プログラムは起動していません。次の4点を順番に確認します。ExecStart= に指定されたパスが存在し、絶対パスであること、ファイルに実行ビットが付いていること、shebang が service の PATH 上に存在するインタープリターを指定していること、ファイルが Unix の改行コードを使用していることです。最後の問題がある場合、file は "with CRLF line terminators" と報告します。この場合、インタープリター名が /bin/bash\r に変わり、kernel が実行を拒否します。

サービスが起動した直後に停止するのはなぜですか?

unit file が、プログラムにはない動作を前提にしています。Type=simple では、systemd はプログラムがフォアグラウンドで実行され続けることを想定します。そのため、バックグラウンドへ fork する daemon は、fork した時点で終了したように見えます。Type=forking では、systemd は最初のプロセスが終了するまで待機します。そのため、フォアグラウンドのプログラムでは、TimeoutStartSec= が切れるまで start job が停止したように見えます。Type= をプログラムの動作に合わせ、プログラムにフォアグラウンド用の flag がある場合は、その flag とデフォルトの Type=simple を使用します。

短い status 出力ではなく、実際のエラーを確認するにはどうすればよいですか?

systemctl status は journal の最後の数行だけを表示し、長い行を省略します。journalctl -u myapp.service -b --no-pager を実行すると、この boot 中に unit が記録したすべての内容を取得できます。-n 200 を追加して表示範囲を広げるか、grep に pipe します。アプリケーションが独自の log file に書き込む場合は、そのファイルも確認します。systemd が取得するのは、プログラムが standard output と standard error に送った内容だけだからです。

エラーメッセージがないのに unit が inactive なのはなぜですか?

最も多い原因は、Condition* directive によって起動がスキップされたことです。これらの check は通知を出しません。condition に失敗しても、start job は成功として扱われます。systemctl show myapp.service -p ConditionResult を実行して ConditionResult=no を探し、その check を示す journal の行を確認します。もう1つの一般的な原因は masked unit です。masked unit は、sudo systemctl unmask で解除するまで、すべての起動を拒否します。

unit file を変更するたびに daemon-reload は必要ですか?

unit file または drop-in を編集した場合は必要です。sudo systemctl daemon-reload により、systemd はディスク上のファイルを再読み込みし、その後 sudo systemctl restart myapp.service が実行中のサービスに適用します。systemctl edit の後は不要です。これは自動的に再読み込みを行います。また、systemd ではなくアプリケーションに属する設定ファイルを変更した場合も不要です。

#systemd#troubleshooting#journalctl#exit-codes#linux-fundamentals