systemdには、依存関係を記述するためのディレクティブとして Wants=, Requires=, BindsTo= などがあり、順序を記述するために Before= と After= が用意されています。systemd.unit(5)に説明は書かれていますが、具体的にどう動くのか分からなかったので、ユニットの起動処理と停止処理に注目して挙動を調べました。
Unitセクション
まずは各ディレクティブの概要から。マニュアルから起動、停止に関連する部分を抜き出して意訳したものを載せます。
Wants=
弱い依存関係を示す。a.service の実行に b.service が必要なとき Wants=b.service と記述する。ユニットファイルに記述する他に、a.service へのシンボリックリンクを b.service.wants/ ディレクトリ以下に配置することでも依存関係を注入できる。
Wants= で指定した依存先ユニットの起動に失敗しても、要求したユニット自体は起動する。また、依存関係はサービスの起動や停止の順序に影響しない。After= または Before= を設定しない限り依存元と依存先は同時に起動する。
Requires=
強い依存関係を示す。Wants= に似て、ユニットファイルに記述する他に a.service へのシンボリックリンクを b.service.requires/ ディレクトリ以下に配置することでも依存関係を注入できる。
Requires= と After= を同時に指定している場合、依存先ユニットの起動に失敗すると要求したユニットも起動しない。ただし ConditionXxx= による条件チェックに失敗した場合、依存先ユニットが正常に停止した場合などは要求したユニットを実行する。
BindsTo=
Requires= よりも強い依存を意味する。Requires= の挙動に加えて、依存ユニットが突然非アクティブ状態になった場合、要求したユニットも一緒に停止する。After= と同時に使うと、上記に加えて ConditionXxx= の条件チェックに失敗した場合でも要求したユニットを停止する。
PartOf=
Requires= と同様の依存関係となるが、影響はユニットの停止と再起動に限定される。依存先→依存元の単方向な依存となる。
Upholds=
Wants= に似ているが、これを設定したユニットが動作している間、Wants= にリストされたユニットも維持される。Wants= は起動時だけに影響するがだが、これは要求したユニットが実行中の間は永続的に影響する。
Before=
ユニットに Before=b.service と記述された場合、このユニットが起動完了するまで b.service の起動を遅延する。複数のユニットを記述する必要がある場合はスペース区切りで列挙する。
ユニットを停止する際には順序が逆になる。a.service の後に b.service を起動した場合、停止するときは b.service の後に a.service を停止する。
After=
Before= の順序が逆になる。
実際の動作検証
上記を読んだとき、なんとなく分かりそうだけど PartOf= の動作は全然わからないと感じました。なので以下のユニットを用意して手元で実験してみました。この手の例は a.service や b.service がよく使われる印象ですが、どっちが依存元なのか分かりづらいので app.service と db.service で例を書きます。まずは他ユニットを要求する app.service のユニットファイルです。
[Unit] Description=app Wants=db.service [Service] Type=simple ExecStart=/bin/sleep inf
次に依存先となる db.service のユニットファイルです。
[Unit] Description=db [Service] Type=simple ExecStart=/bin/sleep inf
この2つを使って、app.service の Wants= を変更したり、After= を設定したりと変化させつつ動作を確認します。
ユニットを起動したときの結果
app.service と db.service どちらも実行していない状態からそれぞれを起動したときは、表のように動作しました。余談ですが、Requires= + BindsTo= + PartOf= のように、1つのユニットに複数のディレクティブを記述した場合、制約の強いものが反映されるようです。
| ディレクティブ | appを起動したとき | appを起動したとき(After=あり) | dbを起動したとき |
|---|---|---|---|
| Wants= | app と db を同時に起動する | db, app の順に起動する | db だけ起動する |
| Requires= | app と db を同時に起動する | db, app の順に起動する | db だけ起動する |
| Requisite= | app だけ起動する | app は起動に失敗する | db だけ起動する |
| BindsTo= | app と db を同時に起動する | db, app の順に起動する | db だけ起動する |
| PartOf= | app だけ起動する | app だけ起動する | db だけ起動する |
| Upholds= | app と db を同時に起動する | db, app の順に起動する | db だけ起動する |
After= を設定しない場合は、Requisite= と PartOf= において依存先が起動していなくても app.service を実行中にします。それ以外は db.service を同時に起動します。このとき After= を設定していないので、Requisite= であってもユニットは実行中状態になります。また、「同時に」と書いてはいますが、プロセスIDを見る限りでは、保証はされていないと思うけれども app.service を先に起動しているようです。
After= を設定した場合は挙動が変化して、実行順序を保証しつつ Requisite= の起動が失敗するようになります。
表の右端でみるように db.service 単体で起動した場合は app.service に影響を与えません。db.service を起動したとき同時に app.service を起動したければ、下でみるように Install セクションの RequiredBy= または WantedBy= を使います。
依存先ユニットの起動に失敗したときの結果
次に依存先ユニットの起動に失敗したときの動作をみます。失敗させるため db.service はすぐに失敗するようなユニットにしておきます。
[Unit] Description=db [Service] Type=simple -ExecStart=/bin/sleep inf +ExecStart=/bin/false
上の実験と同様に、app.service の Wants= を変更しつつ、依存先として db.service を起動したときの様子を確認します。
| ディレクティブ | dbの起動に失敗 |
|---|---|
| Wants= | app だけ起動する |
| Requires= | app だけ起動するが数分後に app を停止する(1) |
| Requisite= | app もすぐに停止する |
| BindsTo= | app もすぐに停止する |
| PartOf= | app だけ起動する |
| Upholds= | app を起動したまま db もリトライし続ける(2) |
表として表現するのが困難な事柄がいくつかあったので補足です。
(1)で数分後というのは、db.service が失敗してすぐに app.service も停止する訳ではありませんでした。手元の環境ではしばらく app.service だけで実行し続けて、だいたい2分後に app.service も停止しました。systemdのユニットにおけるrequiresが機能してないように見えますの回答によると、この挙動は Type=simple だから「起動と同時に成功した」扱いとなっているようです。2分という時間の出どころは不明ですが、Restart= ディレクティブなどで変化するのかもしれませんが試していません。
(2)では、Upholds= は app.service を実行中にしつつ、db.service をリトライし続けます。10分待ってみたけどリトライが止まらないので、それ以上は確認していません。
ユニットを停止したときの結果
最後に、停止するときの動作は次のようになります。After= の有無による変化はありません。
| ディレクティブ | appの正常/異常終了 | dbの正常終了 | dbの異常終了 |
|---|---|---|---|
| Wants= | db は継続する | app は継続する | app は継続する |
| Requires= | db は継続する | app も停止する | app は継続する |
| Requisite= | db は継続する | app も停止する | app は継続する |
| BindsTo= | db は継続する | app も停止する | app も停止する |
| PartOf= | db は継続する | app も停止する | app は継続する |
| Upholds= | db は継続する | db が再起動される | db が再起動される |
Unitセクションのまとめ
After= の有無や Type= の値によって挙動が変わるので難しいですが、誤解を気にせず簡単に言えば Wants= や Requires= は「ユニットを起動するとき依存先ユニットも一緒に起動するか」「依存先ユニットが停止したときどう振る舞うか」がディレクティブ毎に異なると言えそうです。
Installセクション
Install セクションは systemctl enable または systemctl disable したときに影響します。Unit セクションの内容が分かっていれば、依存の向きが逆になる(自身に依存させるような書き方をする)だけで動作としては同じなので理解しやすいと思います。
WantedBy=
WantedBy= にユニットを記述とすると、そのユニットに Wants= を記述した動作と同等になります。例えば acme.service に WantedBy=graphical-session.target を記述した場合、
# acme.service [Unit] Requires=graphical-session.target After=graphical-session.target [Install] WantedBy=graphical-session.target
上記の acme.service を systemctl enable で有効化すると graphical-session.target.wants/ ディレクトリに acme.service へのシンボリックリンクが作成されて、結果的に graphical-session.target が acme.service に依存した状態になります。このとき、acme.service が graphical-session.target に依存して、graphical-session.target も acme.service に依存する状態となって循環するのでは、と危惧するかもしれませんが、上記でみたように Wants= や Requires= は依存先ユニットを起動または停止するかどうかでしかないので問題なく動作します。
ところで graphical-session.target が acme.service に依存すると書くと、依存関係として逆じゃないかと思います。個人的にはずっと気持ち悪さを感じていましたが、.target なユニットは「あるべき状態」を定義するものなので「graphical-session.target としてあるべき状態は xxx.service を含む」と解釈すれば違和感は解消されるのではないでしょうか。
RequiredBy=
WantedBy= と似ていますが Requires= 相当となります。