概要
KubernetesのPodを安全に終了する際に気をつけるべき事前知識と設定方法について説明します。
あらかじめまとめると
- 新規リクエストのルーティングがなくなるまで
preStop
でPodのアプリケーションコンテナの終了開始を待たせる - 既存リクエストが全て処理されるまでアプリケーション側でGraceful Shutdownをする
- SIGKILLで強制終了されないように1, 2が完了するまで
terminationGracePeriodSeconds
を長くする
の3つを実施することになります。
環境
- Kubernetes v1.21.14
前提知識
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
や
といった形で実装するのが良いでしょう。
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") })
まとめ
Podを安全に終了するためには
- 新規リクエストのルーティングがなくなるまで
preStop
でPodのアプリケーションコンテナの終了開始を待たせる - 既存リクエストが全て処理されるまでアプリケーション側でGraceful Shutdownをする
- SIGKILLで強制終了されないように1, 2が完了するまで
terminationGracePeriodSeconds
を長くする
の3つを実施してください。