Carpe Diem

備忘録

KubernetesのPodを安全に終了する

概要

KubernetesのPodを安全に終了する際に気をつけるべき事前知識と設定方法について説明します。

あらかじめまとめると

  1. 新規リクエストのルーティングがなくなるまでpreStopでPodのアプリケーションコンテナの終了開始を待たせる
  2. 既存リクエストが全て処理されるまでアプリケーション側でGraceful Shutdownをする
  3. SIGKILLで強制終了されないように1, 2が完了するまでterminationGracePeriodSecondsを長くする

の3つを実施することになります。

環境

前提知識

Podが終了すると何がトリガーされるか

Podが終了するとまずdeletionTimestampがPodリソースに設定され、Terminating状態になります。

そしてその次に以下の3つの処理がそれぞれ独立(並行)して実施されます。

  • Podの終了処理
  • ServiceからPodへのルーティングの削除
  • ReplicaSetやDeployment管理下からの除外

これらはそれぞれ独立しているため順序に保証はなく「いずれかの処理の完了を待って次の処理をする」のような依存関係を持った制御もできません

Terminating状態になってからのシーケンス

  • Podの終了処理
  • ServiceからPodへのルーティングの削除

のシーケンスを図に表わして、エラーなく終了できるケースとそうでないケースを比較します。

後者は新規リクエストをいつまで受け入れるかに影響し、かつ前者とは独立しているので、この2つを常に意識してパラメータを設定する必要があります。

問題ないケース

問題ないケースでは

  • 新規リクエストが最後までエラーなく受け入れられている
  • 既存リクエストを全て処理している

という状態です。

preStopがない、もしくは短い場合

preStopがない場合、上図のように新規リクエストが途中で受け入れられずエラーになります。

では適切な長さはどれくらいか?というと、これは実際にエラーが出なくなるまで調整するしかないです。
というのも前述したようにServiceからの切り離しやiptablesの更新はPodの終了処理と並行して行われ、その完了を検知できないためです。

また現場で運用した感じではreplica数が大幅に増えたらこれまでエラーが出ない長さのpreStopを入れていてもエラーが出るようになったため、そういった状況にも影響するようです。

Graceful Shutdownがない、もしくは短い場合

Graceful Shutdownがない場合、既存リクエストがクライアントに返却されずに終わるのでLBなどからエラーが返ります。

terminationGracePeriodSecondsが短い場合

terminationGracePeriodSecondsが短い場合、先程のようにGraceful Shutdownが途中で強制終了されてしまうので一部の既存リクエストが返却されません。

最低でもpreStop + graceful shutdownにかかる時間を確保しましょう。

対応方法

次に具体的な設定方法について説明します。

preStop, terminationGracePeriodSeconds

preStop, terminationGracePeriodSecondsはPodリソースにて設定します。

以下はDeploymentでの例です。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: 
spec:
  selector:
    matchLabels:
      name: mypod
  template:
    metadata:
      labels:
        name: mypod
    spec:
      terminationGracePeriodSeconds: 30
      containers:
        - name: nginx
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"] 
          image: nginx
          env:
            - name: ENV
              value: production
...

distrolessの場合はshellがないのでimageを作る際に/bin/sleepをコピーするなどが必要となります。

Graceful Shutdown

christina04.hatenablog.com

christina04.hatenablog.com

といった形で実装するのが良いでしょう。

preStopの代わりにアプリケーション内でsleepさせる

preStopで外からsleepを挟む代わりに、アプリケーション側に直接sleepを導入する方法ももちろん可能です。

   case <-ctx.Done():
        logger.Info("Received TERM signal, attempting to gracefully shutdown servers.")
        healthState.Shutdown(func() {
            logger.Infof("Sleeping %v to allow K8s propagation of non-ready state", drainSleepDuration)
            time.Sleep(drainSleepDuration)

            // Calling server.Shutdown() allows pending requests to
            // complete, while no new work is accepted.
            logger.Info("Shutting down main server")
            if err := mainServer.Shutdown(context.Background()); err != nil {
                logger.Errorw("Failed to shutdown proxy server", zap.Error(err))
            }
            // Removing the main server from the shutdown logic as we've already shut it down.
            delete(servers, "main")
        })

ref: https://github.com/knative/serving/blob/5af4e84d140f679c200fbfec617c13fe691c1711/cmd/queue/main.go#L239-L251

まとめ

Podを安全に終了するためには

  1. 新規リクエストのルーティングがなくなるまでpreStopでPodのアプリケーションコンテナの終了開始を待たせる
  2. 既存リクエストが全て処理されるまでアプリケーション側でGraceful Shutdownをする
  3. SIGKILLで強制終了されないように1, 2が完了するまでterminationGracePeriodSecondsを長くする

の3つを実施してください。

参考