プロセスの常駐設計 — 起動スクリプトで回すかsystemdに任せるか

サーバー再起動後にサービスが自動で立ち上がらず、数時間後に監視アラートで気づく——そういった事態は現場で珍しくない。根本には「プロセス常駐をどの仕組みに委ねるか」という設計判断の曖昧さがある。起動スクリプトで回すかsystemdに任せるかは単なる好みではなく、依存関係・再起動条件・異常検知の責任をどの層に置くかという問いだ。この記事では常駐プロセス設計の判断基準と、陥りやすい境界を整理する。

再起動後にサービスが上がらず気づかれない構造

cron・rc.local・手動起動が積み重なる経緯

構築から数年が経つサーバーでは、rc.localに書かれた起動コマンドとsystemdユニットが混在し、さらにcronの@rebootエントリが重複しているという状態をよく見かける。これは一人の担当者が意図して作った構成ではなく、引き継ぎのたびに「触らないほうが安全」という判断が積み重なった結果だ。30台規模のサーバー群でも、こうした遺物が1〜2台に残っているだけで、再起動後の動作が他のホストと異なる状況を招く。特に問題になるのは、起動スクリプトがpidファイルでデーモン管理をしている場合で、再起動前のpidファイルが残ったままだとスクリプトが「すでに起動中」と判断してサービスを上げない。この構造は手動確認では気づきにくく、実際に負荷がかかるまで発覚しないことが多い。

「起動を確認した」と「起動が保証されている」の違い

systemd管理下にあるユニットには「enabled(自動起動設定あり)」と「active(現在起動中)」という二つの独立した状態がある。enabled設定を入れずにsystemctl startだけで動かしているサービスは、再起動後に自動で上がらない。構築作業中に「動作確認」として実施したsystemctl startが、そのまま本番構成として残るケースが現場で繰り返されている。この判断を設計として明示するには、AnsibleなどのプロビジョニングツールでEnabledとStartedを分けて記述し、意図的に両方をコードに落とすことが有効だ。「現在起動している状態」と「再起動後も起動する設定」を別の責任として扱うことが、常駐設計の出発点になる。

監視と組み合わせることで見えにくくなる盲点

プロセス監視ツールがPID存在確認だけを行っている場合、ExecStartPreの前処理が失敗してサービスが立ち上がっていない状態でも「異常なし」と報告することがある。systemdはこの場合にユニットをfailed状態として記録するが、外部監視がjournaldのログを参照していなければ検知が遅れる。実際に20台以上が稼働する環境で、特定ホストのみ再起動後にfailed状態になっていたが、プロセス監視のスコープ外だったため2日間気づかれなかった事例がある。failed状態の検知をsystemdのユニット状態をメトリクスとして収集する仕組みに委ねるかどうかを、設計段階で決めておく必要がある。

systemdに任せる範囲と自前で持つ範囲の判断

systemdが受け持ちやすい責任と苦手な処理

systemdは起動順序・依存解決・ログ集約・プロセス再起動・cgroupによるリソース管理といった責任を一元的に持てる。特にソケットアクティベーションを使えば、クライアントが接続するまでサービス本体の起動を遅延させる構成も実現できる。複数サービスが同時起動するサーバーで、起動時間を20〜30秒短縮できた事例も報告されている。一方で苦手とするのは、起動直前に外部APIのレスポンスを参照して環境変数を動的に組み立てるような複雑な前処理だ。EnvironmentFileで静的な環境変数を渡す仕組みは用意されているが、動的生成はExecStartPreでスクリプトを呼ぶか別の仕組みに切り出す必要がある。

起動スクリプトが正当化される条件

複数のデーモンを連動して起動・停止する必要がある場合、または起動ロジックに複雑な条件分岐が含まれる場合は、起動スクリプトをsystemdのExecStartから呼ぶ混合パターンが合理的な選択肢になる。この場合、スクリプト自体はsystemdユニット管理下に置くことで、再起動・ログ収集・依存解決の責任はsystemdが担い、ビジネスロジックに近い起動手順はスクリプト側で保持できる。判断の基準は「再起動・ログ・依存の三つをsystemdに委ねられるか」だ。この三つをスクリプト側で自前実装しようとすると、supervisorやdaemontoolsといった別のプロセス管理ツールと設計思想が重複し、二重管理の問題が生まれ始める。

既存スクリプト資産を活かす段階的移行の設計

長年運用されてきた起動スクリプトを一気にsystemdユニットへ移行するコストが高い場合、ExecStart=でスクリプトを呼ぶ薄いラッパーユニットを作ることで移行を段階化できる。ラッパーユニットでWantedBy=multi-user.targetを設定してsystemctl enableを通せば、少なくとも「再起動後の自動起動」という最重要課題だけはsystemdに委ねられる。ただしスクリプトがバックグラウンドにフォークするデーモンを起動する場合はType=forkingの設定とPIDFileの指定が必要になる。ここを誤るとsystemdがプロセスの死活を正しく追跡できなくなるため、この点だけは丁寧に検証しておく必要がある。

比較表:自前起動スクリプト/systemdユニット/コンテナのプロセス管理

観点 自前起動スクリプト systemdユニット コンテナ(Docker/Podman)
自動起動設定の明示性 低(cron @reboot・rc.local等に分散しやすい) 高(enabledフラグをコードで管理できる) 高(restart policyをComposeファイルでコード化)
依存・起動順序の表現 スクリプト内に埋め込み・暗黙的になりがち After=/Requires=/Wants=で宣言的に記述 depends_on+healthcheck条件で表現
異常終了時の再起動 ループ・trap等の自前実装が必要 Restart=設定とStartLimitで細かく制御 restart policy(always/on-failure等)
ログの集約 ファイルまたはsyslogへの個別実装 journaldに自動集約・構造化ログ対応 コンテナランタイムのログドライバに委任
複雑な前処理の柔軟性 高(シェルで自由に記述できる) 中(ExecStartPre/ExecStartPostで補完) 低〜中(entrypointスクリプトで対応)
cgroup・リソース制限 なし(ulimit等を別途設定) CPUQuota/MemoryLimit等でユニット単位管理 –cpus/–memory等で管理
設定変更の適用手順 スクリプト修正後に手動で再起動 daemon-reload後にrestartで適用 イメージ再ビルドまたはCompose再起動

依存関係と起動順序をどこで表現するか

After=とRequires=とWants=の責任の違い

After=はあくまで「起動順序」を表すだけで、依存関係を強制するものではない。After=postgresql.serviceと記述しても、PostgreSQLが起動していなければアプリが終了するという強制力はない。強依存が必要な場合はRequires=を使うが、依存先が停止するとこのユニット自身も停止するため、計画的なメンテナンスで思わぬ影響範囲が生まれることがある。実際の構築では、Wants=で弱依存として宣言し、アプリ側の起動ロジックで接続リトライを実装するパターンのほうが柔軟性が高い。「systemdの依存表現で起動順を制御し、アプリの接続ロジックでサービス可用性を担保する」という役割分担を設計として明文化しておくと、チーム間の認識がずれにくくなる。

ネットワークが「使える状態」をどう定義するか

network.targetはネットワーク設定が完了したことを示すが、実際のインターフェースが通信できる状態になっているかは保証しない。外部ホストへの接続が必要なサービスはnetwork-online.targetへの依存が必要で、さらにDNS解決が必要な場合はnss-lookup.targetも考慮に入れる必要がある。この判断を誤った結果、再起動直後にDNS参照が失敗してサービスがfailed状態になり、その後のRestartによる再起動では問題なく立ち上がるという非再現性の高い挙動を引き起こす事例は少なくない。起動時間が10秒程度伸びたとしてもnetwork-online.targetを待つ設計のほうが、デバッグコストを考えると合理的な判断になることが多い。

データベースの「起動完了待ち」問題をどう設計するか

After=postgresql.serviceはPostgreSQLプロセスが起動したことを意味するが、実際に接続を受け付ける状態かは別の問いだ。特に初期化処理や自動バキューム実行中は接続拒否が発生することがある。この問題に対するsystemdの回答はType=notifyを使ったサービス側からの準備完了通知だが、既存のソフトウェアがこれを実装していないケースも多い。現実的な対処として、ExecStartPreにポート疎通確認や特定クエリへの応答確認をループで行うスクリプトを置く設計が選ばれる。タイムアウトを60〜120秒程度に設定し、それを超えたらユニットをfailed状態にする境界を明示しておくことが、設計として重要な判断になる。

異常終了時の再起動条件と止める見極めをどう置くか

Restart=の選択とexit codeによる制御

Restart=alwaysは「どんな終了理由でも再起動する」設定で、設定ファイル読み込みエラーや権限エラーなど再起動しても解決しない原因でも起動を繰り返す。Restart=on-failureはexit code 0以外の終了時のみ再起動する設定で、正常終了(意図的なシャットダウン等)では再起動しない。設計の観点では、再起動すれば回復可能なもの(一時的なネットワーク断・メモリ不足)と、再起動しても意味がないもの(設定ミス・プログラムのパニック)を区別し、後者はRestartPreventExitStatusで特定のexit codeを再起動対象から除外する判断が有効だ。この区別をユニットファイルに記述することで、障害時の挙動が予測しやすくなる。

バックオフとリトライ上限の設計

RestartSecは再起動前の待機時間を設定する。デフォルト値は短すぎて、問題が残っている状態で高速ループを引き起こすため、少なくとも5〜10秒に設定することが推奨される。StartLimitBurstとStartLimitIntervalの組み合わせでリトライ上限を設け、「一定期間内に規定回数の起動試行が失敗したらfailed状態に移行する」という境界を設計として明示する。failed状態に移行したユニットは手動でのリセット操作をしない限り自動再起動しない。この「人間の介入を要求する境界」を設計に組み込むことが、後述する「壊れたまま動き続ける」問題の防止につながる。

監視・アラートとsystemdの責任分担

systemdが再起動を試みている間、外部監視はプロセス存在確認で「正常」と判断し続けることがある。この状態を見落とさないためには、systemd自身のユニット状態(active/activating/failed)をメトリクスとして収集する監視設計が必要だ。アラート設計の観点では、「再起動が発生した」という通知と「failed状態になった」という通知を別々のアラートとして扱い、前者を情報レベル・後者を緊急レベルとする段階的設計が現場では機能しやすい。サービス数が増えるほど、どのユニットを監視対象とするかの選別も設計上の重要な判断になる。

再起動を効かせすぎて壊れたまま動き続ける境界

Restart=alwaysが引き起こすゾンビ再起動

設定ファイルが壊れた状態でRestart=alwaysを設定したサービスは、再起動のたびにエラーログを吐いてすぐ終了し、またすぐに再起動するループを繰り返す。StartLimitが設定されていなければこのループは無限に続き、journaldにログが大量書き込まれてディスクを圧迫する。100GBを超えるストレージであっても、高頻度ループが続けば数時間でディスクフルになり得る。この状況はプロセス監視では「プロセスが存在する瞬間がある」ためにアラートが上がらないことがあり、実際には機能していないサービスが外から見えにくい状態を生む。StartLimitBurstによる上限設定は、こうしたゾンビループを止める最初の防御ラインになる。

プロセスは生きているが機能していない状態

再起動条件の設計よりも難しいのは「プロセスは起動しているが、リクエストを処理できない」状態の検知だ。デッドロック・メモリリーク末期・接続プールの枯渇などは、プロセス自体は生存しているためsystemdのプロセス管理では検知できない。この層の異常は、ヘルスチェックエンドポイントへの定期的なHTTPリクエストや、DBコネクションの疎通確認を外部監視が行う設計でカバーする必要がある。WatchdogSec=を使ったウォッチドッグ機能も選択肢だが、サービス側での実装が必要であり、既存サービスへの導入可否は個別に判断する。手を動かす具体的な実装方法はセミナーや専門書で確認してほしい。

止まるべきときに止まれる設計

再起動を設計するのと同じ重みで、「グレースフルに停止できる」設計を検討する必要がある。systemctl stopで送られるSIGTERMに対してサービスが適切に後処理を行い、TimeoutStopSec以内に終了することが保証されていなければ、SIGKILLで強制終了されてデータが不整合になるリスクがある。デプロイや計画停止のたびにこのリスクにさらされることになるため、設計段階で「止め方」を定義しておくことは欠かせない。KillMode=processを設定することで、メインプロセスのみにシグナルを送り、子プロセスの後処理を待つ制御が可能になる。再起動設計がどれだけ丁寧でも、停止設計が欠けていれば運用上の穴になり得る。

常駐プロセス設計チェックリスト

設計レビューや引き継ぎの際に確認すべき判断ポイントを以下に示す。「YES」が揃っていない項目が設計の抜け穴になる。

  • 全サービスのsystemdユニットでenabled設定が明示されており、プロビジョニングコードに記録されているか
  • 起動スクリプトを使用している場合、systemdのExecStartから呼ぶ構成になっているか(rc.local・cron @rebootへの依存を残していないか)
  • After=/Wants=/Requires=の使い分けが意図に基づいており、暗黙の起動順依存がないか
  • ネットワーク依存があるサービスはnetwork.targetではなくnetwork-online.targetを参照しているか
  • データベース等の外部サービス依存がある場合、ExecStartPreで接続確認とタイムアウト上限(60〜120秒程度)を定義しているか
  • Restart=の設定が「再起動で回復しない失敗」と「回復可能な失敗」を区別しているか
  • StartLimitBurst/StartLimitIntervalで無限再起動ループを防ぐ上限が設定されているか
  • RestartSecが5秒以上に設定されており、高速再起動ループを起こさない設定になっているか
  • プロセス生存だけでなく機能稼働を確認するヘルスチェックが外部監視に含まれているか
  • failed状態のユニットを検知する監視設計(ユニット状態の定期収集等)があるか
  • TimeoutStopSec以内にグレースフルシャットダウンできることを検証済みか
  • コンテナ環境を使う場合、restart policyとhealthcheckの設定がCompose等にコードとして記録されているか

▶ 手を動かして本番まで到達する

その設計判断を、RHEL10の本番環境で手を動かして固めませんか

ここまでの設計判断は、読めば腹に落ちます。ですが「自分の手で本番まで通しきる自信」は、それとは別の話になります。

最大6名の少人数で、構築実務者の到達点まで引き上げます。座学で終わらせず、本番相当の環境で判断を検証する2日間です。

systemdプロセス管理の設計を実装で確かめる上級Linuxハンズオンへ →

Linuxセミナー上級編(RHEL10)|最大6名の少人数ハンズオン