systemdがサービスを再起動しない理由と対処法
systemdのRestart=はメインプロセスだけを監視します。同じcgroup内で子プロセスが停止しても再起動しない仕組みを、Type=、再起動制限、journalの確認方法とともに解説します。
再起動ポリシーが監視するのは 1 つのプロセスです
systemd の再起動ポリシーがユニットごとに監視するのは、メインプロセスという 1 つのプロセスです。Restart= はそのプロセスの終了ステータスだけを読み取り、それ以外は確認しません。ユニットの control group に 20 個のプロセスがあり、そのうち 1 つが停止しても、メインプロセスが稼働していればユニットは active (running) のままです。systemd の観点では何も失敗していないため、何も再起動されません。
systemd は他のプロセスも認識しています。ユニットの停止時にはそれらを終了し、メモリ使用量をユニットの制限に加算し、ユニットの CPU quota を適用し、systemctl status に表示します。ただし、それらの終了ステータスを読み取ることはありません。再起動ロジックと cgroup は別の仕組みであり、このガイドの大部分では、その間にある差異を扱います。
cgroup が保持する情報と、再起動ロジックが参照する情報
cgroup(control group)は、プロセスの集合を管理するカーネルオブジェクトです。すべての service unit には、unit 名に基づく cgroup が割り当てられます。プロセスはそこから離れられません。子プロセスは親プロセスの cgroup を継承し、権限のないプロセスは別の場所へ自分自身を移動できません。そのため systemd は、2 回 fork する daemon も確実に後処理できます。従来の init スクリプトでは、これを確実に行えませんでした。
次の 2 つの事実を並べて確認してください。
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Restart -p RestartUSec myapp.servicesystemd-cgls は unit 内のすべてのプロセスを一覧表示します。MainPID は再起動ポリシーが参照する単一の数値です。この 2 つが想定と一致しない場合、その不一致が問題です。MainPID=0 は誤った PID より深刻です。systemd が何も追跡していないことを意味するため、Restart= の値が発動することはありません。
メインプロセスの規則には、実際の例外が 1 つあります。kernel の out-of-memory killer が unit の cgroup 内にあるプロセスを kill した場合、systemd はそれを検知します。cgroup の memory.events ファイルを監視しているためです。次に何を行うかは OOMPolicy= が決めます。デフォルトは stop です。unit 全体が停止し、結果は oom-kill として記録されます。これは失敗として扱われるため、Restart=on-failure が発動します。journal にはその内容が明確に記録されます。
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Failed with result 'oom-kill'.したがって、メモリ不足で kill された子プロセスは unit 全体を停止させます。一方、同じ子プロセスが segmentation fault で終了しても、unit 全体は停止しません。unit にメモリ制限を設定する場合は、再起動ポリシーを調整する前に、MemoryMax と CPUQuota が unit の cgroup に適用される仕組みを確認してください。この 2 つの機能が関係するのは、この箇所だけです。
How Type= picks the main process
Type= in the [Service] section is not only about start-up ordering. It is the rule that decides which PID (process ID) becomes MainPID, which is the same as deciding what Restart= is able to see.
Type=simpleis the default. The process systemd forks fromExecStart=is the main process. systemd marks the unit as started immediately, before it knows whether theexeceven worked. A typo in the binary path gives you a start job that succeeds, and thenMain process exited, code=exited, status=203/EXECa moment later.Type=execbehaves likesimple, except the start job waits until theexechas succeeded. That turns the typo above into an honest start failure. It needs systemd 240 or newer, which every supported distribution has. Prefer it tosimple.Type=forkingexpects the process fromExecStart=to fork a background daemon and then exit. systemd waits for the parent to exit, then looks for the real daemon. Give itPIDFile=. Without one,GuessMainPID=(on by default) works only when exactly one process is left in the cgroup. Leave two behind andMainPIDstays0.Type=notifymeans the service callssd_notify(3)and sendsREADY=1when it can serve traffic. It may also sendMAINPID=to hand systemd a different process to track.NotifyAccess=defaults tomain, so a notification sent by a child is ignored and the journal names the PID it came from.Type=oneshothas no lasting main process. The unit goes inactive as soon asExecStart=finishes, unless you setRemainAfterExit=yes.Restart=alwaysandRestart=on-successare refused here, with the messageService has Restart= set to either always or on-success, which isn't allowed for Type=oneshot services. Refusing.The other values, includingon-failure, are accepted.
Two Type=forking errors are worth memorising, because each leaves you with a unit that looks broken for no visible reason:
myapp.service: Can't open PID file /run/myapp.pid (yet?) after start: No such file or directory
myapp.service: New main PID 4711 does not belong to service, and PID file is not owned by root. Refusing.The first means the daemon writes its PID file somewhere else, or writes it later than systemd looks. The second means the PID file names a process outside the unit's cgroup, which systemd refuses to adopt, because a writable PID file would otherwise become a way to make systemd send signals to any process on the box.
ラッパースクリプトが子プロセスの終了を隠す理由
タイトルにある疑問を生む構成は次のとおりです。
#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
waitこの unit は Type=simple なので、メインプロセスは shell です。引数なしの wait は、すべての子プロセスが終了するまで戻りません。worker を kill しても、shell は web プロセスを待ち続けます。そのため shell は終了せず、MainPID も終了せず、Restart= も参照されません。cgroup 内のプロセスが 1 つ減るため、systemctl status は短くなったツリーを表示しますが、unit の状態は active (running) のままです。systemd はこのツリーの変化を監視していません。
同じ誤りの別の形は、より気付きにくいものです。
ExecStart=/bin/sh -c 'export APP_ENV=production; /usr/local/bin/myapp'メインプロセスは myapp ではなく shell です。systemctl stop になると、systemd はメインプロセスに SIGTERM を送信します。しかし、foreground の子プロセスを待っている shell は、そのシグナルを子プロセスへ渡しません。停止処理は既定値で 90 秒の TimeoutStopSec を使い切り、次のように終了します。
myapp.service: State 'stop-sigterm' timed out. Killing.
myapp.service: Killing process 4711 (myapp) with signal SIGKILL.修正方法は exec です。exec /usr/local/bin/myapp と記述すると、shell がプログラムに置き換わるため、MainPID はプログラムになり、シグナルもプログラムに届きます。さらによい方法は、shell を削除し、unit で Environment= または EnvironmentFile= を使用することです。-c 文字列にコマンドが 1 つしかない場合は、bash と dash がそのケースを直接の exec に最適化するため、この問題が表面化しない点に注意してください。文字列に 2 つ目のコマンドを追加すると、shell がプログラムの前段で動作し続けます。
テスト用 VPS で 2 分以内に再現する
上記の wrapper を /usr/local/bin/two-children.sh として保存し、chmod +x で実行可能にします。2 つのプログラムパスを sleep 3600 に置き換えます。Type=simple と Restart=on-failure で unit からこの wrapper を起動するように設定し、systemctl daemon-reload して起動します。systemd-cgls --unit two-children.service を実行し、shell と 2 つの子プロセスという 3 つの PID を確認します。sudo kill <pid> で子プロセスの 1 つを kill します。unit をもう一度確認します。ツリー内のプロセスは 1 つ減っていますが、状態は active (running) のままで、journal に新しい内容はありません。次に sudo kill -9 <shell pid> を実行します。unit は失敗し、KillMode=control-group が既定値であるため、残った子プロセスもクリーンアップされます。journal には Scheduled restart job, restart counter is at 1.
Restart=の全ての値と、alwaysよりon-failureが適する場合
Restart=には7つの値があり、それらを分ける基準は、何を正常終了とみなすかです。systemdは、終了コード0、SuccessExitStatus=に列挙されたコード、さらにシグナルSIGHUP、SIGINT、SIGTERM、SIGPIPEを正常終了として扱います。それ以外は、SIGKILLやSIGSEGVを含めて異常終了です。
noがデフォルトです。unit自身は再起動しません。そのため、Restart=行がないunitは最初のクラッシュで停止し、そのまま停止し続けます。on-successは、正常終了した場合にのみ再起動します。on-failureは、ゼロ以外の終了コード、異常なシグナル、起動または停止のタイムアウト、ウォッチドッグの期限切れで再起動します。on-abnormalは、異常なシグナル、タイムアウト、ウォッチドッグの期限切れで再起動しますが、通常のゼロ以外の終了コードでは再起動しません。on-abortは、異常なシグナル、つまりクラッシュの場合にのみ再起動します。on-watchdogは、WatchdogSec=の期限が切れた場合にのみ再起動します。alwaysは、正常終了してステータス0になった場合を含め、上記の全ての場合に再起動します。
長時間実行するデーモンには、on-failureが適切なデフォルトです。クラッシュから復旧できますが、意図的なexit 0はそのままにします。alwaysは、制御不能な理由で正常終了するプログラムに適しています。例えば、接続先が切断されたときに0を返すトンネルクライアントです。alwaysの問題は、バグを隠してしまうことです。サービスが起動し、壊れた設定ファイルを読み込み、エラーを記録して0で終了すると、無限にループします。唯一の兆候は、再起動カウンターが増え続けることです。
SuccessExitStatus=によって、正常終了と異常終了の境界を変更できます。Borgは警告で1、エラーで2を返すため、SuccessExitStatus=1のないバックアップunitは、読み取れないファイルを1つスキップするたびにfailedと判定されます。RestartPreventExitStatus=には、alwaysの場合でも再起動を阻止するコードを列挙します。これは、プログラムが「再起動してはならない」と伝えるための正しい方法です。RestartForceExitStatus=は逆の動作をします。バックアップジョブは再起動ループではなく、timerで起動するType=oneshot unitに入れるべきです。その場合は、スケジュールに従ってジョブを実行するserviceとtimerの組み合わせを構成例として使用します。
テスト時には注意が必要です。サービスを通常のkill <pid>で終了するとSIGTERMが送信されます。SIGTERMは正常終了の一覧に含まれるため、Restart=on-failureは正しく何もせず、設定が壊れていると判断することになります。代わりにkill -9 <pid>またはsystemctl kill -s SIGKILL myapp.serviceを使用してください。また、Restart=のどの値も、systemctl stopの後や、BindsTo=またはPartOf=の依存関係がなくなったためにunitが停止された場合には実行されません。停止ジョブは障害ではありません。
RestartSec と 100 ミリ秒のデフォルト
RestartSec= は、unit の停止から systemd が再度起動するまでの待機時間です。デフォルトは 100 ミリ秒です。unit に実際に読み込まれた値を確認します。
systemctl show -p RestartUSec -p StartLimitIntervalUSec -p StartLimitBurst myapp.serviceRestartUSec=100ms を設定していない unit では、RestartUSec=100ms と表示されます。このデフォルト値は、サービスが 1 回クラッシュして復旧する場合には問題ありません。しかし、サービスをまったく起動できない場合には適切ではありません。5 回の再起動が 0.5 秒未満で発生し、次に説明するレート制限に達するためです。データベース、マウント、ネットワークルートを待つサービスでは、RestartSec=5s 以上を設定してください。
2026 年 8 月時点では、systemd 254 以降で RestartSteps= と RestartMaxDelaySec= も使用できます。これらは、試行回数に応じて遅延を RestartSec= から上限値まで延ばします。Ubuntu 24.04 には systemd 255 が含まれており、これらを利用できます。Debian 12 には systemd 252 が含まれており、利用できません。依存先が長時間停止する可能性がある場合は、遅延を段階的に延ばす方法が適しています。
「start request repeated too quickly」の本当の意味
これは、systemd が任意に諦めたと受け取られがちな状態です。実際にはカウンターです。ルールは、ユニットが StartLimitBurst= 内に StartLimitIntervalSec= 回を超えて起動されると、systemd が再度の起動を拒否し、failed 状態にするというものです。デフォルトは 10 秒間に 5 回の起動です。
journal には次の順序で記録されます。
myapp.service: Scheduled restart job, restart counter is at 5.
myapp.service: Start request repeated too quickly.
myapp.service: Failed with result 'start-limit-hit'.
Failed to start myapp.service - My application.そして systemctl start には、すでに対処方法が示されます。
Job for myapp.service failed because start of the service was attempted too often. See "systemctl status myapp.service" and "journalctl -xeu myapp.service" for details. To force a start use "systemctl reset-failed myapp.service" followed by "systemctl start myapp.service" again.systemctl reset-failed myapp.service はカウンターと failed 状態をクリアします。それ以外の方法ではクリアされないため、単純な systemctl start は、これを実行するまで拒否され続けます。手動での起動も上限に数えられます。そのため、設定ファイルの編集中に焦って systemctl restart を何度か実行すると、クラッシュがまったくなくても上限に達することがあります。
誤解を招く点は、start-limit-hit がサービスの失敗理由を示さないことです。示すのは、短時間に繰り返し失敗したという事実だけです。本当の原因は、その前にある journal の行に記録されています。
両方の設定は [Unit] セクションに記述します。[Service] に記述した例も見つかります。古い systemd はこの場所を受け付けていたため、混乱が生じます。設定は [Unit] に記述し、systemctl show で systemd が読み込んだ値を確認してください。実際に適用されるのは、読み込まれた値だけです。
[Unit]
Description=My application
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=exec
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=10sこれにより、上限に達して停止するまで、5 分間に 5 回の起動を試行します。StartLimitIntervalSec=0 は上限を完全に無効化します。選択の意味を理解しておく必要があります。決して起動できないサービスが無期限に再試行し、そのたびに journal へ記録されるためです。マシン全体のデフォルト値は /etc/systemd/system.conf に DefaultStartLimitIntervalSec= と DefaultStartLimitBurst= として設定されています。
隣接する設定にも注意が必要です。StartLimitAction= は上限に達したときの動作を決め、reboot、reboot-force、poweroff などの値を指定できます。デフォルトは none です。この場合、ユニットは失敗状態になり、マシンの状態は変更されません。リモートの VPS では、poweroff を指定すると、プロバイダーのコンソールを開くまでサーバーが停止したままになります。
修正 1: 1 unit につき 1 プロセス
ほとんどの場合、これが答えです。2 つのプログラムを実行する必要がある場合は、2 つの unit を作成します。各 unit に実際のメインプロセス、実際の終了ステータス、独自の再起動ポリシーを持たせられます。ログ、リソース制限、再起動カウンターも分離できます。これは、午前 3 時の障害対応で必要になる構成です。
unit 間の関係は、shell script ではなく unit file に記述します。
After=は起動順序だけを指定します。障害については何も指定しません。Requires=は、この unit と同時に別の unit を起動し、別の unit が明示的に停止された場合はこの unit も停止します。BindsTo=はRequires=に加えて、重要な動作を指定します。クラッシュを含め、別の unit が何らかの理由で停止すると、この unit も停止します。After=と組み合わせてください。組み合わせない場合、順序は未定義です。PartOf=は停止と再起動を下位の unit に伝播します。そのため、systemctl restart myapp.targetを実行すると、PartOf=であるすべての unit に適用されます。Upholds=(systemd 249 以降。Ubuntu 22.04 以降)は、指定した unit の実行を維持します。停止すると、systemd が再度起動します。ほかの unit と同じ起動レート制限の対象になります。
API server なしでは決して実行せず、API が起動している間は systemd に実行を維持させる worker の例を示します。
# /etc/systemd/system/myapp-api.service
[Unit]
Description=myapp API server
Wants=network-online.target
After=network-online.target
Upholds=myapp-worker.service
[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp serve
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target# /etc/systemd/system/myapp-worker.service
[Unit]
Description=myapp background worker
BindsTo=myapp-api.service
After=myapp-api.service
StartLimitIntervalSec=120
StartLimitBurst=5
[Service]
Type=exec
User=myapp
ExecStart=/usr/local/bin/myapp worker
Restart=on-failure
RestartSec=5sworker には [Install] セクションがなく、手動で enable することもありません。API unit が Upholds= で worker を起動するため、実行するコマンドは systemctl enable --now myapp-api.service だけです。設定を再読み込みし、systemd が 2 つの unit をどのように処理したかを確認します。
sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/myapp-worker.service
systemctl list-dependencies myapp-api.servicesystemd-analyze verify は、file に問題がなければ何も出力しません。出力がある場合は問題です。通常は、記述したセクションで systemd が認識できない key、または存在しない unit への依存関係が原因です。
修正 2: Type=notify にすると、systemd は PID より多くの情報を把握できます
プログラムが systemd 通知プロトコルに対応している場合は、それを使用します。Type=notify により、サービスは準備完了時点を systemd に通知できます。これにより、起動順序を推測ではなく実際の状態に基づいて制御できます。また、サービスは MAINPID= を送信して、ランチャーではなく対象プロセスを systemd に指定できます。
WatchdogSec= は、手間をかける価値がある設定です。これを設定すると、サービスは少なくとも sd_notify(3) ごとに WATCHDOG=1 を送信する必要があります。メッセージが途絶えると、systemd はサービスを SIGABRT で終了し、failed として扱います。そのため、Restart=on-failure または Restart=on-watchdog によってサービスを復旧できます。実行中のまま停止したプロセスを再起動する、組み込みの方法はこれだけです。終了ステータスに基づくポリシーでは、この状態を検出できません。
[Service]
Type=notify
NotifyAccess=main
ExecStart=/usr/local/bin/myapp serve
WatchdogSec=30s
Restart=on-failure
RestartSec=5swatchdog のタイムアウトは、journal に myapp.service: Watchdog timeout (limit 30s)! として記録された後、プロセスが kill されたことが記録されます。一方、unit が activating (start) のまま TimeoutStartSec が期限切れになる場合、READY=1 は届いていません。原因は、プログラムがこのプロトコルに対応していないか、NotifyAccess=main が子プロセスから送信された通知を拒否していることです。この場合、journal には両方の PID が記録されます。
HTTP health endpoint を提供しているものの sd_notify に対応していないソフトウェアでは、現実的な選択肢は 2 つあります。小さな timer unit で endpoint をプローブし、systemctl restart を呼び出す方法です。もう 1 つは、コンテナランタイムにプローブを実行させる方法です。Compose の healthcheck と再起動の動作 は、この用途のためにあります。
ユニット内に supervisor を置く方法は、他に選択肢がない場合に限る
ソフトウェアによっては、分離できない launcher の背後で複数のプロセスをまとめて起動する構成になっています。その場合はユニット内で supervisor を実行し、systemd が supervisor を監視し、supervisor が他のすべてを監視する構成を受け入れます。これにより、再起動ポリシーが 2 つのファイルに分散します。
この構成でよくあるのが container runtime です。docker compose または podman のユニットは、まさにこのパターンです。コンテナごとの再起動ポリシーは Compose ファイルに記述し、systemd のユニットは runtime を起動した状態に保つだけにします。この構成であれば、ブート時に Compose スタックを起動するユニットに動作する構成を示しています。そこでは、なぜ Type=oneshot と RemainAfterExit=yes の組み合わせが通常は適切なのかも説明しています。
cgroup は引き続き有効です。supervisor が起動するすべてのプロセスはユニットの cgroup 内に留まるため、MemoryMax=、CPUQuota=、停止時のクリーンアップは、プロセスツリー全体を引き続き対象にできます。委譲されるのは再起動の判断だけです。
どの supervisor を選ぶ場合でも、外側のユニットに Restart=always を設定し、その内部に積極的な再起動ポリシーを設定することは、十分に検討せずに行わないでください。再起動ロジックが 2 層に分かれ、それぞれに独自のバックオフがあると、サービスが数分間フラッピングし、なぜそうなったのか journal から分からなくなります。
ExitType=cgroup は「任意のプロセスが終了したら再起動する」という意味ではありません
ExitType=(systemd 250 以降。Ubuntu 24.04 と Debian 12 にはどちらも含まれています)は、この問題を検索したときに見つかる設定です。しかし、名前から想像する動作とは逆です。デフォルトの ExitType=main では、メインプロセスが終了するとサービスは停止したとみなされます。ExitType=cgroup では、cgroup 内の最後のプロセスが終了するまで、サービスは実行中とみなされます。
つまり、ExitType=cgroup を設定すると、1 つのプロセスの終了に対するユニットの検知は鈍くなります。鋭くなるわけではありません。実際のワーカープロセスを fork し、PID ファイルを書き込まずに親プロセスが終了するプログラムでは、この設定が適しています。この場合、Type=forking ではデーモンを検出できません。しかし、ここで説明している障害には適していません。
「cgroup 内の任意のプロセスが終了したらユニットを再起動する」という意味の Restart= の値はありません。この動作が必要な場合は、1 ユニットにつき 1 プロセスにする必要があります。プログラムを分割できず、ラッパースクリプトを管理している場合は、最初の子プロセスが終了するとすぐに戻る wait -n が最も近い方法です。
#!/bin/bash
/usr/local/bin/myapp-web &
/usr/local/bin/myapp-worker &
wait -n
exit 1これで子プロセスが終了すると、ラッパーも非ゼロのステータスで終了するため、Restart=on-failure が動作します。ただし、これは妥協策であり、解決策ではありません。2 つのプログラムで再起動カウンターを 1 つ共有し、ログも 1 つのストリームにまとまります。また、障害が発生した側だけを個別に再起動することもできません。
実際に発生したことを確認する方法
次の 4 つのコマンドを、この順番で実行します。
systemctl status myapp.service
systemd-cgls --unit myapp.service
systemctl show -p MainPID -p NRestarts -p Result -p ExecMainStatus myapp.service
journalctl -u myapp.service -b -o short-precisesystemctl status は、状態、メイン PID、cgroup ツリーを 1 画面に表示します。正常な unit では、Active: active (running) と表示され、Main PID: 行には想定したプロセス名が示されます。画面下部のツリーに認識できないプロセスがある場合、または想定したプロセスがない場合は、その時点で原因を特定できます。
systemd-cgls --unit は同じツリーを省略せずに表示します。unit が少数を超えるプロセスを保持するようになると、この違いが重要になります。
systemctl show は、機械で処理できる形式の情報を表示します。NRestarts= は再起動回数です。サービスが 40 回再起動したのか、ブート後から継続して稼働しているのかを、最も速く判別できます。Result= には最後に発生した失敗の理由が格納されます。値は exit-code、signal、timeout、oom-kill、watchdog、start-limit-hit のいずれかです。ExecMainStatus= は、最後に実行されたメインプロセスの生の終了ステータスです。
journal には一連の経過が記録されています。検索する行は次の 3 つです。
myapp.service: Main process exited, code=exited, status=1/FAILURE
myapp.service: Failed with result 'exit-code'.
myapp.service: Scheduled restart job, restart counter is at 1.code=exited, status=N は、プログラムが N を返すことを選択したことを示します。原因はプログラムまたはその設定にあります。code=killed, signal=SEGV は、プログラムがクラッシュしたことを示します。code=killed, signal=TERM は通常、別のプロセスが停止を要求したことを示します。これは失敗ではなく、Restart=on-failure も発生しません。code=dumped は、プログラムが core ファイルを残したことを示します。systemd-coredump がインストールされていれば、coredumpctl list でその内容を確認できます。
複数のマシンを対象にする場合、定期的に収集する価値があるのは NRestarts です。毎日カウンターが増える unit は、誰も気付いていなくても毎日失敗しています。2 台または 3 台を超えるサーバーを管理するようになったら、全サーバーで 1 つのコマンドを一貫して実行する方法により、推測ではなくレポートとして確認できます。
FAQ
systemctl がプロセスの終了後もサービスを active と表示するのはなぜですか?
systemd は、サービス unit ごとに 1 つのプロセス、つまりメインプロセスを追跡します。Restart= が読み取るのも、そのプロセスの終了ステータスだけです。unit が起動したその他のプロセスは同じ cgroup に属し、unit の停止時には systemd によって終了されます。ただし、終了したかどうかを個別に監視することはありません。systemctl show -p MainPID myapp.service を実行し、その番号を systemd-cgls --unit myapp.service と比較してください。終了したプロセスがツリー内に表示されるものの、MainPID ではない場合、systemd は設計どおりに動作しています。対策は、1 unit につき 1 プロセスとし、unit 間の関係を BindsTo= と Upholds= として定義することです。
「start request repeated too quickly」とはどういう意味ですか?
unit が StartLimitIntervalSec= の間に StartLimitBurst= 回を超えて起動されたため、systemd が起動を停止したという意味です。既定値は 10 秒間に 5 回の起動です。これはレート制限であり、サービスが失敗した理由は示しません。そのため、直前の journal の行を確認してください。systemctl reset-failed myapp.service で状態をクリアしてから、根本的な障害を修正します。サービスが起動の遅い何かを待機する場合は、RestartSec= を増やしてください。既定の間隔は 100 ミリ秒なので、5 回の試行を 1 秒未満で使い切るためです。
Restart=always と Restart=on-failure のどちらを使用すべきですか?
ほとんどの場合は on-failure を使用してください。クラッシュ、ゼロ以外の終了、タイムアウト、watchdog の作動で再起動し、意図的な exit 0 はそのままにします。always は、プログラムが制御できない理由で正常終了する場合だけ使用してください。たとえば、ピアとの切断時に 0 を返すクライアントなどです。always の問題は、壊れた設定を読み込み、エラーを 1 件記録して 0 で終了するサービスが無限にループすることです。目に見える症状は、systemctl show 内の NRestarts が増え続けることだけです。
手動でプロセスを kill しても再起動されないのはなぜですか?
systemd は SIGHUP、SIGINT、SIGTERM、SIGPIPE を正常終了として扱い、通常の kill <pid> は SIGTERM を送信するためです。Restart=on-failure では、正常終了は失敗ではありません。そのため再起動されず、設定が壊れているように見えても、実際には壊れていません。kill -9 <pid> または systemctl kill -s SIGKILL myapp.service でテストしてください。これは異常終了となり、再起動ポリシーが適用されます。同じ規則により、systemctl stop が再起動ポリシーと競合することもありません。
StartLimitIntervalSec と StartLimitBurst はどこに記述しますか?
[Unit] セクションに記述します。以前の資料や古い systemd バージョンでは [Service] に記述されているため、コピーした例同士で内容が異なる場合があります。使用中のバージョンがどちらを有効とするか、推測しないでください。systemctl daemon-reload の後に systemctl show -p StartLimitBurst -p StartLimitIntervalUSec myapp.service で systemd が読み込んだ内容を確認し、その値を正しいものとして扱います。systemd-analyze verify /etc/systemd/system/myapp.service は systemd が認識しないキーを検出します。ファイルに問題がなければ何も出力しません。