Tech Learning Daily

2026-08-04 (Tue) — 第 19 号
AI が毎朝届ける、ソフトウェア技術の基礎解説
Container 🌳 応用 ⏱ 約 8 分

Pod を消しただけなのに、なぜ 502 が数件だけ出るのか?

kubectl rollout や CI/CD 経由のデプロイのあと、ダッシュボードのエラー率グラフに一瞬だけ小さな山ができるのを見たことがあるはずだ。件数は数件から十数件、時間にして数秒。アプリのコードにもロードバランサーの設定にも、それらしいエラーは見当たらない。この数秒のズレは実装のバグではなく、Kubernetes の Pod 終了処理そのものに組み込まれた設計上の競合状態が引き起こしているものだ。

🎯 3 行まとめ
  • Pod を削除すると「エンドポイントからの除去」と「アプリへの SIGTERM 送信」という 2 つの処理が同時並行で始まり、どちらが先に終わるかの保証がない
  • kube-proxy や Ingress コントローラーへの除去情報の伝播にはラグがあり、そのわずかな間に新しいリクエストが終了処理中の Pod へ送られてしまう
  • terminationGracePeriodSeconds の既定値は 30 秒で、preStop フックで SIGTERM 送信を数秒遅らせて伝播ラグを吸収するのが定石の対策だ

Pod 削除は「エンドポイント除去」と「シグナル送信」を同時に走らせる

kubectl delete pod や Deployment のロールアウトで Pod が削除対象になると、API サーバーはその Pod オブジェクトに deletionTimestamp を刻み、Pod は Terminating 状態になる。ここから 2 本の処理が同時に走り始める。1 本目は制御プレーン側の処理で、Pod を Service の EndpointSlice(どの Pod にトラフィックを転送するかを保持する API オブジェクト)から除外し、それを watch している kube-proxy や Ingress コントローラーに変更を伝える。2 本目は Pod が乗っているノードの kubelet の処理で、preStop フック(設定されていれば)を実行したあと、コンテナの PID 1 に SIGTERM を送る。

Kubernetes の公式ドキュメントは、この 2 本の処理の間に順序の保証はないと明記している。エンドポイント除去の通知が kube-proxy や Ingress に届いて実際のルーティングが更新されるより先に、kubelet 側の SIGTERM 送信と後続の停止処理が終わってしまうことがある。つまり「もうトラフィックを流していないはず」の Pod へ、外側からはまだリクエストが飛んでくる時間帯が存在する。

kubectl delete pod 要求
        ▼
 deletionTimestamp 付与
 (Pod は Terminating に)
   ┌──────┴───────┐
   ▼               ▼
制御プレーン        kubelet:
Endpoint除去を      preStop 実行後
kube-proxy/Ingress  SIGTERM 送信
へ伝播 (ラグあり)     │
   │                ▼
   │            猶予内(既定30s)に
   │            終了しなければ
   │            SIGKILL で強制終了
   └────────┬────────┘
        同時並行 = 順序保証なし

SIGTERM を受けてから強制終了までの猶予は、既定 30 秒

kubelet が送る SIGTERM を受け取ったアプリケーションプロセスには、後片付けをして自発的に終了するための猶予期間が与えられる。この長さは Pod の spec.terminationGracePeriodSeconds で決まり、既定値は 30 秒だ。設定しなければ、SIGTERM から 30 秒経ってもプロセスが終了していない場合、kubelet は SIGKILL を送って強制終了させる。SIGKILL はプロセスに後片付けの機会を一切与えないため、処理中だったリクエストのコネクションはその場で切断される。

preStop フックがある場合、それは SIGTERM の前に実行される。ここで注意したいのは、preStop の実行時間は terminationGracePeriodSeconds の 30 秒に上乗せされるのではなく、その 30 秒の予算の中に含まれるという点だ。preStop に 25 秒かかる sleep を書けば、残り 5 秒しかアプリ本体の終了処理に使えない。

🍱 たとえるなら

閉店の張り紙を出すタイミングと、実際にシャッターを下ろすタイミングが別々の店を考えるとよい。本部が「この店は閉店します」とチェーン全体の店舗検索アプリへの反映を依頼するのと、店員がレジを締めてシャッターを下ろすのは、まったく別の担当者が別々に進める作業だ。反映が済むまでの間は、検索アプリを見た客がまだ「営業中」だと思って来店してしまう——Kubernetes のエンドポイント除去と SIGTERM 送信も、同じ理由で足並みが揃わないことがある。

伝播のラグが、なぜ数件だけの 502 に直結するのか

この記事の疑問「なぜデプロイのたびに 502 が数件だけ出るのか」に答えると、原因の多くはこのラグの窓に着弾したリクエストだ。EndpointSlice の変更が kube-proxy のルーティングルールや Ingress コントローラー・クラウドのロードバランサーの設定に反映されるまでには、watch 通知の伝達やコントローラー自身の再計算処理を挟むぶん、ゼロではない時間がかかる。この間に来た新規リクエストは、まだ古いルーティング情報に従って、SIGTERM 後・SIGKILL 前の(あるいはすでに消えた)Pod へ送られてしまう。

ここまで読むと「じゃあ伝播が終わるまで待てば解決する」と単純に思うかもしれない。しかし実際にその待ち時間を作るのはアプリでも Kubernetes 本体でもなく、次に説明する preStop フックというワンクッションだ。

定石の対策: preStop で SIGTERM 送信を数秒遅らせる

実務で最もよく使われる対策は、preStop フックに sleep を仕込んで SIGTERM の到達自体を遅らせることだ。

spec:
  terminationGracePeriodSeconds: 45
  containers:
    - name: app
      lifecycle:
        preStop:
          exec:
            command: ["sh", "-c", "sleep 5"]

sleep 5 を挟むと、Pod が Terminating になってから実際に SIGTERM が届くまで 5 秒の猶予ができ、その間にエンドポイント除去がクラスタ内に伝播する時間を稼げる。同時に terminationGracePeriodSeconds を既定の 30 秒より長い 45 秒に伸ばしているのは、preStop の 5 秒とアプリ本体の終了処理の時間を合わせても 30 秒に収まらない場合、SIGKILL が先に発動してしまうのを避けるためだ。伸ばさずに sleep だけ足すと、preStop の待ち時間がそのままアプリの終了処理時間を圧迫する。

ただし sleep が効くのは、待っている間もアプリがリクエストを普通に受け付け続けている場合に限る。SIGTERM を受けた瞬間にリスニングソケットを閉じる実装になっていると、preStop の sleep 中であってもその Pod への新規リクエストはやはり失敗する。preStop はあくまで「除去の伝播を待つための時間稼ぎ」であって、アプリ側のシグナルハンドリングを代替するものではない。

💼 実務でどう出会うか

Datadog や New Relic のようなオブザーバビリティ基盤では、デプロイイベントのマーカーがタイムライン上に打たれ、その直後の数秒〜十数秒だけエラー率がわずかに跳ねているのを見つけることがある。厄介なのは、原因の Pod はすでに SIGKILL で強制終了済みでログが残っていないことが多く、アプリ側のスタックトレースを追っても手がかりが出てこない点だ。この場合、疑うべきはアプリのバグではなく、preStop の有無や terminationGracePeriodSeconds の設定である。

⌨️ 手を動かす(5 分)

SIGTERM を無視するプロセスが、既定の猶予期間ぶんだけ生き延びてから SIGKILL される様子を、実際の待ち時間として確認する。

kind create cluster --name shutdown-demo 2>/dev/null || true
kubectl run stubborn --image=busybox --restart=Never \
  --command -- sh -c "trap '' TERM; sleep 3600"
kubectl wait --for=condition=Ready pod/stubborn --timeout=60s
time kubectl delete pod stubborn

trap '' TERM で SIGTERM を無視させているため、time の出力する real の値はおよそ 30 秒になるはずだ。Pod の spec に terminationGracePeriodSeconds を指定していないので既定値の 30 秒が使われ、その間 kubelet は SIGTERM を送ったまま待ち続け、期限が来て初めて SIGKILL を送って強制終了させている。

🙅 よくある誤解
  • SIGTERM を送ればプロセスはすぐ消える — 実際には terminationGracePeriodSeconds(既定 30 秒)が経過するまでプロセスの自発的な終了を待っており、それでも終了しない場合に限って SIGKILL で強制終了される。
  • preStop フックさえ入れれば 502 は完全になくなる — preStop はエンドポイント除去の伝播を待つための時間稼ぎに過ぎず、アプリが SIGTERM を受けた瞬間にリスニングを止める実装だと、sleep 中でも新規リクエストの失敗は防げない。
  • 「Terminating」と表示された Pod にはもうトラフィックは来ない — エンドポイント除去と SIGTERM 送信は並行して進む設計上の競合状態のため、表示が変わったあとも伝播ラグの間は新規リクエストが届くことがある。
📖 用語ミニ辞典
deletionTimestamp
Pod 削除要求を受けて API サーバーが刻む削除時刻。この時点から Pod は Terminating になる。
EndpointSlice
Service がどの Pod にトラフィックを転送するかを保持する API オブジェクト。kube-proxy や Ingress コントローラーが watch する。
preStop フック
kubelet が SIGTERM 送信前に実行するコンテナライフサイクルフック。
terminationGracePeriodSeconds
SIGTERM 送信から SIGKILL までの猶予秒数。既定値は 30 秒。
SIGKILL
猶予期限までに終了しなかったプロセスを kubelet が強制終了させるシグナル。後片付けの機会を与えない。
🔗 もっと深く
最近の記事

同じ注文が二回処理された。メッセージキューの「重複」はなぜ起こるのか?

SQSやRabbitMQは失うより重複させるat-least-once配信を採用しており、重複はブローカーがack有無しか見ていないために起きる仕様である。冪等性キーによる無害化と、FIFOキューでも消費側の冪等性が必要な理由を解説。
Arch🌿 基礎⏱ 約 8 分2026-08-02

キャッシュの有効期限が切れた瞬間、裏で何が起きているのか?

TTL は新鮮さとオリジン負荷のトレードオフであり、Vary によるキャッシュキー設計を誤ると誤配信が起きる。TTL失効直後の同時アクセスが招くキャッシュスタンピードを、stale-while-revalidateがどう防ぐかを解説。
Arch🌿 基礎⏱ 約 7 分2026-08-01

HTTPS の鍵マークは、いったい何を保証しているのか?

TLS ハンドシェイクは証明書チェーンによる本人確認とECDHEによる使い捨て鍵の合意を行う。TLS 1.3で静的RSA鍵交換が廃止され前方秘匿性が標準になった理由を、1-RTTのメッセージフローとあわせて解説。
Network🌿 基礎⏱ 約 7 分2026-07-31

ブラウザに URL を打ってから、IP に辿り着くまで何が起きているのか?

名前解決はブラウザ・OS・再帰的リゾルバの3段キャッシュをまず確認し、無ければルート→TLD→権威DNSサーバーへの委任の連鎖をたどる。「DNSの浸透」の正体はTTLによる各リゾルバの個別キャッシュであることを解説。
Network🌿 基礎⏱ 約 7 分2026-07-30