マルチサービス起動レディネス:running、ready と依存/失敗/再起動の伝播
技術ノート
公開 / DavidHLPL
このページは一つの質問に答える:Compose や systemd で依存関係を宣言したとき、実際には何が保証されるのか。核心結論:オーケストレーターは宣言された condition(service_started / service_healthy / service_completed_successfully)に基づく起動ゲートしか適用せず、業務レディネスを自動で保証しない。順序付け、依存、失敗と再起動の伝播は直交する四つの意味論であり、分けて判断しなければならない。
コアメカニズム
1. 六つの概念を区別する決定表
| 概念 | 意味 | 判定/トリガー | 重要な境界 |
|---|---|---|---|
| running(プロセス起動) | オーケストレーターはコンテナが「running」であることだけを待つ | コンテナ状態が running;ポートバインディングはこの層に属する | アプリの ready を待たない。ポートがリスンされていても ready の証拠にならない |
| ready(業務レディネス) | ユーザーが明示的に定義しなければならない | healthcheck が通るか、ワンショットタスクが完了する(service_completed_successfully、v2.20.0+) |
プローブはユーザーコマンド。オーケストレーターはアプリ内状態を検証しない |
| ordering(順序付け) | 前後関係だけを決め、依存は作らない | Compose の短い構文 depends_on: [db](service_started と同等);systemd の After=/Before= |
短い構文は起動順序しか保証しない。After= は Requires=/Wants= と直交 |
| requirement(依存確立) | 「依存が満たされなければ起動しない」と宣言 | 長い構文 condition: service_healthy;systemd の Requires=/Wants= |
Wants= は弱い形式で、失敗しても全体トランザクションに影響しない |
| failure propagation(失敗の伝播) | 依存が不健康なら依存側は起動しない | up --wait が失敗終了;--abort-on-container-failure で全停止;systemd の Requires=+After= |
--abort-on-container-failure は -d と非互換、--wait とは排他 |
| restart propagation(再起動の伝播) | 明示操作で起きた再起動だけが伝播する | depends_on.restart: true;systemd の Requires= による明示 stop/restart |
コンテナ実行時の自動再起動は伝播しない。systemd の予期しない失敗には BindsTo= が必要 |
2. 重要な意味論の切り分け
- running ≠ ready:Compose は起動時にコンテナの ready を待たず、running だけを待つ(docker/docs の固定ソース原文)。レディネスはユーザーが明示定義しなければならない:
condition: service_healthy(healthcheckで健康を定義)またはservice_completed_successfully(ワンショットタスクの完了)。 - 順序 ≠ 依存(systemd):
After=/Before=は順序依存だけを設定し、Requires=/Wants=とは直交する(v261.2 マニュアル原文 “independent and orthogonal”)。公式の一般的な書き方は同一ユニットをAfter=とWants=の両方に入れる。service ユニットは「設定されたすべての起動コマンドが呼び出された(成否に関わらず、ExecStartPost=を含む)」時点で完了とみなされる——起動済みはサービス可能を意味しない。 - healthcheck はユーザー定義のレディネスプローブ:spec 原文は、コンテナが healthy かを判定する check を宣言するもので、Dockerfile の
HEALTHCHECKと同じ働きだと述べる。プローブが何を測るかは完全にユーザーコマンドが決め、オーケストレーターは業務レベルの推論をしない。testはNONE/CMD/CMD-SHELLをサポートし、disable: trueで無効化できる。 - ポート占有 ≠ レディネス:
portsは公開マッピングを定義するだけ(HOST 未指定時は全インターフェース0.0.0.0にバインド)で、コンテナ内のアプリ状態には関わらない。ポートがリスンされているのは「プロセス起動」層の信号であり、ready の証拠にはならない。
3. 失敗の伝播と再起動の伝播
- Compose の失敗:spec は
service_healthyを付けた依存が先に healthy になってから依存側を起動することを保証する。up --wait(暗黙の detached モード)は依存が不健康なとき失敗終了する(raw 実測:exit code 1、出力 “dependency failed start: container … is unhealthy”、app は作成のみで未起動)。--abort-on-container-failureはいずれかのコンテナが失敗終了したら全部を停止し、-dと非互換で--waitとは排他。 - Compose の再起動:
depends_on.restart: trueは「明示的な Compose 操作」(docker compose restart/ update など)で起きた再起動だけを伝播する。コンテナ実行時レイヤーの自動再起動(restart: always/on-failure/unless-stopped)はdepends_onでは伝播しない。 - systemd:
Requires=+After=の場合、依存のアクティベーション失敗で本ユニットの起動が妨げられる。Wants=は弱い形式で、失敗してもトランザクション全体の有効性に影響しない。Requires=は依存が明示 stop/restart されたときだけ連動し、予期しない失敗では伝播しない。「依存が予期せず停止したら連動停止」をカバーするにはBindsTo=を使う。
最小組み合わせの例
Compose:healthcheck + depends_on.condition: service_healthy
services:
app:
image: alpine:3.19
command: ["sh", "-c", "echo APP_STARTED; sleep 60"]
depends_on:
db:
condition: service_healthy
restart: true
db:
image: redis:7.2-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
retries: 5
start_period: 10s
timeout: 5s
選定の要点(訂正 raw の最小実験より):固定公式イメージ、ビルド不要、資格情報不要、ポートなし。app は healthcheck を定義しないので、up --wait に対しては running で満足し、db の redis-cli ping が通った後で app が起動する。healthcheck のプローブコマンドが ready の定義を決める。別のコマンドに変えれば別のレディネス基準になる。
systemd:After= と Wants=/Requires= の組み合わせ境界
[Unit]
After=db.service
Wants=db.service
After=は順序だけを決める:db が先に起動するが、db の起動失敗は本ユニットに影響しない。Wants=をRequires=に変える:db のアクティベーション失敗(かつAfter=が存在する場合)で本ユニットは起動しない。db が明示 stop/restart されると本ユニットも連動 stop/restart される。- 境界の確認:
After=だけならRequires=/Wants=は空。Requires=だけでAfter=がなければ起動は妨げられない。予期しない失敗の伝播にはBindsTo=が必要。
最小検証
まず上の匿名 fixture を専用の一時ディレクトリの compose.yaml に保存する。既存の開発・本番 Compose プロジェクトのディレクトリに以下のコマンドをコピーしないこと。restart はサービスを中断し、--abort-on-container-failure はその隔離 project の全コンテナを停止し、down はそのコンテナとネットワークを停止・削除する。
fixture="$(pwd)/compose.yaml" # 現在のディレクトリにはこの匿名 fixture だけを置く
project="readiness-contract-$$" # この shell 内で一意。以降のコマンドでも同じ値を使う
printf 'Copy these values to the scenario terminal: fixture=%q project=%q\n' "$fixture" "$project"
docker compose -f "$fixture" -p "$project" config -q
docker compose -f "$fixture" -p "$project" up -d --wait --wait-timeout 30
docker compose -f "$fixture" -p "$project" ps
docker inspect --format '{{json .State.Health}}' \
"$(docker compose -f "$fixture" -p "$project" ps -q db)"
# 警告:restart は db を中断する。depends_on.restart: true のとき app も再起動する。
docker compose -f "$fixture" -p "$project" restart db
# 警告:down はこの隔離 project のコンテナとネットワークを停止・削除する。--volumes は付けないこと。
docker compose -f "$fixture" -p "$project" down
シナリオコマンドは同じ fixture/project を使い、メインフローと別に実行する。別ターミナルでは、最初のブロックが表示した実際の値を次の二行に入れる。Compose に現在のディレクトリから project を自動推測させてはならない:
fixture="<FIXTURE_PATH>" # 最初のブロックが表示した fixture 値に置き換える
project="<PROJECT_NAME>" # 最初のブロックが表示した project 値に置き換える
# イベントストリームは Ctrl-C までブロックする。別ターミナルで同じ -f/-p で up を実行する。
docker compose -f "$fixture" -p "$project" events
# 失敗ゲート:隔離 fixture 内でのみ、db の healthcheck.test を恒常 exit 1 に変更する。
docker compose -f "$fixture" -p "$project" up -d --wait --wait-timeout 8
# フォアグラウンドの失敗伝播は、いずれかのコンテナが失敗するとこの隔離 project の全コンテナを停止する。
# これは -d と非互換で、--wait とは排他。非ゼロで終了するサービスを含む別の隔離 fixture が必要。
docker compose -f "$fixture" -p "$project" up --abort-on-container-failure
systemctl show <unit> -p After -p Before -p Requires -p Wants # After= と Requires= は直交するフィールド
systemd-analyze verify <unit>.service # ユニットファイルを検証する
決定表に照らして検証結果を解釈する:ps の healthy 列はプローブが通ったことだけを証明し、業務レディネスは証明しない。events の health_status シーケンスで依存順を確認する。systemctl show で After= だけがあり Requires=/Wants= が空なら、順序だけがあって依存はない。
オーケストレーターが代替できない部分
- migration はオーケストレーターの責務にない:spec で唯一の「ワンショットタスク」順序プリミティブは
condition: service_completed_successfully(Compose v2.20.0+ と注記)で、順序付けだけを担う。移行ロジック自体はユーザーが service/コマンドとして定義しなければならない。schema 変更、データバックフィルなどの業務レディネスはオーケストレーターの責務にない。 - 業務レディネスは healthcheck の意味論にない:キャッシュウォームアップ、整合性、業務アサーションなどのアプリ内状態をオーケストレーターは検証しない。プローブはユーザー定義の任意コマンドにすぎない。
- したがって「移行完了後に app を起動する」のような要件は、ユーザーが移行をワンショット service にし、app に
service_completed_successfullyの依存を宣言するのが正しい書き方であり、オーケストレーターの自動推論に頼るべきではない。
適用条件
- 「自分のオーケストレーション宣言は何を保証するのか」を判断するときは、決定表に沿って項目ごとに照合する。特に順序と依存、失敗と再起動の伝播は混同されがちなので区別する。
- 「healthy になってから起動」が必要なら長い構文
condition: service_healthyを使う。前後関係だけなら短い構文で十分(service_startedと同等)。 - systemd で「依存が失敗したら起動しない」が必要なら
Requires=+After=を使う。予期しない失敗も連動させたい場合にだけBindsTo=を導入する。
非適用とリスク
- 本ページはまず
multi-service-readiness-contractに保存された compose-spec/systemd の固定点に基づく。続いてmulti-service-readiness-contract-correctionが docker/docs と Compose CLI の固定ソースとワンショット Redis/Alpine 実験を独立に補い、multi-service-readiness-safety-correctionが-f/-pのプロジェクト隔離とdownの削除範囲を補う。service_completed_successfullyは Compose v2.20.0+ が必要。restartフィールドの v2.17.0 以前の意味論は未カバー。 - ポート占有とレディネスの分離は仕様からの導出であり、「ポートはバインド済みだがアプリは未準備」の専用実験は行っていない。
--waitの失敗、healthy 後の起動、明示 restart の伝播は訂正 raw の最小 fixture で観測した。 - 未カバー:swarm mode での
depends_onの挙動差、systemd のRequisite=/PartOf=/Conflicts=の完全な意味論。
証拠と不確実性
- 情報源の事実:
multi-service-readiness-contractは最初の Compose/systemd 仕様契約と匿名 PostgreSQL 例を保存する。multi-service-readiness-contract-correctionは後の固定ソースとワンショット Redis/Alpine 実験を独立に保存し、service_healthy起動ゲート、--wait失敗終了、明示 restart の伝播を観測した。multi-service-readiness-safety-correctionは Compose v5.4.0 の project flags とdownの削除境界を固定する。 - 仕様からの導出:ポート占有 ≠ レディネス。オーケストレーターはプロセス/ポートから業務状態を自動推論できない。
- 未検証項目:「ポートはバインド済みだがアプリは未準備」の専用実機挙動は観測していない。systemd 側の
Requires=/BindsTo=の伝播も実ホストで実行しておらず、v261.2 マニュアルの意味論を引用するだけに留まる。
関連ページ
出典と関連文書
- multi-service-readiness-contract
- multi-service-readiness-contract-correction
- multi-service-readiness-safety-correction
- MySQL パフォーマンス切り分け:現象 → 指標 → 層 → 安全な緩和
- データベース Schema ドリフト:history、schema、query の三ビューで特定する
- Testcontainers 1.20.6:Docker API バージョンを先に切り分けてから、daemon の利用不可を判断する
- Dubbo + Nacos:登録名と group/version を先に確認してから、本物の smoke test を実行する
- マイクロサービスデータ所有権:領域 owner を先に決めてから、DB 分割と移行を考える
研究記録
- 元の文書: src/content/note/ja/multi-service-readiness.md
- 出典リビジョン: f49953fb0d4bd52a39bf81167b0c4b7dd7d78a64
- 証拠の状態: 有効