Gunicornのシグナルハンドリングを試す

Python用のWSGIアプリケーションサーバーであるGunicornは、プロセス管理機能を持つマスタープロセスと、アプリケーションを動作させるワーカープロセスで構成されています。 親プロセス側となるマスタープロセスにシグナルを送ることでワーカープロセスを制御できます。

docs.gunicorn.org

シグナルはkillコマンドを使って送信できます。

Graceful Shutdownなどの挙動を見てみます。

試したPythonのバージョンは 3.13, Gunicornは23.0.0

WSGIアプリケーションを準備

main.py:

import time

def slow_response(data: bytes, interval: int) -> iter:
    """
    指定された時間だけスリープして1バイトずつデータを返すジェネレータ
    """
    for byte in data:
        time.sleep(interval)
        yield byte.to_bytes()

def application(environ, start_response):
    """
    WSGIアプリケーション
    """
    status = '200 OK'
    headers = [
        ('Content-Type', 'text/plain'),
    ]
    start_response(status, headers)
    return slow_response(b'Hello\n', 1)

このアプリケーションは、1バイトずつデータを返すジェネレータを使用しています。 「Hello」という文字列を1バイトずつレスポンスし、1バイト返すごとに1秒待機します。

Gunicornを起動

Gunicornを起動します。

gunicorn main

main.pyに application というWSGIアプリケーションが定義されているので、Gunicornはそれを起動します。

起動するとGunicornのログにはプロセスIDがログで表示されます。 kill コマンドでプロセスを終了できることを確認しておきます。

kill <プロセスID>

curlでリクエス

Gunicornが起動したら、curlでリクエストを送ります。

curl -N http://localhost:8000/

-N オプションは、バッファリングを無効にするオプションです。 これにより、サーバーからのレスポンスをリアルタイムで受け取ることができます。

SIGTERMによるGraceful Shutdownの挙動を確認

Gunicornは、SIGTERMシグナルを受け取ると、Graceful Shutdownを行います。

killコマンドでSIGTERMシグナルを送信して、Gunicornの挙動を確認します。

killコマンドは、明示的にシグナルを指定しない場合、デフォルトでSIGTERMシグナルを送信します。

linuxjm.sourceforge.io

curlでリクエストを送信した状態で、別のターミナルでkillコマンドを実行します。

GunicornはSIGTERMを受信したタイミングで、ログに「Handling signal: term」と表示しています。

しかし、curlのリクエストが完了するまで、Gunicornはプロセスを終了しません。

curlのリクエストが完了すると、Gunicornは「Worker exiting: (pid: <プロセスID)」と表示し、プロセスを終了します。

このように、GunicornはSIGTERMシグナルを受信した後、現在処理中のリクエストが完了するまでプロセスを終了しないことがわかります。このような動作をGraceful Shutdownと呼びます。

SIGQUITによる強制終了の挙動を確認

SIGQUITシグナルを送信すると、Gunicornは現在処理中のリクエストがあっても強制終了します。

kill -s SIGQUIT <プロセスID>

同様にcurlでリクエストを送信した状態で、別のターミナルでkillコマンドを実行します。

GunicornはSIGQUITを受信したタイミングで、ログに「Handling signal: quit」と表示しています。

curlのリクエストは強制終了されました。 リクエストを処理中のアプリケーションではSystemExit例外で強制終了されました。

SIGHUPによるワーカーの再起動の挙動を確認

SIGHUPシグナルを送信すると、ワーカーを再起動します。

kill -s SIGHUP <プロセスID>

SIGHUPシグナルでは、Gunicornは古いWorkerをGraceful Shutdownし、新しいWorkerを起動します。

SIGTTINとSIGTTOUによるワーカー数の増減を確認

SIGTTINシグナルを送信すると、Gunicornはワーカー数を増やします。

SIGTTOUシグナルを送信すると、Gunicornはワーカー数を減らします。

kill -s SIGTTIN <プロセスID>
kill -s SIGTTOU <プロセスID>

curlで並列リクエストを送信して、処理中にGunicornのワーカー数を増やしてみます。

curl -N http://localhost:8000/ & curl -N http://localhost:8000/ & wait

1つ目の結果はワーカーが1つの場合です。1つのリクエストが完了するまで、次のリクエストは待機します。

2つ目の結果は途中でSIGTTINシグナルを送信して、ワーカーを2つに増やした場合です。 途中から2つのリクエストが並列で処理されているのがわかります。

その後、SIGTTOUシグナルを送信して、ワーカーを1つに減らすのを試しました。

感想

Gunicornのシグナルハンドリングを試してみました。

シグナル操作でGunicornのワーカーを制御できるのは便利ですね。

適切にシャットダウンする操作は、アプリケーションの安定性のために知っておくと良さそうです。

コード

https://github.com/tokibito/sample_nullpobug/tree/main/python/gunicorn_signal