Kubernetes におけるサービスのグレースフルシャットダウン実装と検証

Kubernetes サービスのグレースフルシャットダウンの必要性

ある本番環境でのデプロイ中に、支払い処理中の注文データが永続化されない事象が発生した。この問題は、外部決済システムへのリクエスト後に注文レコードを保存する直前に Pod が強制終了されたことに起因する。システムは 24 時間稼働しており、メンテナンスウィンドウが存在しないため、ゼロダウンタイムでのローリングアップデートが必須となった。

検証環境構成

検証は以下の環境で実施された:

  • Kubernetes クラスタ(Master ×1、Worker ×3)
  • アプリケーション:支払いサービス(replicas=2)
  • サービス公開ポート:NodePort 31553
  • 負荷テストツール:Apache JMeter

Deployment 設定によるグレースフルシャットダウン実装

以下の Deployment 定義では、preStop フックを用いて、Pod 終了前にサービスディスカバリ(Nacos)へのステータス通知と十分な処理時間の確保を実現している。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service
  labels:
    app: payment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: payment
  template:
    metadata:
      labels:
        app: payment
    spec:
      terminationGracePeriodSeconds: 120
      containers:
      - name: payment-app
        image: payment:v2.1-beta.76
        ports:
        - containerPort: 8011
        env:
        - name: TZ
          value: Asia/Shanghai
        - name: REGISTER_HOST
          value: nacos
        # ... その他の環境変数(省略)
        volumeMounts:
        - name: localtime
          mountPath: /etc/localtime
          readOnly: true
        - name: app-jar
          mountPath: /app/app.jar
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8011
          initialDelaySeconds: 30
          periodSeconds: 10
        lifecycle:
          preStop:
            exec:
              command:
              - /bin/sh
              - -c
              - |
                wget -qO- --post-data '' \
                'http://127.0.0.1:8011/actuator/service-registry?status=DOWN' \
                --header 'Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8' \
                && sleep 120
      volumes:
      - name: localtime
        hostPath:
          path: /etc/localtime
      - name: app-jar
        hostPath:
          path: /app/backend/pay/app.jar
      strategy:
        type: RollingUpdate
        rollingUpdate:
          maxUnavailable: 25%
          maxSurge: 25%

JMeter による負荷テストと結果分析

ローリングアップデート中、JMeter は継続的にリクエストを送信し、HTTP ステータスコードと応答時間を記録した。テスト結果の要点は以下の通り:

  • 新規 Pod が readinessProbe を通過するまで、トラフィックは旧 Pod のみにルーティングされる。
  • 旧 Pod は preStop フックによりサービスレジストリから登録解除され、新規リクエストを受け付けなくなる。
  • その後、120 秒間(terminationGracePeriodSeconds)をかけて既存リクエストを処理し、安全にシャットダウンされる。

テストログには 4 件の「失敗」として記録されたリクエストが存在したが、HTTP ステータスコードはすべて 200 OK であり、アプリケーションレベルでのエラーは確認されなかった。ログの一部を以下に示す:

1599805677891,15,HTTP Request,200,OK,Thread Group 1-7,...
1599805677902,4,HTTP Request,20HTTP Request,200,O0,OK,Thread Group 1-1,...  ← ログ破損の可能性
1599805677902,5,HTTP Request,200,OK,Thread Group 1-10,...

2 行目のログには 20HTTP Request,200,O0 という不正な形式が含まれており、これは JMeter 自体のスレッド競合やログ書き込み時のバッファリング問題によるものと推測される。実際のサービス応答は正常であり、グレースフルシャットダウンは意図通り機能していた。

タグ: Kubernetes graceful-shutdown Spring Boot Nacos JMeter

6月30日 16:06 投稿