はじめに
来期は「本番デプロイをもっと気軽に。高速に」を実現したいな〜と思っている SRE 川津です。
そもそも、本番デプロイが気軽にできない事の根本には 「起動しているアプリケーションを気軽にシャットダウンできない」 ことがあります。
何が問題か?
現状のリリースプロセス
LM では、本番デプロイが 半 自動化されています
# | Actor | Operation |
---|---|---|
1 | 人間 | リリース PR を master マージします |
2 | CI | Docker Image をビルドします |
3 | CI | ECS (待機系) に新しい Image がデプロイされます → 古いコンテナがシャットダウンされます |
4 | 人間 | コンテナが完全に立ち上がり切る (Health = Green ) のを待ちます |
5 | 人間 (CI) | 稼働⇔待機系 の Switch をします。 (CI 手動実行) |
作業は自動だが、張り付いて「大丈夫かな?」って確認したり、最後まで待ってたりしないといけない。
理想のリリースプロセス
言うまでもなく、理想の (みんなが求めている) リリースプロセスは ↓ でしょう。
# | Actor | Operation |
---|---|---|
1 | 人間 | リリース PR を master マージします |
2 | CI | Docker Image をビルドします |
3 | CI | ECS (待機系) に新しいコンテナがデプロイされます → 古いコンテナがシャットダウンされます |
4 | CI | コンテナが完全に立ち上がり切る (Health = Green ) のを待ちます |
5 | CI | 稼働⇔待機系 の Switch をします。 (CodeBuild 手動実行) |
短期間にリリースすると、実行中の処理が死ぬ
短時間 (例えば5分毎) にリリースをするという事は、 頻繁に古いアプリケーション (プロセス) をシャットダウンする という事です。
プロセスのシャットダウンで、 処理中のリクエストが中断されると障害になる ので困ります。
Web アプリケーション・プロセスには、大きく次の2つの処理がありますが、 シャットダウン指示が来たらこれらの処理が終わるのを待つ 必要があります。
# | 処理種別 | 説明 |
---|---|---|
(1) | Web API | HTTP リクエストを受けて、レスポンスを返す同期的な処理です。 |
(2) | バックグラウンド処理 | プロセス上で Thread 等を立ち上げて、非同期処理をします。 |
Linux SIGNALs
プロセスの停止
そもそも Web サーバープロセスはどうやって停止するかというと、 SIGNAL を用いて停止します。
皆さんが良く使う
CTLR + C
やCTRL + Z
も実は Shell (bash
,zsh
, ...) がプロセスに SIGNAL を送っています。
具体的には下表の通り、コマンド等様々な方法で SIGNAL を送信できます。
シグナル名 | 番号 | kill command | Shell CTRL | ECS & Kubernetes | ※実装依存 | 説明 |
---|---|---|---|---|---|---|
SIGHUP | 1 | kill -SIGHUP {pid} |
-- | -- | Yes | プロセスが端末から切断された際のシグナル |
SIGINT | 2 | kill -SIGINT {pid} |
CTRL + C | -- | Yes | 割り込み |
SIGKILL | 9 | kill -SIGKILL {pid} |
-- | SIGTERM 送信から30秒後に送信 | NO | プロセスの実装に依存しない OSからの強制終了 |
SIGTERM | 15 | kill {pid} |
-- | 最初に送信 | Yes | プロセスの終了指示 |
SIGTSTP | 20 | kill -SIGTSTP {pid} |
CTRL + Z | -- | Yes | 一時停止シグナル |
「※実装依存」が
Yes
のシグナルは、プロセスでシグナルを受け取った際の動作をコーディングできます。
シグナル・ハンドリング
Ruby 言語を例に、シグナル・ハンドリングの実例を見てみます。次の様な signal.rb
コードを用意します
# SIGINT (ctrl + c) 通知時の動作を上書きしちゃったぜ! Signal.trap(:INT) { puts 'hello' } # 無限ループで停止. while true sleep(1) end
このコードを実行しましょう。無限ループなのでプロセスは生存し続けます。
$ ruby signal.rb ...
普段であれば CTRL + C
でプロセスは終了してくれますが、何度やってもゾンビの様に生き続けます。
^Chello ^Chello ^Chello ...
ゾンビなので、kill
コマンドで倒します。
$ pgrep -f signal.rb 35311 $ kill 35311
kill {pid}
はkill -SIGTERM {pid}
と同じです。 (※ default が SIGTERM)
ちなみにコードを ↓ の様に変更すると、さらに無敵になります。
+ Signal.trap(:TERM) { puts 'MUTEKI !!' }
Signal.trap(:INT) { puts 'hello' }
while true
sleep(1)
end
Graceful Shutdown
さて、これまで説明した通り、Web アプリケーションプロセスは一般的に SIGTERM (15)
で終了します。
従って、SIGTERM (15)
を受けたタイミングで、それぞれ ↓ の通り対応すれば安全にプロセス終了できます。
# | 処理種別 | 説明 |
---|---|---|
(1) | Web API | 1. 新しい HTTP (TCP) 接続を受け付けない様にする 2. 既に処理中のリクエストが全て応答しきるのを待つ |
(2) | バックグラウンド処理 | 1. 新規の非同期処理の受付を停止する 2. 既に処理中の非同期処理が全て完了するのを待つ |
しかし、上記処理の終了を永久に待つわけにはいきません。
Amazon ECS や Kubernetes (GKE / EKS / AKS) では、コンテナプロセスを停止する際にはまず SIGTERM (15)
が送信されますが、30 秒 (※デフォルト値) 経ってもプロセス終了しない場合は強制終了 SIGKILL (9)
が送信されます。
この「デフォルト 30 秒」を設定変更して、処理 (1), (2) の想定される最大実行時間に合わせてやれば安全な停止ができます。
が一方で、あまりに長すぎると「いつまで経ってもデプロイが終わらない」現象に見舞われます。
Engine | Maximum timeout |
---|---|
Amazon ECS | stopTimeout (最大 120 秒) |
Kubernetes | terminationGracePeriodSeconds (K8S の制限は無いが、クラウドベンダー側での制限はあるかも) |
最後に
本記事では「Graceful Shutdown = Web アプリケーションの安全な停止」について基本的な考え方を説明させて頂きました。
前述の例では Ruby コードで直接シグナルをハンドリングするコードを書きましたが、使用しているフレームワーク (e.g. puma
, Rails
...) では、その機能が用意されており有効にするだけで良いケースも多々あります。
お使いのフレームワークでの Graceful Shutdown について是非調べてみて下さい!