はじめに
新しいバージョンのアプリケーションをデプロイする時に、アプリケーションをコンテナで運用している場合は、コンテナを作り替える必要がある。デプロイ時に適切な手順を踏まないと、リクエストを正しく捌けずに、クライアントにエラーを返すことになる。これは本番環境で動いているアプリケーションに取ってはクリティカルな問題である。
Kubernetesは RollingUpdate
をデフォルトで対応していため、デプロイは適切にManifestを設定をしていればダウンタイムなくデプロイしてくれる。新しいPodの生成からServiceへの追加はヘルスチェック(readnessProbe)を適切に設定するだけで良い。しかし、古いPodを停止する時には、色々考慮する問題が出てくる。適切に設定を行わないと、まだアプリケーションがリクエストを処理中にもかかわらず、Podを停止してしまうということが起こり得る。今回はこの問題を解決するために考察したことをまとめた。
KubernetesのPodを停止するまでの挙動
Kubernetesの挙動は以下のようになっている。
Kubernetesでは「 preStop
処理 + SIGTERM
処理」と、「ServiceからのPodの除外処理」が非同期で行われる。コンテナは SIGTERM
のシグナルが送られた場合に、適切に処理中のリクエストを捌き切ってから終了するようにする必要がある。そうでなければ、 SIGKILL
シグナルがコンテナに送られて強制終了してしまう。したがって、適切な terminationGracePeriodSeconds
を設定する必要がる。
また、 Service
は、該当のコンテナに新しいリクエストを送らないように切り離すが、コンテナが処理を行っている途中である場合は、その接続を切断しないようにしなければいけない。
Service (LoadBalancer)
LoadBalancerがコンテナに新しいリクエストを送らないようにするとともに、現在行っている処理の接続を切断しないようにするために、Connection Draining
の設定をする必要がある。
例えば、EKSでは、 Connection Draining
の有効化や、タイムアウトの設定を以下のように設定する。
apiVersion: v1
kind: Service
metadata:
name: test
annotations:
service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "10"
service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled: "true"
service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout: "10"
spec:
...
アプリケーションのGraceful Shutdown
コンテナが安全に終了するためには、それぞれのコンテナがGraceful Shutdownをサポートしている必要がある。
例えば、 Gunicorn
はGraceful Shutdownを対応している。適切に graceful_timeout
の値を設定すれば問題ない。
また、アプリケーションコンテナの前に Nginx
のようなリバースプロキシを立てるのはよくある構成だが、その場合、 Nginx
も同様に対応していないと、 Nginx
が先に終了し、クライアントに 504(Gateway Timeout)
が返ってしまう。
Nginx
自体はGraceful Shutdownに対応しているが、Graceful Shutdownを行うためのシグナルが、 SIGTERM
ではなく、 SIGQUIT
である。
TERM, INT fast shutdown
QUIT graceful shutdown
そこで、終了時のシグナルを SIGTERM
から SIGQUIT
に変更してやる必要がある。
FROM nginx:<version>
...
STOPSIGNAL SIGQUIT
また、タイムアウトは worker_shutdown_timeout
の値を設定する。
コンテナ実行時の複数コマンド実行
SIGTERM
シグナルはコンテナ内の PID 1
のプロセスに送られる。コンテナ起動時に複数コマンドを実行したい場合は、Startup Scriptを使うことはあるが、通常のスクリプトを用いると、そのスクリプトが PID 1
を持つことになり、アプリケーションにシグナルが送られない。
そこで、 exec
を用いて、スクリプトのプロセスをアプリケーションのプロセスに変更することで対処できる。
...
ENTRYPOINT ["./startup.sh"]
CMD ["runserver"]
#!/bin/sh
set -e
# コマンド
exec "$@"
PID 1問題
LinuxにおけるPID 1は通常 init
である。 init
は全てのプロセスの親であるため、このプロセスが殺されると、システムがダウンしてしまうため、特別扱いされている。具体的には、明示的にハンドラを設定していないシグナルは無視される。
NAME
kill - send signal to a process
NOTES
The only signals that can be sent to process ID 1, the init process,
are those for which init has explicitly installed signal handlers.
This is done to assure the system is not brought down accidentally.
したがって、アプリケーションサーバが明示的に SIGTERM
のハンドラを設定していない場合は、プロセスが SIGTERM
を無視してしまい、終了処理が適切に行われない。 Node.js
でこれが起きるらしい。
この問題の回避方法は「明示的にシグナルをハンドリングする」または、「アプリケーションのプロセスをPID 1以外で立ち上げる」の2つが考えられる。前者はそれぞれで対応すれば良い。前者が難しい場合は、後者を使う必要がある。
Kubernetes 1.17以上の場合は、ShareProcessNamespace を使ってPID 1を回避できる。
apiVersion: v1
kind: Pod
metadata:
name: test
spec:
shareProcessNamespace: true
...
Kubernetes以外、またはKubernetesのバージョンが低い場合は、 tiniのような軽量initと呼ばれるライブラリを使えば解決できる。
...
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]
CMD ["runserver"]
参考
- https://www.amazon.co.jp/dp/4295004804/
- https://text.superbrothers.dev/200328-how-to-avoid-pid-1-problem-in-kubernetes/
- https://ngzm.hateblo.jp/entry/2017/08/22/185224
- https://man7.org/linux/man-pages/man2/kill.2.html
- https://github.com/krallin/tini
- https://docs.gunicorn.org/en/stable/settings.html
- https://stackoverflow.com/questions/58408087/is-there-a-easy-way-to-shut-down-python-grpc-server-gracefully
- https://docs.celeryproject.org/en/stable/userguide/workers.html
- https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/classic/config-conn-drain.html
- https://kubernetes.io/docs/concepts/cluster-administration/cloud-providers/
- http://nginx.org/en/docs/control.html
- https://docs.gunicorn.org/en/stable/settings.html
- https://kubernetes.io/ja/docs/tasks/configure-pod-container/share-process-namespace/
- http://nginx.org/en/docs/ngx_core_module.html#worker_shutdown_timeout
- https://github.com/nginxinc/docker-nginx/issues/377