Systemdのトラブルシューティング:サービス依存関係と順序指定ディレクティブの理解

Requires、Wants、After、Before、および診断コマンドを正しく使用して、systemdの依存関係と順序の問題を修正します。

Systemdのトラブルシューティング:サービス依存関係と順序指定ディレクティブの理解

ほとんどの混乱を招くsystemdの依存関係バグは、要件と順序という2つの別々の概念を混同することから生じます。Requires=Wants=は「他のどのユニットを引き込むべきか?」に答え、After=Before=は「このユニットは別のユニットに対していつ起動すべきか?」に答えます。このガイドから1つだけ覚えておくなら、After=postgresql.serviceはPostgreSQLを自動的に起動しないことを覚えておいてください。これは、両方のユニットが起動されている場合、あなたのユニットはPostgreSQLの起動ジョブが実行されるまで待つべきだと言っているだけです。

この区別は、「手動で起動すると動作するが、再起動後は失敗する」という多くのインシデントの原因です。Webアプリがデータベースソケットが接続を受け入れる前に起動する。ワーカーが/mnt/jobsがマウントされる前に起動する。サービスがnetwork.targetを待つが、IPアドレスがまだ割り当てられていないために失敗する。これらは特殊なsystemdの問題ではありません。これらは明示的にする必要がある通常の起動時の前提条件です。

コア依存関係ディレクティブ:RequiresWants

Systemdは、ユニット間の直接的な依存関係を定義するために2つの主要なディレクティブを使用します:RequiresWants。これらのディレクティブは、ユニットファイル(例:.serviceファイル)の[Unit]セクション内に配置されます。

Requires=

Requires=ディレクティブは、強い依存関係を確立します。ユニットAがユニットBをRequires=する場合、systemdはAが起動されたときにBを起動します。Bを起動できない場合、Aの起動ジョブも失敗します。Aが実行中にBが明示的に停止された場合、通常はAも停止されます。これは、必要な関係がもはや成立しないためです。

例:

データベースサービス(mariadb.service)に重大に依存するWebアプリケーションサービス(myapp.service)を考えます。myapp.serviceユニットファイルには以下が含まれる可能性があります:

[Unit]
Description=My Web Application
Requires=mariadb.service

[Service]
ExecStart=/usr/bin/myapp

[Install]
WantedBy=multi-user.target

このシナリオでは、mariadb.serviceの起動に失敗するか手動で停止された場合、systemdはmyapp.serviceも停止します。myapp.serviceを起動しようとしてmariadb.serviceが実行されていない場合、systemdは最初にmariadb.serviceを起動しようとします。mariadb.serviceが失敗すると、myapp.serviceは起動しません。

Wants=

Wants=ディレクティブは、より弱いオプションの依存関係を定義します。ユニットAがユニットBをWants=する場合、systemdはユニットAを起動するときにユニットBを起動しようとしますが、ユニットBの起動に失敗したり実行されていなくても、ユニットAはアクティブになります。これは、別のサービスから恩恵を受けるが、機能が低下したり警告が表示されたりしても独立して機能できるサービスに役立ちます。

例:

監視エージェント(monitoring-agent.service)が特定のロギングサービス(app-logger.service)なしでも実行できるが、理想的には利用可能であるべきだとします。monitoring-agent.serviceユニットファイルは次のようになります:

[Unit]
Description=Monitoring Agent
Wants=app-logger.service

[Service]
ExecStart=/usr/bin/monitoring-agent

[Install]
WantedBy=multi-user.target

ここで、systemdはmonitoring-agent.serviceがアクティブ化されたときにapp-logger.serviceを起動しようとします。ただし、app-logger.serviceの起動に失敗した場合でも、monitoring-agent.serviceは正常に起動を続行します。

Requires= vs. Wants=

  • Requires=:強い依存関係。必要なユニットが失敗すると、依存するユニットは失敗するか停止します。
  • Wants=:弱い依存関係。依存するユニットは必要なユニットを起動しようとしますが、失敗しても続行します。

Requires=Wants=を暗黙的に含むことに注意することが重要です。ユニットが別のユニットを必要とする場合、暗黙的にそれを「欲しい」ともみなします。

順序指定ディレクティブ:AfterBefore

RequiresWants何を実行する必要があるかを定義するのに対し、AfterBeforeはユニットが互いに対していつ起動されるべきかを定義します。これらのディレクティブは、システムの起動プロセス中またはユニットがオンデマンドでアクティブ化されるときの操作の順序を制御します。これらは依存関係ディレクティブと組み合わせて使用されることがよくあります。

After=

After=ディレクティブは、現在のユニットの起動ジョブがリストされたユニットの起動ジョブの後に順序付けられるべきであることを指定します。それ自体では、それらのユニットをトランザクションに引き込みません。また、依存関係がアプリケーションにとって論理的に準備ができていることを証明するわけでもありません。systemdのユニットのアクティブ化状態のビューのみを使用します。

例:

ネットワーク依存のサービス(custom-network-app.service)は、ネットワークが完全に構成された後にのみ起動する必要があります。これは通常、ネットワークターゲット(network.target)の後に起動することを保証することで処理されます。

[Unit]
Description=Custom Network Application
Requires=network.target
After=network.target

[Service]
ExecStart=/usr/bin/custom-network-app

[Install]
WantedBy=multi-user.target

この構成では、custom-network-app.servicenetwork.targetの両方が同じトランザクションの一部である場合、systemdはcustom-network-app.servicenetwork.targetの後に順序付けます。アドレス、DNS、または別のホストへのルートを必要とするサービスの場合、network-online.targetが意図に近いことがよくありますが、それはディストリビューションのwait-onlineサービスが有効で正しく構成されている場合に限ります。

Before=

Before=ディレクティブは、現在のユニットがBefore=にリストされたユニットの前に起動されるべきであることを指定します。これは、シャットダウン中に他のユニットの後に停止する必要があるサービスや、特定のサービスの前に起動して環境を提供する必要があるサービスに役立ちます。

例:

メールサーバー(postfix.service)が、メールを送信する可能性のあるユーザー向けサービスの前に実行されている必要があるシナリオを想像してください。Before=を使用して、postfix.serviceが早く起動することを保証できます。

[Unit]
Description=Postfix Mail Transfer Agent
# ... 他のディレクティブ(Conflicts=など)
Before=user-session.target

[Service]
ExecStart=/usr/lib/postfix/master

[Install]
WantedBy=multi-user.target

この設定は、user-session.targetの一部であるすべてのものが起動を開始する前にpostfix.serviceを起動しようとします。同様に、シャットダウン中は、postfix.serviceAfter=user-session.targetが対応している場合、最後に停止されるものの1つになります。

After= vs. Before=

  • After=:リストされたユニットが現在のユニットの起動前にアクティブであることを保証します。
  • Before=:現在のユニットがリストされたユニットの前に起動することを保証します。

After=Before=は補完的です。ユニットAがBefore=Bと言う場合、順序はAが最初、次にBです。ユニットBがAfter=Aと言う場合、結果は同じです。通常、関係を1つのユニットファイルで表現するだけで済みます。自分のサービスを編集するときは、自分のサービスが何の後に来る必要があるかを言う方が通常は明確です。なぜなら、それは推論をローカルに保つからです。

堅牢な構成のためのディレクティブの組み合わせ

実際のシナリオでは、これらのディレクティブを組み合わせて複雑な依存関係グラフを作成することがよくあります。multi-user.targetは、システムがマルチユーザー操作の準備ができていることを示す一般的なターゲットです。多くのサービスは、WantedBy=multi-user.targetおよびAfter=multi-user.target(より正確には、multi-user.targetが依存するAfter=basic.targetAfter=getty.targetなど)になるように構成されています。

一般的なパターン:

データベースを必要とし、ネットワーク構成後に起動する必要があるサービスは、次のようになります:

[Unit]
Description=My Application Service
Requires=mariadb.service
Wants=other-optional-service.service
After=network.target mariadb.service

[Service]
ExecStart=/usr/local/bin/my_app

[Install]
WantedBy=multi-user.target

パターンの説明:

  1. Requires=mariadb.servicemy_app.serviceが機能するためにはmariadb.serviceが実行されている必要があることを保証します。mariadb.serviceが失敗すると、my_app.serviceは停止します。
  2. Wants=other-optional-service.serviceother-optional-service.serviceを起動しようとしますが、失敗してもmy_app.serviceは続行します。
  3. After=network.target mariadb.servicemy_app.serviceをネットワークターゲットとMariaDBの起動ジョブの後に順序付けます。アプリが別のホスト上のデータベースにTCP経由で接続する必要がある場合は、network.targetが「ネットワークが使用可能」を意味すると仮定する代わりに、適切なnetwork-online設定を使用してください。
  4. WantedBy=multi-user.target:有効にすると(systemctl enable my_app.service)、このディレクティブはシンボリックリンクを追加し、システムがmulti-user.target状態に達したときにmy_app.serviceが起動されるようにします。

高度な考慮事項とベストプラクティス

  • WantedBy vs. RequiredByWants vs. Requiresと同様に、WantedByは弱い順序付けであり、RequiredByは強い順序付けです。ほとんどのサービスはWantedBy=multi-user.targetを使用します。
  • Conflicts=:このディレクティブは、現在のユニットと同時に実行されるべきではないユニットを指定します。現在のユニットが起動されると、競合するユニットは停止され、その逆も同様です。
  • 推移的依存関係:依存関係は推移的です。AがBを必要とし、BがCを必要とする場合、Aは間接的にCを必要とします。Systemdはこれらのチェーンを自動的に処理します。
  • Condition*=ディレクティブConditionPathExists=ConditionFileNotEmpty=ConditionVirtualization=などを使用して、システム状態に基づいてユニットのアクティブ化を条件付きにし、堅牢性をさらに高めます。
  • systemctl list-dependencies <unit>を使用する:このコマンドは、直接および間接的な依存関係を含むユニットの依存関係ツリーを視覚化するのに非常に役立ちます。
  • systemctl status <unit>を使用する:設定変更後は常にサービスのステータスを確認してください。多くの場合、依存関係の問題を含む失敗の理由が表示されます。
  • 循環依存関係を避ける:systemdはそれらを解決しようとしますが、直接的な循環依存関係(A Requires BB Requires A)は起動ループや失敗につながる可能性があります。これを避けるために依存関係を慎重に設計してください。

実用的なデバッグ手順

依存関係のバグが発生した場合、まずsystemdが実際に構築したトランザクションを確認します:

systemctl status my_app.service
journalctl -b -u my_app.service --no-pager
systemctl list-dependencies my_app.service
systemctl show my_app.service -p Wants -p Requires -p After -p Before -p BindsTo -p PartOf

systemctl statusは、systemdがユニットの起動に失敗したか、後で強制終了したか、アプリケーションが異常であるにもかかわらずアクティブと見なしているかを示します。journalctl -bは現在のブート内に留まります。これは、依存関係の問題がブート時にのみ発生することが多いため重要です。systemctl showは直接的ですが便利です。ドロップイン、ベンダーファイル、生成された依存関係が適用された後の最終的なマージ済みユニットプロパティを表示します。

依存関係がどこから来たのかわからない場合は、完全なユニットを検査します:

systemctl cat my_app.service

これにより、パッケージ化されたユニットと/etc/systemd/system/my_app.service.d/の下にあるオーバーライドファイルが表示されます。ベースユニットは正しいが、古いオーバーライドに以前の移行からのAfter=mysql.serviceがまだ含まれているという本番サービスの例を見たことがあります。サービスはもう存在しないユニットを待っており、ログはアプリケーションが壊れているように見せかけていました。

起動タイミングの問題については、次を使用します:

systemd-analyze critical-chain my_app.service
systemd-analyze blame

critical-chainは、どのユニットがサービスへのパスを遅延させたかを示すため、タイムスタンプをじっと見るよりも優れています。blameは「悪い」サービスのランキングとして扱うと誤解を招く可能性がありますが、1つの依存関係が予想よりもはるかに長くかかる場合に役立ちます。

本番環境で有効なパターン

ローカルデータベースの依存関係の場合、これは妥当な出発点です:

[Unit]
Description=API service
Requires=postgresql.service
After=postgresql.service

[Service]
ExecStart=/usr/local/bin/api
User=api
Restart=on-failure

[Install]
WantedBy=multi-user.target

これは、PostgreSQLがAPIとともに起動され、APIの起動がPostgreSQLの後に順序付けられるべきであることを示しています。すべての移行が完了したことや、データベースがアプリが使用する正確な資格情報を受け入れることを保証するものではありません。それが重要な場合は、アプリケーションレベルの準備完了チェックを追加してください。Systemdはプロセスを順序付けることはできますが、サービスにチェックを教えない限り、スキーマの状態を理解することはできません。

マウントされたパスの場合は、手動でマウントユニットに名前を付けるよりもRequiresMountsFor=を優先します:

[Unit]
RequiresMountsFor=/srv/uploads

[Service]
ExecStart=/usr/local/bin/upload-worker
User=uploads

Systemdはパスに必要なマウントユニットを導出します。これは、/srv/uploadssrv-uploads.mountにマップされることを覚えておくよりも保守が簡単です。

オプションのヘルパーには、Wants=を使用します:

[Unit]
Wants=metrics-agent.service
After=metrics-agent.service

メトリクスエージェントが失敗しても、メインサービスは起動できます。これは通常、ロギングサイドカー、オプションのエクスポーター、ローカル通知ヘルパーに望ましい動作です。2つのサービスが関連しているという理由だけでRequires=を使用しないでください。依存するサービスが他のユニットなしでは本当に有用な作業を実行できない場合にのみ使用してください。

一緒に停止する必要がある緊密に結合されたサービスの場合は、BindsTo=PartOf=を検討してください。BindsTo=Requires=よりも強力で、バインドされたユニットが消えた場合にサービスも消えるべき場合(特定のデバイスユニットに結び付けられたサービスなど)に役立ちます。PartOf=はグループに役立つことがよくあります。親ユニットを再起動または停止すると、子ユニットに伝播する可能性があります。これらは最初に選ぶディレクティブではありませんが、Requires=ではきれいに表現できない問題を解決します。

よくある落とし穴

WantedBy=multi-user.targetで有効になっている通常の長時間実行サービスにAfter=multi-user.targetを追加しないでください。これはしばしば奇妙な順序付けを生み出し、作者の意図をほとんど表現しません。ほとんどのサービスはmulti-user.targetによって引き込まれます。それらはそれがすでに到達された後に起動する必要はありません。

network.targetが「インターネットに到達可能」を意味すると仮定しないでください。これはネットワーク管理の同期ポイントであり、接続性テストではありません。アプリケーションがリモートAPIと通信する場合は、とにかくアプリケーション内に再試行ロジックを追加してください。ブート時のネットワーク順序付けはノイズを減らしますが、DNS障害、ルーティング変更、またはリモート依存関係のダウンから保護することはできません。

より良いオプションがない限り、ExecStartPre=/bin/sleep 30で長いスリープを隠さないでください。スリープは、依存関係がすぐに準備できたときにブートを遅くし、依存関係が予想よりも長くかかったときにまだ失敗します。実際のソケット、ファイル、またはAPIをチェックする小さな準備完了ループは、通常より明確です。

依存関係ディレクティブを変更したら、systemctl daemon-reloadを実行し、サービスを再起動し、同じブートからジャーナルを確認してください。最速の修正は、通常、間違っていた正確な順序付けの仮定を証明するものです。