🐳

Kubernetes で動く Unicorn を gracefulShutdown する技術

2024/12/15に公開

初めに

最近、Unicorn を使用している Rails サーバーを k8s へ移行する機会があり、その際 Unicorn の gracefulShutdown の設定で色々つまづいたのでまとめます。

今となっては puma を使ってるサービスの方が圧倒的に多いと思うので、Unicorn x k8s で動かす機会が少ないかもしれないですが、もし参考になれば幸いです。

一応、前提となる構成は以下の通りです。

  • Ruby on Rails
    • Unicorn
  • EKS
  • ALB Ingress Controller

k8s の gracefulShutdown について

k8s x ALB を利用している場合、gracefulShutdown で意識すべき設定は以下のとおりです。

  • preStop
    • Pod が削除される際に SIGTERM を受け取って Hook される処理。ここから deletionGracePeriodSeconds のカウントダウンが始まる。
    • preStopが実行し終わると、コンテナへSIGTERMが送信される
  • deletionGracePeriodSeconds
    • preStop から始まる Pod の終了処理全体で許容される時間
  • deregistration_delay.timeout_seconds
    • ALB 側の設定値。ターゲット(Pod)がターゲットグループから削除される際に、リクエストの処理を完了するために待機する秒数を設定するパラメータ

この記事がとてもわかり易いので、まずはこの記事からPodの終了について学ぶと良いと思います。
https://zenn.dev/hhiroshell/articles/kubernetes-graceful-shutdown

ここで意識することは
deregistration_delay.timeout_seconds < preStop < deletionGracePeriodSeconds
となるように設定することです。
つまり、たとえば preStop > deregistration_delay.timeout_seconds となった場合、deregistration_delay.timeout_seconds が終わる前に preStop が終了し、コンテナへ SIGTERM が投げられます。せっかく deregistration_delay.timeout_seconds で ALB で処理中のリクエストを待っているのに、コンテナが終了したら意味がなくなってしまいます。

同様に preStop > deletionGracePeriodSeconds となった場合はどうでしょうか?deletionGracePeriodSeconds で設定した時間が経過した場合、SIGKILL によって強制的にコンテナが終了します。そのため、 preStop で本来行いたい処理が行えなくなる可能性があります。

基本的に同様の構成 + puma で動いているようなサーバーであれば、これらの設定値だけ気にすればよいと思います。

しかし Unicorn では、preStop終了後にコンテナへSIGTERMが送信されるということが問題になります。

Unicorn の gracefulShutdown について

まず少し Unicorn の gracefulShutdown について解説します。

Unicorn の gracefulShutdown については公式ドキュメントに記載されています
https://yhbt.net/unicorn/SIGNALS.html

INT/TERM - quick shutdown, kills all workers immediately
QUIT - graceful shutdown, waits for workers to finish their current request before finishing.
WINCH - gracefully stops workers but keep the master running. This will only work for daemonized processes.

このように、 Unicorn では SIGTERM が送信された場合は即時 shutdown されます。
また、 SIGQUIT で master, worker process の gracefulShutdown , SIGWINCH で worker process の gracefulShutdown が行われます。

先ほど

しかし Unicorn では、preStop終了後にコンテナへSIGTERMが送信されるということが問題になります。

と記載したように、 SIGTERM が送信されると即時 shutodown になるため、こちら側でどうにかしなければいけません。

Unicorn を k8s 上で gracefulShutdown させるために

preStop が終了した時点で SIGTERM がコンテナへ送信されます。
そのため、preStop の間に Unicorn を gracefulShutdown させる方法で解決したいと思います。

たとえば単純に sleep する処理を書く場合はこんな感じで書けます。

apiVersion: v1
kind: Pod
metadata:
  name: unicorn-app
spec:
  containers:
  - name: unicorn
    image: your-image:latest
    lifecycle:
      preStop:
        exec:
          command: ["sh", "-c", "sleep 300"]
    terminationGracePeriodSeconds: 320

sleep 300 によって、300s の間は gracefulShutdown のための猶予時間となります。
要はこの時間で既存のリクエストをすべてさばくことができれば、その後 SIGTERM が送信されても問題ないということになります。
つまり、サービスの中で一番処理に時間のかかるリクエストよりも余裕を持って sleep すれば、めんどくさいことは気にせずに gracefulShutdown することができます。

例えばサービスへのリクエストで一番時間のかかる処理が 60s の場合、sleep の時間をそれ以上にすることで、worker process がリクエストを捌き切った後、 preStop が終了するようになります。( ALB が draining になるまでの時間も考慮する必要はあります )

しかしながら、この方法では必ず決まった間 sleep するため、その時間が長くなるほど、Pod の削除に時間がかかることになります。
また、意図しない長時間のリクエスト ( sleep 時間を超過するようなリクエスト ) が発生した場合は gracefulShutdown することができないいため、あまり積極的に採用したい方法では無いです。

そのため、 Unicorn の process を監視するような方法で gracefulShutdown を実装します。
例えばこのように書けます。

apiVersion: v1
kind: Pod
metadata:
  name: unicorn-app
spec:
  containers:
  - name: unicorn
    image: your-image:latest
    lifecycle:
      preStop:
        exec:
          command: ["sh", "-c", "sleep 60; kill -WINCH 1; while [ $(pgrep -fc 'unicorn worker') -gt 0 ]; do sleep 1; done"]
    terminationGracePeriodSeconds: 300

各コマンドの解説です。

  • sleep 60
    • ALB からのサービスアウトを待つ時間です。60s でなくても良いとは思いますが、少し余裕を持った秒数を設定したほうが良いと思います(公式等で何秒かかるのか見つけることができなかった)。
  • kill -WINCH 1
    • worker process のみ gracefulShutdown させる
    • ここは QUIT でも良いです。その場合は grep する文字列も修正してください。
  • while [ $(pgrep -fc 'unicorn worker') -gt 0 ]; do sleep 1; done
    • loop を回し、worker process が終了したかどうかを確認し続ける。すべて終了した場合は preStop を終了する

WINCH or QUIT のあとに loop 処理で worker を監視し、すべての worker process がなくなってから preStop を終了することで、 gracefulShutdown を実装しています。
preStop が終了した後 SIGTERM が送信されますが、その時点で worker process はすべて終了しているため、Pod が終了しても問題ありません。
もし loop 処理をせずに、 WINCH or QUIT を送信した場合は、すぐに preStop が終了して SIGTERM が送信されるため、即時 shutdown となるため注意です。

もう一点、この実装であれば process が存在しない場合はすぐに preStop が終了するため、先程の sleep だけの実装よりも Pod を削除する時間が短くなります。
これは、リソースの節約にも繋がります。

また、この場合も前述の通り
deregistration_delay.timeout_seconds < preStop < deletionGracePeriodSeconds
を守る必要があります。

そのため、普段からサービスの中で遅いレイテンシー等を収集しつつ、 gracefulShutdown ではどれくらいの猶予時間が必要なのか(deletionGracePeriodSeconds は何秒必要なのか)がわかるようにしておくと良いと思います。

また、 preStop での SIGWINCH 前の sleep について少し注意しなければいけません。
この sleep があまりに短すぎると ALB が draining 状態に入る前(新規リクエストを受け付けなくなる前)に worker process を shutdown することになってしまいます。
許容できるのならある程度余裕を持って sleep を入れることをおすすめします。

参考ブログ

gracefulShutdown について知りたいならひとまず絶対に読んでほしい
https://zenn.dev/hhiroshell/articles/kubernetes-graceful-shutdown

gracefulShutdown に関するそれぞれの設定値について分かりやすく書かれていてとても参考にさせていただきました🙇‍♂️
https://christina04.hatenablog.com/entry/kubernetes-pod-graceful-shutdown

Discussion