Systemd依存関係の理解:ユニット競合の防止と修正
systemdの依存関係、順序付け、ターゲット、競合の仕組みを学び、サービスを確実に起動し、障害のデバッグを容易にします。
Systemd依存関係の理解:ユニット競合の防止と修正
Systemdの依存関係は、中途半端に理解されがちです。ユニットファイルにRequires=postgresql.serviceと記述しても、アプリケーションがまだ早く起動しすぎて、なぜsystemdが依存関係を無視したのかと疑問に思うことがあります。systemdは何も無視していません。Requires=とAfter=は異なる質問に答えるものです。
この区別が、ほとんどのsystemd依存関係問題の核心です。一方のディレクティブは、別のユニットが同じトランザクションに引き込まれるかどうかを制御します。別のディレクティブは順序を制御します。その他のディレクティブはシャットダウン動作をリンクしたり、あるユニットのライフタイムを別のユニットにバインドしたり、2つのユニットを相互排他的にしたりします。これらの概念を分離すれば、ユニットの競合ははるかに謎めかなくなります。
基礎:Systemdユニット依存関係ディレクティブ
Systemdは、ユニットファイル(通常は/etc/systemd/system/または/lib/systemd/system/に配置)内の特定のディレクティブを使用して、あるユニットがいつ起動、停止、または別のユニットを待つべきかを指示します。これらのディレクティブを理解することは、依存関係を正しく管理するための第一歩です。
コア依存関係ディレクティブ
これらのディレクティブはユニット間の関係を制御します。それ自体では、常に起動順序を制御するわけではありません。
Requires=:- 強い依存関係を確立します。必要なユニットの起動に失敗した場合、現在のユニットも失敗します。
PartOf=を意味するわけではなく、自動的に「このユニットの後に起動する」ことを意味するわけでもありません。
Wants=:- 弱い依存関係です。対象のユニットが失敗しても、現在のユニットは起動を試み続けます。これはオプションの依存関係に使用されます。
BindsTo=:Requires=と似ていますが、停止に関してより強力です。バインドされたユニットが(何らかの理由で)停止すると、現在のユニットも停止します。
PartOf=:- 現在のユニットが別のユニット(例:メインサービスに関連する特定のソケットアクティベーション)の従属部分であることを示します。上位のユニットが停止すると、従属ユニットも停止します。
Conflicts=:- 2つのユニットが同時にアクティブになってはならないことを示します。一方を起動すると、systemdは同じトランザクションの一部として他方を停止します。
コア起動同期ディレクティブ
これらのディレクティブは、依存ユニットが必須ユニットに対していつ起動すべきかを指示します。
After=:- 現在のユニットの開始ジョブが、リストされたユニットの開始ジョブの後に順序付けられることを指定します。それ自体でそのユニットを引き込むわけではありません。
Before=:- 現在のユニットがリストされたユニットの前に起動すべきであることを指定します。
ベストプラクティス: 一般的なサービス起動順序付けには、
Wants=とAfter=の組み合わせが最も一般的で安全なパターンです。Requires=は、依存関係の失敗が必ず依存サービスを失敗させる必要がある場合に予約すべきです。
例:サービスファイルでの依存関係の定義
PostgreSQL(postgresql.service)が管理するデータベースと通信する必要があるカスタムアプリケーションサービスmyapp.serviceを考えます。
# /etc/systemd/system/myapp.service
[Unit]
Description=My Custom Application
# PostgreSQLが起動していることを確認してから起動を試みる
Requires=postgresql.service
After=postgresql.service
[Service]
ExecStart=/usr/bin/myapp
[Install]
WantedBy=multi-user.target
この例は意図的に厳格です。PostgreSQLが起動できない場合、myapp.serviceも失敗する必要があります。データベースなしで縮退モードで実行できるサービスの場合は、同じAfter=postgresql.serviceの順序付けでWants=postgresql.serviceを使用します。アプリケーションは依然としてsystemdに最初にPostgreSQLを起動するよう要求しますが、PostgreSQLが利用できない場合でも続行することが許可されます。
現実に合致する場合、弱い関係を選択しても恥ずかしいことではありません。メトリクスエクスポーター、ログシッパー、キャッシュウォーマーは、依存関係が存在する場合に有用ですが、メインシステムの起動をブロックすべきではありません。ハードな依存関係は、他のユニットなしで起動することが間違っているか危険な場合にのみ予約するのが最善です。
ネットワークアプリケーションの場合は、より注意が必要です。network.targetは、多くの場合、基本的なネットワークスタックが存在することを意味し、DHCPが完了したかDNSが使用可能であることを意味するわけではありません。アプリケーションが起動時に実際に設定されたネットワークを必要とする場合は、以下を使用します。
[Unit]
Wants=network-online.target
After=network-online.target
次に、ディストリビューションに対応するwait-onlineサービス(systemd-networkd-wait-online.serviceやNetworkManagerのwait-onlineユニットなど)が有効になっていることを確認します。これがないと、network-online.targetはあなたが思っているようには待機しない可能性があります。
依存関係の問題の診断
サービスが起動に失敗した場合、systemdは通常ログに十分な情報を提供しますが、依存関係チェーンが根本原因を不明瞭にする可能性があります。以下は、トラブルシューティングに不可欠なツールとコマンドです。
1. ユニットのステータスとログの確認
基本的な出発点は、サービスのステータスを確認し、起動失敗の試行直後にログをレビューすることです。
# 全体的なステータスを確認(依存関係の失敗に言及していることが多い)
systemctl status myapp.service
# ユニットに特化した詳細なログを表示
journalctl -u myapp.service --since "5 minutes ago"
2. 依存関係ツリーの分析
Systemdは、何が**何を待っているかを正確に確認するための強力な可視化ツールを提供します。
systemctl list-dependencies
このコマンドは、指定されたユニットによって必要とされる、または望まれるユニットを、依存関係チェーン全体をたどって表示します。
myapp.serviceの起動に必要なものを確認するには:
# 前方依存関係(自分の前に起動しなければならないもの)
systemctl list-dependencies --after myapp.service
# 逆依存関係(自分に依存するもの)
systemctl list-dependencies --before myapp.service
ツリー形式が邪魔な場合は--plainを使用し、非アクティブなユニットも表示する必要がある場合は--allを追加します。
systemctl list-dependencies --plain --all myapp.service
systemd-analyze dot
より大きな依存関係の質問については、グラフを生成して視覚的に検査します。
systemd-analyze dot 'myapp.service' | dot -Tsvg > myapp-deps.svg
Graphvizがインストールされていないサーバーでは、テキスト出力も依然として有用です。ユニット名を検索して、systemdが認識しているエッジを確認できるからです。ブートタイミングの問題については、systemd-analyze critical-chain myapp.serviceの方が完全なグラフよりも読みやすいことがよくあります。
3. 競合と順序付けの問題の検出
依存関係の競合は、サービスが早すぎる起動や予期しない停止のために失敗するという形で現れることがよくあります。
循環依存関係: これは最も危険な競合であり、ユニットAがBを必要とし、ユニットBがAを必要とする場合です。Systemdはこれを解決しようと試みますが、多くの場合、一方または両方のユニットが無期限にfailedまたはactivating状態のままになります。
システム全体にわたる潜在的な問題を見つけるには、順序付けに関連する特定の障害メッセージをログで検索できます。
journalctl -b | grep -E "failed|refused to start|dependency was not satisfied"
また、実行時の動作が問題であると想定する前に、カスタムユニットに対してsystemd-analyze verifyを実行します。
systemd-analyze verify /etc/systemd/system/myapp.service
これにより、順序付けの循環、不明なディレクティブ、無効なユニット参照を早期にフラグ付けできます。設計が正しいことを証明するわけではありませんが、複雑な依存関係の問題であるかのようにタイプミスを追いかける手間を省くことができます。
一般的な依存関係の問題の修正
特定されたら、依存関係の問題は関連するユニットファイルのディレクティブを調整することで解決できます。
シナリオ1:前提条件の準備ができる前にサービスが起動する
症状: アプリケーションログにデータベース接続エラーが表示されるが、systemctl statusではpostgresql.serviceがactiveと表示される。
診断: サービスにAfter=postgresql.serviceがないか、アプリケーションが必要とする特定のデータベース、ソケット、資格情報、スキーマの準備ができる前にPostgreSQLがアクティブになっている可能性があります。Systemdはユニットを順序付けできますが、すべてのアプリケーションレベルの準備状態条件を自動的に理解できるわけではありません。
修正: 単純なユニット関係から始めます。
[Unit]
Requires=postgresql.service
After=postgresql.service
それでもアプリケーションがデータベースと競合する場合は、適切なレイヤーで準備状態の問題を修正します。一部のサービスはType=notifyをサポートしており、初期化が完了した後にのみ準備完了を報告します。一部のアプリケーションは、依存関係がブート時だけでなくいつでも再起動する可能性があるため、再試行ロジックを必要とします。限定的なローカルチェックには、ExecStartPre=コマンドが合理的です。
[Service]
ExecStartPre=/usr/bin/pg_isready -q -h 127.0.0.1 -p 5432
ExecStart=/usr/local/bin/myapp
この種の事前チェックは控えめに使用してください。スリープとループを含むシェルスクリプトに成長した場合、アプリケーションは代わりに適切な再試行動作を必要とする可能性があります。
依存関係の修正としてsleep 30は避けてください。静かな開発マシンでは競合を隠すかもしれませんが、ストレージが遅い、VMがビジー、DNSを待っているホストでは再び失敗する可能性があります。実際の順序付けディレクティブ、準備完了通知、ソケットアクティベーション、またはアプリケーションの再試行ループは、十分な時間が経過したという期待ではなく、サービスが準備完了である理由を提供します。
シナリオ2:起動/停止順序の競合
症状: システムを停止すると、重要なプロセスがハングしたり、突然失敗したりする。
診断: これは多くの場合、BindsTo=の誤用、または兄弟サービスにおけるBefore=とAfter=ディレクティブの複雑な相互作用を示しています。
修正: 兄弟であるサービス(例:同じターゲットによって起動されるサービス)をレビューします。サービスAがサービスBの実行中に実行されなければならない場合は、BindsTo=またはRequires=を使用します。サービスAがタスクを完了してからサービスBがクリーンアップを開始する必要がある場合は、After=の順序が正しいことを確認します。
シャットダウンの順序は起動の順序の逆であることを忘れないでください。app.serviceにAfter=database.serviceがある場合、シャットダウン時にsystemdはdatabase.serviceの前にapp.serviceを停止します。これは通常、望ましい動作です。アプリケーションはデータベースが消える前に作業の受け入れを停止します。多くのシャットダウンバグは、別個のシャットダウン専用設定ではなく、起動順序付けの欠如に起因します。
シナリオ3:不要な依存関係の削除
症状: 不要なサービスが起動チェーンに引き込まれているため、システムの起動が遅い。
診断: オプションの接続のみが必要な場合にRequires=を使用した可能性があります。
修正: Requires=をWants=に変更します。サービスが機能するために依存関係が絶対に必要でない場合、Wants=は依存関係が失敗したりマスクされたりしてもシステムが続行できるようにします。
# 修正前(厳しすぎる)
Requires=optional_logging.service
# 修正後(より良い)
Wants=optional_logging.service
After=optional_logging.service
これは、監視エージェント、メトリクスエクスポーター、サイドカーヘルパー、オプションのローカルキャッシュに特に役立ちます。メインサービスがそれらなしで有用な作業を実行できる場合、Requires=は部分的な障害を完全な障害に変えます。厳格な依存関係は、本当に必要なものに使用します。それなしでは起動できないアプリケーションのローカルデータベース、アプリケーションのデータを保持するマウントポイント、または唯一サポートされているアクティベーションパスであるソケットユニットなどです。
シナリオ4:マウントまたはデバイスの準備ができていない
症状: サービスがブート時にNo such file or directoryで失敗するが、ログイン後にはパスが存在する。これは、/mnt/data、/srv/app、リムーバブルディスク、暗号化ボリューム、ネットワークファイルシステムから読み取るサービスでよく発生します。
診断: サービスがマウントユニットのアクティブ化より前に起動しているか、マウントがオプションであり、サービスを停止せずに失敗した可能性があります。
修正: マウントユニット名を見つけます。
systemd-escape -p --suffix=mount /mnt/data
/mnt/dataの場合、通常はmnt-data.mountが生成されます。次に、サービスの後に順序付けし、サービスがデータなしで実行できない場合はそれを要求します。
[Unit]
Requires=mnt-data.mount
After=mnt-data.mount
マウントが/etc/fstabからのものである場合、nofail、x-systemd.automount、_netdevなどのオプションがブート動作に影響します。そのマウントの失敗が本当にサービスをブロックすることを望まない限り、オプションのマウントにハードなRequires=を追加しないでください。
シナリオ5:ターゲットが引き込みすぎる
症状: サービスを有効にすると、無関係なユニットが起動しているように見える、またはサービスを無効にしてもブート中に表示されなくならない。
診断: サービスは、[Install] WantedBy=...によるターゲット、別のサービスのWants=、またはソケット、タイマー、パス、マウントユニットによって引き込まれている可能性があります。enableはブート時アクティベーションのためのシンボリックリンクを作成します。これはユニットが起動できる唯一の方法ではありません。
修正: ユニットと逆依存関係の両方を検査します。
systemctl cat myapp.service
systemctl list-dependencies --reverse myapp.service
systemctl list-timers --all | grep myapp
systemctl list-sockets --all | grep myapp
ソケットユニットがサービスをアクティブ化する場合、myapp.serviceのみを無効にするだけでは不十分な場合があります。望ましい動作に応じて、myapp.socketも無効化またはマスクする必要があるかもしれません。
変更の適用とリロード
ユニットファイルを変更するたびに、変更をテストする前にsystemdに設定をリロードするよう指示する必要があります。
# 1. systemdマネージャー設定をリロード
sudo systemctl daemon-reload
# 2. 影響を受けるサービスを再起動
sudo systemctl restart myapp.service
# 3. ステータスを確認
systemctl status myapp.service
小さなメンタルチェックリスト
依存関係の問題が複雑に感じられたら、いくつかの簡単なチェックに絞り込みます。
まず、他のユニットをそもそも起動すべきかどうかを尋ねます。はいの場合は、Wants=またはRequires=を使用します。現在のサービスがそれなしで実行できる場合は、Wants=を優先します。そのユニットの失敗がこのサービスを失敗させることを意味する場合は、Requires=を使用します。
次に、起動順序が重要かどうかを尋ねます。はいの場合は、After=またはBefore=を追加します。依存関係ディレクティブがそれ自体で順序付けを処理することを期待しないでください。
第三に、起動後のライフタイム結合が重要かどうかを尋ねます。あるユニットの停止が別のユニットを停止させる必要がある場合は、BindsTo=またはPartOf=を検討します。これらを軽率に使用しないでください。日常的な再起動が予想以上にカスケードする可能性があります。
最後に、systemdが実際にロードしたものを検査します。
systemctl cat myapp.service
systemctl show myapp.service -p Wants -p Requires -p After -p Before -p Conflicts
この出力は、編集したと思っているファイルよりも多くの場合有用です。ドロップイン、ベンダーユニット、生成されたユニット、ソケット、タイマー、ターゲットはすべて最終的な動作を変更する可能性があります。