0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GKEローリングアップデート時の502エラーの原因特定と対策

0
Posted at

はじめに

GKEでContainer-native Load Balancing(NEG)を使用している環境で、ローリングアップデート時に502エラーが発生する問題を調査・解決しました。この記事ではその過程を記録しています。

問題の概要

GKE(Container-native Load Balancing / NEG)環境で、 「レプリカ数がゾーン数未満」 の状態でローリングアップデートを行うと、502エラーが発生する場合があります。

image.png

TL;DR

原因はingress-gceコントローラの既知のバグ(仕様)(kubernetes/ingress-gce #1718 です。Podが別ゾーンに再配置される際、LBのヘルスチェックが新Podを認識する前に旧Podが削除され、一瞬「正常なバックエンドが0」の状態が発生します。

対策は、以下の2つを組み合わせて「新旧Podの生存期間をオーバーラップ」させることです。

  • BackendConfig: checkIntervalSec を短縮し、新PodがHealthyになるまでの時間を早める。
  • Deployment: minReadySeconds を設定し、新PodがAvailableと見なされるまでの猶予期間を設ける。
# BackendConfig: ヘルスチェック間隔を短縮
apiVersion: cloud.google.com/v1
kind: BackendConfig
spec:
  healthCheck:
    checkIntervalSec: 5
    timeoutSec: 3
    healthyThreshold: 1
    unhealthyThreshold: 3
# Deployment: 新PodのAvailable判定を遅延
apiVersion: apps/v1
kind: Deployment
spec:
  # minReadySeconds > (checkIntervalSec * healthyThreshold) に設定
  minReadySeconds: 15

minReadySeconds > (checkIntervalSec * healthyThreshold)にすることで、LBヘルスチェックが新Podをhealthyと判定する前に旧Podが消えることを防ぎます。

環境

  • GKE 1.34.4(Regular channel)
  • リージョン: asia-northeast1(3ゾーン: a, b, c)
  • Ingress: GCE Class(L7 HTTP Load Balancer)
  • Container-native Load Balancing(NEG)有効
  • レプリカ数: 1

根本原因

メカニズム

  1. ローリングアップデートで新Podが別ゾーンにスケジュールされます
  2. NEGコントローラが新ゾーンにNEGエンドポイントを作成します
  3. NEGコントローラ内部のキャッシュが未同期のため、BackendServiceとの紐付けを認識できません
  4. Readiness Gateの条件cloud.google.com/load-balancer-neg-readyLBヘルスチェック通過を待たずに即座にTrueになります(LoadBalancerNegWithoutHealthCheck
  5. Kubernetesは新PodがReadyと判断し、旧Podの削除を開始します
  6. 旧PodがService Endpointsから除去され、NEGからDetachされます
  7. 新PodはNEGにAttach済みですが、LBのヘルスチェックがまだ通過していません
  8. LBから見てhealthyなバックエンドが0個 → failed_to_pick_backend502

image.png

同一ゾーン内で発生しない理由

同一ゾーン内のPod入れ替えでは、LBが既にそのゾーンのNEGをhealthyと認識しているキャッシュが残っているため、新Podが素早くhealthy判定されます。

Googleの対応:事実上の「仕様」

Google Issue Trackerでは、この挙動はWONTFIX(修正予定なし) とされています。これは単なる放置ではなく、GKE Ingress(L7 LB)のアーキテクチャ設計に起因する制限です。
設計上の理由: ingress-gceコントローラは、パフォーマンスとスケーラビリティの観点から、すべての BackendServiceの紐付けをリアルタイムかつ完全に同期することが困難です。

「楽観的Ready」の採用: バックエンドとの紐付けが確認できない場合、コントローラは通信を遮断するリスクを避けるため、「安全側に倒して拒否」するのではなく**「楽観的にReady(True)にする」**動作がハードコードされています。

結論: GKEの標準動作として「Readiness Gateはゾーンを跨ぐ際のLBヘルスチェックまでは保証しない」という仕様として扱う必要があります。

対策の詳細

1. BackendConfig: checkIntervalSecの短縮(30秒→5秒)

新PodがNEGにAttachされてからLBがhealthyと判定するまでの時間を短縮します。

apiVersion: cloud.google.com/v1
kind: BackendConfig
spec:
  healthCheck:
    checkIntervalSec: 5  # デフォルト30秒から短縮
    timeoutSec: 3
    healthyThreshold: 1
    unhealthyThreshold: 3

これだけでは不十分です。502の持続時間が約35秒→約12秒に短縮されましたが、完全には消えませんでした。

2. Deployment: minReadySecondsの追加

新PodがReadyになってからAvailableと見なされるまでの猶予期間を設けます。Deployment controllerは新PodがAvailableになるまでrolling updateの次のステップ(旧Podの削除)に進まないため、この待機時間中にLBヘルスチェックが新Podをhealthy判定します。

apiVersion: apps/v1
kind: Deployment
spec:
  minReadySeconds: 15

検証結果

checkIntervalSec minReadySeconds 502
30秒 なし 約35秒間発生
5秒 なし 約12秒間発生
5秒 15秒 発生なし

minReadySecondsの値の決め方

minReadySeconds「Readiness Probe通過後」 にカウントが開始される待機時間であり、アプリ自体の起動時間には依存しません。この値は、LBが新PodをHealthyと認識し、トラフィックを完全に切り替えるまでの時間を稼ぐために設定します。目安として、以下の計算式を満たすように設定するのが安全です。
$$minReadySeconds > (checkIntervalSec \times healthyThreshold) + \text{Margin}$$

  • checkIntervalSec: LBのヘルスチェック間隔
  • healthyThreshold: 正常判定に必要な連続成功回数(BackendConfigで指定)
  • Margin: ネットワーク遅延やNEG反映のラグを考慮したバッファ(5〜10秒程度)

今回の検証(checkIntervalSec: 5, healthyThreshold: 1)では、$(5 \times 1) + 10 = 15$秒 としています。

checkIntervalSec healthyThreshold 推奨minReadySeconds デプロイ時間への影響
5秒 1回 15秒 +15秒 / Pod
5秒 3回 25秒 +25秒 / Pod
10秒 3回 40秒 +40秒 / Pod

効果がなかった対策

preStop hook

lifecycle:
  preStop:
    exec:
      command: ["sleep", "30"]

Podのプロセスは延命されますが、Service Endpointsからの除去とNEG Detachは防げません。LBから見ると旧Podは既にルーティング対象外となるため、502の解消には寄与しませんでした。

レプリカ数による根本回避

レプリカ数がゾーン数以上(3ゾーンなら3レプリカ)の場合、topologySpreadConstraintsで全ゾーンにPodを分散配置すれば、ローリングアップデートでゾーン変更が発生しません。この場合、上記の対策は不要です。

spec:
  replicas: 3
  template:
    spec:
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: my-app
          matchLabelKeys:
            - pod-template-hash

調査の経緯

以下は原因特定に至った調査プロセスの記録です↓↓

再現テスト

継続的にHTTPリクエストを送信しながらkubectl rollout restartを実行し、502の発生を監視しました。

while true; do
  code=$(curl -s -o /dev/null -w '%{http_code}' -m 5 "$URL")
  if [ "$code" != "200" ]; then
    echo "$(date '+%H:%M:%S') HTTP $code"
  fi
  sleep 0.5
done

結果、同一条件のrollout restartでも502が出る場合と出ない場合がありました。

LBログによる原因特定

Cloud Loggingで502発生時のLBログを確認しました。

resource.type="http_load_balancer"
resource.labels.url_map_name:"<your-url-map>"
timestamp>="2026-03-25T00:56:00Z"
timestamp<="2026-03-25T00:58:00Z"

全ての502でstatusDetailsが**failed_to_pick_backend**でした。これはLBがhealthyなバックエンドを1つも見つけられなかったことを意味します。

NEGのAttach/Detachイベントとの照合

LBログと同じ時間帯のNEGイベントを確認しました。

resource.type="k8s_cluster"
(jsonPayload.message:"Attach" OR jsonPayload.message:"Detach")
jsonPayload.message:"<service-name>"

502が発生した場合(Podが別ゾーンに配置):

時刻 イベント ゾーン
00:57:13 Attach(新Pod) zone-c
00:57:26 最後の200
00:57:27 502開始
00:57:39 Detach(旧Pod) zone-b

502が発生しなかった場合(同一ゾーン内):

時刻 イベント ゾーン
Attach(新Pod) zone-c
Detach(旧Pod) zone-c
502なし

ゾーンをまたぐPod移動時にのみ502が発生していました。

ゾーン変更の強制再現

nodeSelectorで特定ゾーンへの配置を強制し、ゾーン変更を確実に発生させることで502を安定的に再現できました。

kubectl patch deployment <name> -n <ns> --type='json' \
  -p='[{"op":"add","path":"/spec/template/spec/nodeSelector","value":{"topology.kubernetes.io/zone":"asia-northeast1-b"}}]'

Readiness Gateの確認

NEGのReadiness Gate条件を確認したところ、根本原因が判明しました。

kubectl get pod <pod> -o jsonpath='{.items[0].status.conditions}' | python3 -m json.tool
{
  "reason": "LoadBalancerNegWithoutHealthCheck",
  "message": "NEG is not attached to any BackendService with health checking. Marking condition to True.",
  "status": "True",
  "type": "cloud.google.com/load-balancer-neg-ready"
}

まとめ

  • GKE NEGのReadiness Gateには既知の仕様があり、ゾーンをまたぐPod移動時に502が発生します
  • checkIntervalSec短縮 + minReadySecondsの組み合わせで防止できます
  • preStop hookはこの問題には効果がありません
  • レプリカ数 >= ゾーン数 にできる場合は topologySpreadConstraints で根本回避が可能です

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?