こんにちは。スタディプラスのSREグループの水口です。
2022年5月に入社してからStudyplus Engineering Podcastには何度か出演しておりましたが、テックブログは今回初めて書きます。
この記事では、SidekiqをKubernetes(Amazon EKS)で動作させる際、Graceful Shutdownさせるために必要な前提知識を整理します。
背景
スタディプラスでは、老朽化しつつあるAmazon EC2で稼働していたサービスをAmazon EKSに載せ替えております。今回取り上げるバッチサーバーのSidekiqをAmazon EKSに移行させるにあたって以下の課題がありました。
- 当社サービスの仕様上、25秒以内(後述)に完了しないSidekiqのジョブが存在しており、Sidekiqのデフォルト設定だとジョブが強制終了されてしまう
- 強制終了されたジョブは再実行されるが、冪等性が担保されていないジョブが一部存在する
- ジョブの冪等性を担保し、短時間で完了するように修正する人的リソース、時間的な余裕がない
本来はSidekiqのジョブもまとめて改善すべき課題ではありますが、運用上多くのメリットを享受できるため、いったんAmazon EKS移行を先行して進めることとしました。
Amazon EKSに移行する際、注意したポイントは大別して以下の3点です。
- Sidekiqプロセスの停止
- Kubernetes Podの停止
- Kubernetes Nodeの停止
Sidekiqプロセスの停止
SIGTERM + timeout オプション
SidekiqプロセスはSIGTERMを受け取ると、デフォルトでは25秒のタイムアウトの範囲内でプロセスを終了します。このときジョブの取得は停止しますが、SIGTSTPを受け取ったとき同様に現在のジョブの処理は継続します。*1*2
このタイムアウトはSidekiq起動時のtimeoutオプションで変更できます。これによって、最も長いジョブの処理時間より長く設定することでジョブの処理完了までにプロセスが強制終了されてしまうことを防ぐことができます。
# 終了まで600秒の猶予時間を持たせる bundle exec sidekiq -C config/sidekiq.yml -e production -t 600
Kubernets Podの停止
Deploymentのマニフェストについて
マニフェストを記述する際、SidekiqのDeploymentでは、preStopフックでSidekiqプロセスにSIGTERMを送るようにすると、上述の方法でタイムアウトを設定している範囲内であればGraceful Shutdownが実現できます。*3
lifecycle: preStop: exec: command: ["/bin/sh","-c", "kill -TERM $(ps aux | grep sidekiq | grep busy | awk '{ print $2 }')"]
- 追記(2023/9/28)
Sidekiq公式のWikiの記載によると、command: ["/bin/sh","-c", "bundle exec sidekiq -t 100 -e production --config config/sidekiq_custom.yml"]
のようなシェルコマンドでSidekiqを起動することが推奨されていません。TERM
シグナルがSidekiqに直接ではなくシェルプロセスにのみ送付されるためです。
terminationGracePeriodSecondsについて
上述のようにpreStopフックで kill -TERM
を実行する場合、terminationGracePeriodSeconds(default: 30)
に気をつける必要があります。
Podの終了処理が開始されてからterminationGracePeriodSeconds
の間にSidekiqプロセスが終了しない場合、Sidekiqのtimeoutオプションを長めに設定していたとしても、SIGKILLを受け取ったPodが強制的に終了されます。
そのため、Sidekiqのtimeoutオプションと合わせてterminationGracePeriodSeconds
も延長しておく必要があります。目安としてはSidekiqのタイムアウトよりも5秒長く確保するのがよいでしょう。*4
また、Podのライフサイクルについては以下の2記事がとても参考になりました。
Kubernetes Nodeの停止
SidekiqのPodがスケジュールされているNodeが不意に終了されてしまわないように、Kubernetes(当社の場合Amazon EKS)では以下に注意する必要があります。
Cluster Autoscaler について
Cluster AutoscalerでのNodeのスケールインが発生するとき、--max-graceful-termination-sec(default: 600)
の範囲内でPodが終了しない場合、Nodeは強制的に終了します*5。しかし、弊社のEKSクラスタは複数サービスが同居するマルチテナントのクラスタとなっており、特定サービスの処理時間が長いSidekiqのためだけに設定するのは避けたいため、別の方法を検討しました。
"cluster-autoscaler.kubernetes.io/safe-to-evict": "false"
のannotationをSidekiqのDeploymentに記述することで、SidekiqのPodがスケジュールされているNodeをCluster Autoscalerによる操作の対象外としました。*6
apiVersion: apps/v1 kind: Deployment metadata: name: sidekiq-deployment annotations: cluster-autoscaler.kubernetes.io/safe-to-evict: 'false' spec: replicas: ...
Amazon EC2 スポットインスタンス について
スポットインスタンスはAWSクラウド内の使用されていない待機中のEC2を安価で利用できるサービスです。コスト最適化のために有効なサービスである一方、予期しないNodeの中断のおそれがあるため、Sidekiq用のPodをスケジュールするNodeでの利用は避けました。
スポットインスタンスが中断されるまで2分の猶予があるため、すべてのジョブが115秒以内に終了する、冪等性が担保されているという条件を満たす場合は採用の余地がありそうです。
【参考】既存実装について
Amazon EKS移行前の実装についてもご紹介します。
Amazon EC2でSidekiqを動かしていたときはSidekiq Enterprise
のEnt Rolling Restarts
を活用してSidekiqのRolling Restartを実現しておりました。
その動作にはeinhornという専用のプロセスマネージャーが利用されています。Ent Rolling Restartsを実行すると古いSidekiqプロセスはジョブの取得を停止しますが、すべてのジョブが完了するまでプロセスを終了しません。本題ではないのでこれ以上踏み込みませんが、これによって安全にRolling Restartを行うことができておりました。
当社のSidekiq Enterpriseの利用方法については以下の記事にも書かれております。よろしければご覧ください。
おわりに
以上の点に注意することによってEKSクラスタで稼働するSidekiqのGraceful Shutdownを実現できました。当社の全サービスのAmazon EKS移行はまだ終えておりませんし、移行済のサービスについても課題が残っております。改善をしつつ、引き続き進めていきます。
【宣伝】Podcastのご紹介
スタディプラスでは所属エンジニアが出演するStudyplus Engineering Podcastを配信しており、私も定期的にホストを務めております。最新第16回に私は出演しておりませんが、Kaigi on RailsでRBSに関する登壇をしたエンジニア山田さんに話を聞く回となっております。是非聞いてみてください。
*1:Signals - mperham/sidekiq Wiki.
*2:Kubernetes - mperham/sidekiq Wiki.
*3:Combine TSTP and TERM into a seperate Signal / Kubernetes gracefull shutdown - Issue #4822 - mperham/sidekiq.
*4:Bump default shutdown timeout · Issue #3968 · sidekiq/sidekiq · GitHub
*5:Does CA respect GracefulTermination in scale-down? - uswitch/kubernetes-autoscaler.
*6:What types of pods can prevent CA from removing a node? - uswitch/kubernetes-autoscaler.