MENU

systemdユニット起動順序の競合診断|After/Requires依存で防げない障害の切り分け

目次

After/Requires だけでは防げない起動順序の問題とはなにか

systemd のユニット設定で After= や Requires= を記述していても、依然として起動順序の競合が発生するケースがあります。「依存先が起動した」という systemd の判断と「サービスが実際に応答できる状態になった」という事実は、必ずしも一致しないからです。

After=B.service は「B の起動処理が完了したあとに A を起動する」という順序指示であり、B が正常動作していることを保証しません。Requires=B.service は「B の起動に失敗したら A も失敗させる」という依存宣言ですが、B が起動した直後に内部でエラーを起こした場合や、B がポートの listen を完了していない場合には効果がありません。

この仕組みを理解せずに障害を追うと、「設定は正しいはずなのに起動できない」という迷宮に入り込みます。以降では、競合診断の進め方から対処法までを一本で整理します。

systemd の依存関係と順序制御の内部動作

障害を切り分ける前に、systemd の依存・順序制御が実際にどう動作するかを整理しておきます。

依存関係ディレクティブの違い

  • Requires=:指定ユニットが起動しない(または停止した)場合、このユニットも失敗させる。
  • Wants=:Requires の軟化版。指定ユニットの失敗がこのユニットの起動を止めない。
  • BindsTo=:Requires より強く、指定ユニットが停止すると同時にこのユニットも停止させる。
  • After= / Before=:起動順序のみを制御する。依存関係は別途 Requires や Wants で宣言する必要がある。

見落とされがちな点として、After= と Requires= はそれぞれ独立したディレクティブです。Requires=B.service を書いても After=B.service を省略すると、A と B が並行起動される場合があります。逆に After=B.service だけでは B の失敗が A に伝播しません。

Type= が「起動完了」の意味を変える

systemd が「ユニットの起動が完了した」と判断するタイミングは Type= によって異なります。

  • Type=simple(デフォルト):ExecStart のプロセスを fork した時点で「起動完了」とみなす。サービスが実際に接続を受け付けるより前に完了扱いになる。
  • Type=forking:親プロセスが終了(デーモン化完了)した時点で完了。PIDFile= の指定が推奨される。
  • Type=notify:sd_notify(3) で READY=1 を送信した時点で完了。サービス側の実装が必要。
  • Type=oneshot:ExecStart のプロセスが終了するまで完了とみなさない。初期化スクリプトに使われる。

PostgreSQL や MySQL などのデータベースを Type=simple で管理している環境では、After= で依存を宣言してもアプリケーションが接続を試みた時点でデータベースがまだ listen していない、というケースが典型的です。

障害の切り分け手順:journalctl と systemctl で原因を特定する

ステップ1:直近のブート時ログを確認する

まず直近ブート時のユニット別ログを確認します。

# 直近ブートの特定ユニットのログ
journalctl -b -u myapp.service

# 2回前のブートと比較したい場合
journalctl -b -1 -u myapp.service

ログに “Failed to connect” や “Connection refused” などの接続エラーが出ている場合、依存先サービスの準備完了前に起動を試みている可能性が高いです。エラーの発生タイムスタンプと依存先ユニットのログを照合することで、どちらが先に動いたかを把握できます。

ステップ2:ユニットの実際のプロパティを確認する

systemctl show はユニットファイルに書かれた設定だけでなく、ドロップインや自動補完された依存関係も含めた実効値を表示します。

# 依存・順序関係の実効値を確認
systemctl show myapp.service -p After,Before,Requires,Wants,BindsTo

# Type と現在の状態を確認
systemctl show myapp.service -p Type,ActiveState,SubState,Result

ここで期待していた依存先が After= に含まれていない場合、ユニットファイルの記述漏れかドロップインによる上書きが疑われます。

ステップ3:起動タイムラインを可視化する

systemd-analyze は各ユニットの起動タイムラインを出力します。

# 依存チェーンと各ユニットの起動タイミングを表示
systemd-analyze critical-chain myapp.service

# 依存グラフをDOT形式で出力(graphviz が必要)
systemd-analyze dot myapp.service | dot -Tsvg > dep.svg

critical-chain の出力では、各ユニットの起動開始時刻(@)と所要時間(+)が示されます。依存先の起動完了より前に依存元が動き出している場合は、After= の設定か Type= の選択を見直す必要があります。

ステップ4:ドロップインと override の有無を確認する

パッケージ管理側が提供するユニットファイルの設定をドロップインが上書きしていないか確認します。

# ユニットファイルとドロップインを連結して表示
systemctl cat myapp.service

systemctl cat は、本体ユニットファイルと有効なドロップイン(/etc/systemd/system/myapp.service.d/*.conf)を順に連結して表示します。意図しない After= や Type= の書き換えが入っていないか確認できます。

競合パターン別の対処法

パターン1:依存先が「起動完了」でも実際には未応答

Type=simple の依存先サービスがプロセス起動直後に「完了」とみなされ、実際の listen 準備が整う前に依存元が接続を試みるケースです。対処の選択肢は以下の3つです。

  • 依存先を Type=notify に変更する:サービス側で sd_notify を実装している(または対応している)場合に有効。READY=1 の通知を受けてから依存元を起動できる。
  • ExecStartPre でポーリングする:依存元の ExecStartPre に wait-for-it スクリプトや nc -z を使った待機処理を追加する。簡便だが、タイムアウト設計が必要。
  • ソケットアクティベーションを活用する:systemd の .socket ユニットを使うと、ソケットへの最初の接続を受けた時点でサービスを起動できる。サービス準備前の接続はカーネルのソケットキューに保持される。

パターン2:network.target への依存がタイムアウトを引き起こす

一部のディストリビューションでは network.target と network-online.target の意味が異なります。network.target はネットワークインターフェースの「設定開始」を示すに過ぎず、IPアドレスの付与が完了したことを保証しません。外部通信が必要なサービスには network-online.target を使い、NetworkManager-wait-online.service または systemd-networkd-wait-online.service が有効になっているかを確認します。

# NetworkManager-wait-online の有効化
sudo systemctl enable NetworkManager-wait-online.service

# ユニットファイルの依存記述(想定例)
[Unit]
After=network-online.target
Wants=network-online.target

パターン3:oneshot サービスの後処理と競合する

Type=oneshot のサービス(マイグレーション実行・tmpfiles 作成など)が完了する前に依存元が起動するケースです。oneshot は ExecStart のプロセス終了を待つため一見安全ですが、RemainAfterExit=yes を設定していない場合、サービスの ActiveState が inactive になり After= の順序が期待通りに機能しないことがあります。初期化処理を oneshot で実装する場合は RemainAfterExit=yes を合わせて設定するのが一般的です。

切り戻しと注意点

ユニットファイルやドロップインを変更した場合、必ず systemctl daemon-reload を実行してから変更を反映します。reload を忘れた状態で systemctl restart を実行しても、変更前の設定が使われます。

# ユニットファイル変更後の手順
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl status myapp.service

変更を切り戻す場合、ドロップインを編集したときはそのファイルを削除または元に戻してから daemon-reload します。systemctl edit で作成したドロップインは /etc/systemd/system/unitname.d/override.conf に保存されているため、このファイルを削除することで元の設定に戻せます。

Type= を変更する場合は特に注意が必要です。Type=forking にしたにもかかわらず PIDFile= を指定しないと、systemd がプロセスの追跡に失敗して誤検知の失敗を報告することがあります。また Type=notify に変更した場合、サービス側のプロセスが実際に sd_notify を送信するか事前に確認が必要です。対応していないプロセスでは TimeoutStartSec 経過後に失敗扱いになります。

2026年の現行環境における差分

2024〜2025年のディストリビューション(RHEL 9 系・Ubuntu 24.04・Debian 12)では systemd 252 以降が標準となっており、いくつかの変化点があります。

  • Type=notify-reload の追加(systemd 253 以降):SIGHUP によるリロードを sd_notify で通知できる新しい Type です。従来の Type=notify ではリロード完了を systemd に伝えられなかった問題に対応しています。
  • systemd-networkd-wait-online の –any オプション:systemd 244 以降で利用可能な --any オプションにより「いずれかのインターフェースがオンラインになれば完了」という動作を設定できます。複数 NIC 環境でのタイムアウト問題を回避するために活用できます。
  • コンテナ環境での注意点:Podman や Docker のシステムコンテナ(systemd を PID 1 として動作させる構成)では、ホスト側の target に依存する After= 記述がコンテナ内で解決されずに無視されるケースがあります。コンテナ内で動作確認する際は systemctl list-jobs で実際に処理されているジョブを確認するのが有効です。
  • DefaultDependencies=no の扱い:早期起動ユニットで DefaultDependencies=no を設定している場合、sysinit.target より前に起動するため shutdown 時の順序にも影響が出ることがあります。特にコンテナ向け最小構成で手動管理しているユニットで注意が必要です。

systemd のバージョン確認は systemctl --version で行えます。ディストリビューションのバックポート状況によってはマイナーバージョンの機能差があるため、対象環境での動作確認が重要です。

「コマンドは打てる。次は”現場で通用する型”を最短で身につけたい」——そんなあなたへ。

ネットの断片的なTipsをコピペするだけでは、体系的な運用スキルはなかなか積み上がりません。リナックスマスター.JPの無料メルマガ「リナマガ」では、現場で実際に使うLinuxサーバー運用の考え方を、順を追って実務目線でお届けしています。いま登録された方には、安全なサーバー構築の「型」をまとめた『Linuxサーバー構築入門マニュアル(図解60P)』を完全無料でプレゼント中です。

>> 図解60P『Linux入門マニュアル』を無料で受け取る
(メルマガ登録は10秒・いつでも解除OK)

※ 「独学の時間がもったいない」「プロから現場の技術を最短で学びたい」という本気の方には、2日で実務レベルが身につく初心者向けハンズオンセミナーもご用意しています。

PR・広告

[試して理解]Linuxのしくみ 増補改訂版(Amazon)

プロセス・権限・ファイルシステムなどLinux内部を図解で理解できる一冊。運用判断の勘所に。

Amazonで見る

※ Amazonのアソシエイトとして、当サイトは適格販売により収入を得ています。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次