TT Lab
はじめる
学ぶ 学習パス コース

ボリューム・ネットワーク・Compose

コンテナは生きているのにサービスは死んでいる

TT Labで続きを見る

一言でいうと

プロセスが生きていることと、リクエストを処理できることは、別の事実です。ヘルスチェックは、その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になります。

境界は、次のように引きます。

この区別をしないと、「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つにまとめると、一時的に忙しいインスタンスが再起動されてしまいます。

現場での姿

次の確認で見ること

続くクイズでは、実行中(running)と正常(healthy)を区別し、start-period・再起動ポリシー・Composeのヘルス条件を、どんな障害に適用すべきかを判断します。前のラボで作ったコンテナの状態遷移と終了コードを根拠に答えてみてください。