コンテナは生きているのにサービスは死んでいる
一言でいうと
プロセスが生きていることと、リクエストを処理できることは、別の事実です。ヘルスチェックは、その2つの間の隔たりを埋め、再起動ポリシーは、失敗したときに何をするかを決めます。
なぜ必要なのか
docker psにUp 3 hoursと表示されています。ところが、ユーザーは500を受け取っています。プロセスは生きていますが、DBコネクションプールが枯渇しているか、スレッドがデッドロックしているか、ディスクが満杯で書き込みができない状態です。
Dockerが知っているのは、PID 1がまだ死んでいないという事実1つだけです。それだけで「サービス中」と判断すると、障害を何時間も見逃します。
どう動くのか
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -fsS http://localhost:8080/healthz || exit 1
| オプション | 意味 | 誤って設定すると |
|---|---|---|
interval |
検査の間隔 | 短すぎると負荷が増え、長いと検知が遅れます |
timeout |
1回の検査の制限時間 | 短すぎると、正常なのに失敗と判定されます |
start-period |
起動の猶予。この区間の失敗はリトライに数えません | ないと、起動の遅いアプリが死に続けます |
retries |
連続失敗の許容回数 | 1だと、一瞬の遅延でもunhealthyになります |
状態はstarting → healthy / unhealthyと移ります。docker inspect --format '{{.State.Health.Status}}'で確認できます。
重要な落とし穴: Docker単独では、unhealthyになってもコンテナを再起動しません。状態を表示するだけです。再起動は、オーケストレーター(Composeのdepends_on: condition: service_healthy、Swarm、Kubernetesのliveness probe)が行います。ヘルスチェックを入れたから勝手に復活するだろう、と信じてはいけません。
ヘルスチェックのエンドポイントをどう使うか
/healthzが無条件に200 OKだけを返すなら、何も検査していないのと同じです。逆に、DB・キャッシュ・外部APIをすべて確認すると、外部API1つが不安定になっただけで、自分たちのサービスがまるごとunhealthyになります。
境界は、次のように引きます。
- liveness(生きているか): プロセス自体が回復不能な状態か。再起動が答えであるものだけです。
- readiness(受け付ける準備ができたか): 今トラフィックを受けてよいか。DBコネクションなどを含みます。
この区別をしないと、「DBが一瞬遅くなった → liveness失敗 → 再起動 → コネクションを張り直すのでさらに遅くなる → また失敗」というフィードバックに陥ります。実際によく起こります。
再起動ポリシー
| ポリシー | 動作 |
|---|---|
no(デフォルト) |
死んだらそのままにします |
on-failure[:N] |
0以外の終了コードのときだけ、最大N回再起動します |
always |
常に再起動します。デーモンの再起動時にも、再び起動します |
unless-stopped |
alwaysと同じですが、人がstopしたものはそのままにします |
alwaysは、無限の再起動ループを作ることがあります。設定ミスですぐ死ぬコンテナが、毎秒何回も再起動してログを埋める状況が代表的です。Dockerは、再起動の間隔を徐々に延ばして(バックオフ)緩和しますが、根本的な解決ではありません。
depends_onは順序だけを保証する
services:
app:
depends_on: [db] # db 컨테이너가 '시작'된 뒤 app 을 시작할 뿐
dbがリクエストを受け付ける準備ができたかは見ません。PostgreSQLは、プロセスが起動したあとも、初期化にさらに数秒かかります。その間にアプリが接続すると失敗します。condition: service_healthyでヘルスチェックと結び付けて、はじめて意味を持ちます。
ヘルスチェックがかえって障害を生む場合
ヘルスチェックは安全装置ですが、誤って作るとないよりも悪いものになります。次の3つを避ければよいです。
依存先を検査に入れると、一緒に死にます。/healthがデータベースを照会すると、DBが一瞬遅くなったとき、すべてのインスタンスが同時にunhealthyになって再起動します。再起動したインスタンスが一斉にDBに接続することで、状況がさらに悪化します。生きているかを見る検査は、自分自身だけを見ます。
起動の遅いプログラムを殺してしまいます。JVMや大きなモデルを読み込むサービスは、起動に1分かかりますが、検査が30秒で失敗すると、永遠に再起動を繰り返します。Dockerは--start-period、KubernetesはstartupProbeが、この時間を与えます。
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 CMD curl -fsS http://localhost:8080/healthz || exit 1
再起動ポリシーが失敗を隠します。restart: alwaysは、死ぬたびに復活させるため、設定が間違っていて30秒ごとに死ぬサービスが、見かけ上は「動いている」ように見えます。on-failure:5のように回数を決めておけば、失敗し続けたときに止まり、そのとき人が気づきます。KubernetesのCrashLoopBackOffも同じ意味です。指数的に遅らせながら、問題を表面化させる仕組みです。
検査に使うツールが、イメージにないことがあります。distrolessイメージには、curlもwgetもないため、ヘルスチェックが常に失敗します。そのときは、アプリケーションのバイナリが自分自身を検査するモードを持つようにするか、KubernetesのHTTPプローブのように、外から検査する方式を使います。
2つの検査を分けます。トラフィックを受け付ける準備ができたか(readiness)と、生きているか(liveness)は、別の問いです。前者が失敗するとトラフィックだけが外れ、後者が失敗すると再起動します。1つにまとめると、一時的に忙しいインスタンスが再起動されてしまいます。
現場での姿
- 「再起動したら動きました」が繰り返される → livenessは通るのに、実際には使えない状態です。
- デプロイ直後にだけ失敗する →
start-periodがなく、起動中の検査に引っかかっています。 - ログが再起動メッセージでいっぱいになる →
alwaysと即時失敗の組み合わせです。
次の確認で見ること
続くクイズでは、実行中(running)と正常(healthy)を区別し、start-period・再起動ポリシー・Composeのヘルス条件を、どんな障害に適用すべきかを判断します。前のラボで作ったコンテナの状態遷移と終了コードを根拠に答えてみてください。