Gunicorn の仕組みとワーカーモデル

Gunicorn は「Green Unicorn」の略で、Ruby の Unicorn サーバーの設計思想を Python に移植したものである。プリフォーク型のワーカーモデルを採用し、シンプルながらも堅牢な本番運用を実現している。その内部構造を理解することで、適切な設定とトラブルシューティングが可能になる。

プリフォーク型アーキテクチャ

Gunicorn は、起動時にマスタープロセスが複数のワーカープロセスを「フォーク」して生成する。このアーキテクチャをプリフォーク(pre-fork)と呼ぶ。

# 4 つのワーカーで起動
gunicorn -w 4 app:application

この場合、1 つのマスタープロセスと 4 つのワーカープロセスが生成される。プロセス構成を ps コマンドで確認できる。

$ ps aux | grep gunicorn
user  1234  gunicorn: master [app:application]
user  1235  gunicorn: worker [app:application]
user  1236  gunicorn: worker [app:application]
user  1237  gunicorn: worker [app:application]
user  1238  gunicorn: worker [app:application]

マスタープロセスはリクエストを直接処理せず、ワーカーの管理に専念する。ワーカープロセスが実際のリクエスト処理を担当する。

マスタープロセス

ワーカーの起動・監視・再起動を管理。シグナルを受け取り、graceful restart やシャットダウンを制御する。リクエスト処理は行わない。

ワーカープロセス

WSGI アプリケーションを実行し、リクエストを処理する。各ワーカーは独立したプロセスとして動作し、互いに影響を与えない。

ワーカープロセスの動作

各ワーカーは、ソケットを共有してリクエストを受け付ける。OS のカーネルが、複数のワーカー間でリクエストを分配する。

# 概念的な動作イメージ(実際の実装とは異なる)
import socket
import os

def worker_loop(app, sock):
    while True:
        # ソケットから接続を受け付ける
        client, addr = sock.accept()
        
        # リクエストを処理
        handle_request(app, client)
        
        client.close()

ワーカーがクラッシュした場合、マスタープロセスが検知して新しいワーカーを生成する。この仕組みにより、アプリケーションのバグでワーカーが停止しても、サービス全体は継続できる。

ワーカータイプ

Gunicorn は複数のワーカータイプをサポートしており、アプリケーションの特性に応じて選択できる。

sync ワーカー(デフォルト)

同期的にリクエストを処理する最もシンプルなワーカーだ。1 つのリクエストが完了するまで次のリクエストを受け付けない。

gunicorn -k sync app:application

CPU バウンドな処理や、シンプルなアプリケーションに適している。外部 API 呼び出しやデータベースアクセスが多い場合は、I/O 待ちの間にワーカーがブロックされてしまう。

gthread ワーカー

各ワーカープロセス内で複数のスレッドを動かすワーカーだ。I/O 待ちの間に他のスレッドがリクエストを処理できる。

# 4 プロセス × 4 スレッド = 16 並行処理
gunicorn -k gthread -w 4 --threads 4 app:application

GIL(Global Interpreter Lock)の制約はあるが、I/O バウンドなアプリケーションでは効果的だ。sync ワーカーよりも少ないプロセス数で同等のスループットを実現できる。

gevent ワーカー

グリーンスレッド(軽量スレッド)を使った非同期ワーカーだ。コルーチンベースの並行処理により、大量の同時接続を効率的に処理できる。

pip install gevent
gunicorn -k gevent -w 4 app:application

gevent はモンキーパッチングにより、標準ライブラリの I/O 操作を非同期化する。これにより、既存のコードを変更せずに非同期の恩恵を受けられる。ただし、C 拡張を使うライブラリとの互換性には注意が必要だ。

eventlet ワーカー

gevent と同様にグリーンスレッドを使うが、実装が異なる。

pip install eventlet
gunicorn -k eventlet -w 4 app:application

gevent と eventlet はどちらも似た用途に使えるが、ライブラリの互換性やコミュニティのサポート状況を考慮して選択する。

ワーカー数の決定

適切なワーカー数は、サーバーの CPU コア数とアプリケーションの特性に依存する。

CPU バウンドの場合

ワーカー数 = CPU コア数 × 1〜2 が目安。コア数を超えるワーカーを動かしても、コンテキストスイッチのオーバーヘッドが増えるだけ。

I/O バウンドの場合

ワーカー数 = CPU コア数 × 2〜4 が目安。I/O 待ちの間に他のワーカーが処理できるため、コア数より多くても効果がある。

Gunicorn の公式ドキュメントでは (2 × CPU コア数) + 1 が推奨されている。4 コアのサーバーであれば 9 ワーカーだ。

# CPU コア数を取得して自動設定
gunicorn -w $(( 2 * $(nproc) + 1 )) app:application

ただし、これはあくまで出発点であり、実際の負荷テストを行って調整すべきだ。

タイムアウト設定

ワーカーが一定時間応答しない場合、マスタープロセスがワーカーを強制終了して再起動する。これにより、処理がスタックしたワーカーがリソースを占有し続けることを防ぐ。

# タイムアウトを 60 秒に設定
gunicorn --timeout 60 app:application

デフォルトは 30 秒だ。長時間かかる処理(レポート生成など)がある場合は、タイムアウトを延長するか、非同期タスクキュー(Celery など)に処理を委譲することを検討する。

sync ワーカー以外では --graceful-timeout も重要だ。graceful restart 時に、処理中のリクエストが完了するまで待つ時間を指定する。

gunicorn --timeout 60 --graceful-timeout 30 app:application

シグナルによる制御

マスタープロセスは Unix シグナルを受け取り、様々な操作を実行する。

# Graceful restart(新しいコードを読み込む)
kill -HUP $(cat gunicorn.pid)

# ワーカー数を増やす
kill -TTIN $(cat gunicorn.pid)

# ワーカー数を減らす
kill -TTOU $(cat gunicorn.pid)

# Graceful shutdown
kill -TERM $(cat gunicorn.pid)

# 強制終了
kill -QUIT $(cat gunicorn.pid)

HUP シグナルによる graceful restart は、ダウンタイムなしでコードを更新できる。マスタープロセスは新しいワーカーを起動し、古いワーカーを徐々に停止する。

設定ファイル

コマンドラインオプションが多くなる場合は、Python ファイルで設定を管理できる。

# gunicorn.conf.py
import multiprocessing

# バインドアドレス
bind = '0.0.0.0:8000'

# ワーカー数
workers = multiprocessing.cpu_count() * 2 + 1

# ワーカータイプ
worker_class = 'gthread'

# スレッド数(gthread の場合)
threads = 4

# タイムアウト
timeout = 60
graceful_timeout = 30

# プロセス名
proc_name = 'myapp'

# ログ
accesslog = '/var/log/gunicorn/access.log'
errorlog = '/var/log/gunicorn/error.log'
loglevel = 'info'

# PID ファイル
pidfile = '/var/run/gunicorn/gunicorn.pid'

# デーモン化
daemon = False
# 設定ファイルを指定して起動
gunicorn -c gunicorn.conf.py app:application

フック関数

設定ファイルでフック関数を定義すると、ライフサイクルイベントに処理を追加できる。

# gunicorn.conf.py

def on_starting(server):
    """マスタープロセス起動時"""
    print("Master process starting")

def on_reload(server):
    """設定リロード時"""
    print("Configuration reloaded")

def pre_fork(server, worker):
    """ワーカーフォーク前"""
    print(f"Worker {worker.pid} forking")

def post_fork(server, worker):
    """ワーカーフォーク後"""
    print(f"Worker {worker.pid} forked")

def worker_exit(server, worker):
    """ワーカー終了時"""
    print(f"Worker {worker.pid} exiting")

これらのフックは、データベース接続の初期化やクリーンアップ処理に活用できる。

Gunicorn のプリフォークモデルとワーカー管理の仕組みを理解すれば、本番環境での安定運用と、パフォーマンスチューニングの基盤が整う。まずはデフォルト設定で始め、負荷テストの結果を見ながら最適な設定を探っていくのがよい。