1. 「Webサーバ」という言葉の混乱を解く
1.1. リバースプロキシとしての Web サーバ
「Web サーバ」という言葉は、文脈によってまったく異なるソフトウェアを指します。 この混乱はデプロイ時のトラブルシューティングを著しく困難にするため、本章の冒頭で用語を明確に整理していきます。
最も古典的な意味での「Web サーバ」は、Nginx や Apache(httpd)のようなソフトウェアを指します。 これらはクライアント(ブラウザ)からの TCP 接続を受け付け、HTTP リクエストをパースし、レスポンスを返すプログラムです。 もともとは静的ファイル(HTML、CSS、画像)を返すだけのソフトウェアでしたが、現代の Web アプリケーション構成では「リバースプロキシ」としての役割が主になっています。
注釈
「リバースプロキシ」とは、クライアントに対して唯一の窓口となるサーバのことです。 クライアントはリバースプロキシだけと通信し、リバースプロキシが背後のサーバ(アプリケーションサーバ)へリクエストを転送します。 「リバース(逆)」という名前は、クライアント側に設置される「フォワードプロキシ」の逆側(サーバ側)に置かれることから来ています。
リバースプロキシとしての Nginx は、クライアントに対して唯一の窓口となり、リクエストを背後のアプリケーションサーバ(Gunicorn や Uvicorn)に転送します。 その主な責務は以下の通りです。
TLS 終端(HTTPS の暗号化・復号)
静的ファイルの直接配信
リクエストのバッファリング
レスポンスの gzip 圧縮
アクセスログの記録
レート制限
複数のアプリケーションサーバへのロードバランシング
クライアント(ブラウザ)
│
│ HTTPS (port 443)
▼
Nginx(リバースプロキシ = 「Web サーバ」の意味 ①)
│
├─ /static/ → ファイルシステムから直接配信
├─ /media/ → ファイルシステムから直接配信
│
│ HTTP (port 8000, 内部通信)
▼
Gunicorn / Uvicorn(= 「Web サーバ」の意味 ②)
│
│ WSGI / ASGI インタフェース
▼
Django / FastAPI(= 「Web アプリケーション」)
警告
この構成図の中で「Web サーバはどれか」と聞かれたとき、Nginx を指す人もいれば Gunicorn を指す人もいます。 ドキュメントや障害報告で「Web サーバを再起動した」と言われても、どの層を再起動したのかが不明確になってしまいます。 チーム内では「Nginx」「Gunicorn」「アプリケーション」のように具体的な名前で呼ぶことを習慣にしましょう。
1.2. WSGI/ASGI サーバ
Gunicorn、uWSGI、Uvicorn、Daphne、Hypercorn などは、WSGI または ASGI の仕様に準拠してアプリケーションを呼び出すサーバです。 本書ではこれらを「WSGI サーバ」「ASGI サーバ」または総称して「アプリケーションサーバ」と呼んできました。
これらのサーバの主な責務は次の通りです。
TCP ソケットの管理
HTTP リクエストのパース
environ(WSGI)またはscope(ASGI)の構築アプリケーション callable の呼び出し
レスポンスのクライアントへの書き出し
ワーカープロセスの生成・管理
タイムアウト監視
グレースフルシャットダウン
# WSGI サーバ
gunicorn myproject.wsgi:application --workers 4 --bind 127.0.0.1:8000
# ASGI サーバ
uvicorn myproject.asgi:application --workers 4 --host 127.0.0.1 --port 8000
WSGI/ASGI サーバは HTTP を理解しリクエストをパースするため、「HTTP サーバ」でもあります。 実際、Gunicorn や Uvicorn はリバースプロキシなしで直接クライアントのリクエストを受けることも技術的には可能です。 しかし本番環境ではリバースプロキシを前段に置くのが標準構成です。 その理由は、Nginx が大量の同時接続の管理、スロークライアントからのリクエストバッファリング、TLS 処理、静的ファイル配信において、Gunicorn / Uvicorn よりもはるかに効率的だからです。
Gunicorn と Uvicorn の違いを整理すると次の通りです。
項目 |
Gunicorn |
Uvicorn |
|---|---|---|
対応仕様 |
WSGI |
ASGI |
処理モデル |
プリフォーク(マスター + 複数ワーカー) |
asyncio イベントループ |
得意な処理 |
同期処理、CPU バウンド |
非同期処理、I/O バウンド |
WebSocket |
対応なし(単体では) |
対応あり |
Gunicorn に uvicorn.workers.UvicornWorker を組み合わせることで、Gunicorn のプロセス管理能力と Uvicorn の非同期処理能力を両立させる構成も一般的です。
# Gunicorn + Uvicorn ワーカーの組み合わせ
gunicorn myproject.asgi:application \
--worker-class uvicorn.workers.UvicornWorker \
--workers 4 \
--bind 127.0.0.1:8000
この構成では Gunicorn がマスタープロセスとしてワーカーの生成・監視・再起動を担い、各ワーカーは Uvicorn のイベントループで ASGI アプリケーションを実行します。
1.3. 開発サーバ
Django の python manage.py runserver と Uvicorn の uvicorn --reload は開発用のサーバです。
これらはコード変更時の自動リロード、デバッグ情報の表示、開発者の手元での動作確認を目的としています。
# Django 開発サーバ
python manage.py runserver 0.0.0.0:8000
# Uvicorn 開発モード
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# FastAPI の起動(uvicorn を内部で使用)
fastapi dev main.py
危険
開発サーバは本番環境では絶対に使用してはいけません。 本番環境に不適切な理由を以下に示します。
ワーカー管理がない(1プロセスでクラッシュすると全体停止)
タイムアウト管理が弱い
TLS 終端がない
静的ファイル配信が最適化されていない
パフォーマンスが本番ワークロード向けに設計されていない
しかし開発サーバも HTTP をパースしてアプリケーションを呼び出すため、広い意味では「Web サーバ」です。「ローカルで Web サーバを起動して」と言われたとき、開発サーバを指しているのか、本番用のサーバを指しているのかは文脈で判断する必要があります。
1.4. アプリケーションプロセス管理
本番環境では、WSGI/ASGI サーバのプロセスが永続的に動作し続ける必要があります。 サーバプロセスがクラッシュした場合の自動再起動、OS 起動時の自動起動、ログの管理などを担うのがプロセスマネージャです。
最も一般的なのは systemd(Linux)で、他に supervisord やコンテナ環境での Docker / Kubernetes があります。
# /etc/systemd/system/gunicorn.service
[Unit]
Description=Gunicorn daemon for myproject
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myproject
ExecStart=/var/www/myproject/venv/bin/gunicorn \
myproject.wsgi:application \
--workers 4 \
--bind unix:/run/gunicorn/myproject.sock \
--timeout 30
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
注釈
プロセスマネージャは HTTP を理解しません。Gunicorn プロセスを起動・停止・再起動し、クラッシュ時に自動復旧するだけです。
障害時に「サーバを再起動して」と言われたとき、systemctl restart gunicorn なのか systemctl restart nginx なのか、あるいは OS 自体を再起動するのかで意味が変わります。
「どの層を再起動するのか」を常に意識するようにしましょう。
ここまでの4つの層を改めて整理します。
層 |
具体例 |
主な責務 |
|---|---|---|
プロセスマネージャ |
systemd, supervisord, Docker, Kubernetes |
プロセスの起動・監視・再起動 |
リバースプロキシ |
Nginx, Caddy, Traefik |
TLS, 静的配信, バッファ, 圧縮, ロードバランシング |
WSGI/ASGI サーバ |
Gunicorn, Uvicorn, uWSGI, Daphne |
HTTP パース, environ/scope 構築, ワーカー管理, アプリ呼び出し |
アプリケーション |
Django, FastAPI |
ルーティング, ビジネスロジック, ORM, 認証, レスポンス生成 |
重要
本書を通じて追いかけてきた TCP ソケット → HTTP パース → WSGI/ASGI インタフェース → フレームワーク内部という流れは、この4層構造の下から上へ向かう旅でした。
トラブルシューティングにおいて「どの層で問題が起きているか」を特定することが最初のステップです。
502 Bad Gateway: Nginx とアプリケーションサーバの境界で発生
500 Internal Server Error: アプリケーション層で発生
503 Service Unavailable: プロセスマネージャがアプリケーションサーバを起動できていないか、ワーカーが飽和している状態
次節では Gunicorn のアーキテクチャを掘り下げ、プリフォークモデルとワーカー管理の仕組みを追います。
1.5. Python 標準ライブラリの簡易サーバ
1.5.1. 何ができるか
Python には標準ライブラリだけで HTTP サーバを起動できる仕組みが複数用意されています。 本書のVol.1「まずは 1 リクエストだけ処理するサーバを作る」〜Vol.1「WSGI の上に何が必要になるのか」で実際にこれらを使ってきましたが、ここで改めて本番環境との対比で位置づけを整理します。
最もシンプルなのは http.server モジュールです。
# コマンド一発で静的ファイルサーバが起動する
python -m http.server 8000
from http.server import HTTPServer, BaseHTTPRequestHandler
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(b"Hello from http.server")
server = HTTPServer(("127.0.0.1", 8000), MyHandler)
server.serve_forever()
http.server は TCP ソケットのリッスン、HTTP リクエストのパース、レスポンスの書き出しをすべて行います。
Vol.1「HTTP は何をやりとりしているのか」で socket モジュールから手書きした TCP サーバ、Vol.1「まずは 1 リクエストだけ処理するサーバを作る」で HTTP パースを自作したサーバの延長線上にあり、それらの定型処理を標準ライブラリがまとめたものです。
WSGI アプリケーションを動かすための wsgiref.simple_server も標準ライブラリに含まれています。
from wsgiref.simple_server import make_server
def application(environ, start_response):
start_response("200 OK", [("Content-Type", "text/plain")])
return [b"Hello from wsgiref"]
server = make_server("127.0.0.1", 8000, application)
server.serve_forever()
Vol.1「WSGI が生まれた背景」で WSGI の仕組みを学んだとき、まさにこの wsgiref.simple_server を使って application(environ, start_response) の動作を確認しました。
Django の runserver コマンドも内部的にこの wsgiref.simple_server を拡張して動作しています(Vol.2「どこまでが Django の責務で、どこからがサーバの責務か」)。
これらの標準ライブラリサーバが提供するものは次の通りです。
TCP ソケットのバインドとリッスン
HTTP/1.1 リクエストの基本的なパース
リクエストハンドラへのディスパッチ
レスポンスの書き出し
(
wsgirefの場合)environ辞書の構築と WSGI インタフェースの実装
Tip
標準ライブラリサーバの教育的価値は、HTTP リクエストがどのようにパースされ、environ がどのように構築され、application がどのように呼ばれるかを、余計な最適化やプロセス管理のコードに邪魔されずに追跡できる点にあります。
本書のVol.1〜Vol.1「WSGI の上に何が必要になるのか」で使ったのはまさにこの「最低限だけが動く」部分です。
1.5.2. 何に向かないか
標準ライブラリサーバには、本番環境で必要な機能がほぼすべて欠けています。
標準ライブラリサーバが本番に向かない理由を一つずつ見ていきましょう。
プロセス管理がない
http.server も wsgiref.simple_server も単一プロセス・単一スレッドで動作します(http.server には ThreadingHTTPServer がありますが、スレッド数の制御やワーカー管理はありません)。ワーカープロセスが1つしかないため、そのプロセスがクラッシュすればサービス全体が停止します。
# wsgiref は 1 プロセスで逐次処理
server = make_server("127.0.0.1", 8000, application)
server.serve_forever()
# ↑ このプロセスがクラッシュしたら終わり
# ↑ 同時に 1 リクエストしか処理できない
同時接続の処理能力が極めて限定的
デフォルトではリクエストを逐次処理するため、1つのリクエストが処理中は他のリクエストが待たされます。ブラウザが CSS、JavaScript、画像を並行してリクエストするだけで、ページの表示が著しく遅くなります。
タイムアウト管理が弱い
Gunicorn は --timeout でワーカーが応答しない場合にプロセスをキルし再起動しますが、wsgiref にはそのような仕組みがありません。無限ループやデッドロックが発生したビュー関数がプロセス全体を停止させます。
TLS(HTTPS)のサポートがない
本番環境では HTTPS が事実上必須ですが、標準ライブラリサーバには TLS 終端の機能がなく、証明書の管理も行えません。
パフォーマンスの最適化が施されていない
wsgiref の HTTP パーサーは純粋な Python で実装されており、Gunicorn が使用する http-tools や Uvicorn が使用する httptools(llhttp の Python バインディング)と比較するとパース速度に大きな差があります。
グレースフルシャットダウン・リロードの仕組みがない
コードを更新してサーバを再起動する際、処理中のリクエストが中断されます。Gunicorn は SIGHUP シグナルで新しいワーカーを起動し、古いワーカーが処理中のリクエストを完了してから終了するグレースフルリロードを提供します。
これらの制約を表にまとめると次のようになります。
機能 |
wsgiref / http.server |
Gunicorn / Uvicorn |
|---|---|---|
プロセス管理 |
なし |
マスター + ワーカー |
同時接続処理 |
逐次(または限定的スレッド) |
マルチワーカー / イベントループ |
タイムアウト監視 |
なし |
–timeout でワーカーキル |
クラッシュ時の自動復旧 |
なし |
マスターがワーカーを再生成 |
グレースフルリロード |
なし |
SIGHUP / --reload |
TLS 終端 |
なし |
限定的(通常は Nginx に委譲) |
HTTP パーサー性能 |
Pure Python |
C 拡張(httptools, llhttp) |
静的ファイル最適化 |
なし |
なし(Nginx に委譲) |
注釈
Django の runserver が wsgiref をベースにしている理由は、開発環境では上記の制約がほとんど問題にならないからです。
開発者1人がブラウザからアクセスする程度であれば、同時接続数もリクエスト頻度も低く、クラッシュしてもすぐに再起動できます。
自動リロード機能の方がはるかに重要であり、Django はファイル監視による自動リロードを runserver に独自に実装しています。
本書の構成上、標準ライブラリサーバはVol.1「HTTP は何をやりとりしているのか」〜Vol.1「WSGI の上に何が必要になるのか」の「内部構造を理解するための道具」として使い、Vol.2「Django を WSGI 視点で見る」以降のフレームワーク内部の解説では「実際のアプリケーションが動く基盤」として Gunicorn / Uvicorn を前提にしてきました。
標準ライブラリサーバで学んだ HTTP パース、environ 構築、application 呼び出しの流れは、Gunicorn や Uvicorn の内部でも同じ構造で動いています。
違いは、その周囲にプロセス管理、タイムアウト監視、パフォーマンス最適化が追加されている点です。
次節では Gunicorn のプリフォークモデルを掘り下げ、マスタープロセスとワーカープロセスの関係を追います。
1.6. Gunicorn
1.6.1. WSGI サーバとしての役割
Gunicorn(Green Unicorn)は Python の WSGI サーバとして最も広く使われているソフトウェアです。 その責務は、前節で整理した「WSGI/ASGI サーバ」の層に位置し、Nginx などのリバースプロキシと Django などのアプリケーションフレームワークの間を橋渡しします。
gunicorn myproject.wsgi:application --bind 127.0.0.1:8000
この1行で Gunicorn は次の処理を実行します。
myproject.wsgiモジュールをインポートしてapplicationオブジェクト(WSGI callable)を取得するTCP ソケットを
127.0.0.1:8000にバインドしてリッスンを開始する接続を受け付けると HTTP リクエストをパースして
environ辞書を構築するapplication(environ, start_response)を呼び出す返されたイテラブルからレスポンスボディを取り出し、ステータスコード・ヘッダーと合わせてクライアントに送信する
Tip
この流れは wsgiref.simple_server と本質的に同じです。
Vol.1「WSGI が生まれた背景」で make_server("127.0.0.1", 8000, application) としたのと同じインタフェースで、同じ environ が構築され、同じ application callable が呼ばれます。
違いは、Gunicorn がこの処理の周囲にプロセス管理、タイムアウト監視、パフォーマンス最適化を追加している点です。
1.6.2. pre-fork モデル
Gunicorn のアーキテクチャの核心はプリフォーク(pre-fork)モデルです。
┌──────────────────────────┐
│ マスタープロセス │
│ (PID 1000) │
│ - ソケットをバインド │
│ - ワーカーを fork │
│ - ワーカーを監視 │
│ - シグナルを処理 │
└──────┬───────┬───────┬─────┘
│ │ │
fork │ │ │ fork
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│Worker│ │Worker│ │Worker│
│ 1001 │ │ 1002 │ │ 1003 │
│ │ │ │ │ │
│accept│ │accept│ │accept│
│ ↓ │ │ ↓ │ │ ↓ │
│parse │ │parse │ │parse │
│ ↓ │ │ ↓ │ │ ↓ │
│app() │ │app() │ │app() │
│ ↓ │ │ ↓ │ │ ↓ │
│respond│ │respond│ │respond│
└──────┘ └──────┘ └──────┘
マスタープロセスはリクエストの処理を一切行いません。 その責務はワーカープロセスの管理に限定されます。
起動時にソケットをバインドし、
--workersで指定された数だけワーカープロセスをforkで生成しますforkされたワーカーはマスターのソケットを継承し、各ワーカーが独立してそのソケットからacceptを呼び出してクライアント接続を受け取りますOS カーネルが接続を各ワーカーに分配するため、Gunicorn 自身がロードバランシングのロジックを持つ必要はありません
マスタープロセスのもう一つの重要な責務がワーカーの監視です。
各ワーカーは定期的に一時ファイルのタイムスタンプを更新することでマスターに「生存報告」を送ります。
マスターはこのタイムスタンプを --timeout(デフォルト30秒)の間隔で検査し、更新が止まったワーカーを「応答なし」と判断して SIGKILL で強制終了し、新しいワーカーを生成します。
gunicorn myproject.wsgi:application \
--workers 4 \
--timeout 30 \
--bind 127.0.0.1:8000
この設定では4つのワーカープロセスが生成され、各ワーカーが30秒以内にリクエストを処理できなければ強制終了されます。
ビュー関数内で無限ループやデッドロックが発生した場合でも、30秒後にはワーカーが再生成され、サービスが回復します。
wsgiref.simple_server にはこの仕組みがなかったため、同様の問題が発生するとサーバ全体が永久に応答不能になっていました。
マスタープロセスはシグナルで制御されます。
シグナル |
動作 |
|---|---|
|
グレースフルリロード(新しいワーカーを起動し、古いワーカーが処理完了後に終了) |
|
グレースフルシャットダウン |
|
即座に停止 |
# グレースフルリロード(コード更新時)
kill -HUP $(cat /run/gunicorn/myproject.pid)
# グレースフルシャットダウン
kill -TERM $(cat /run/gunicorn/myproject.pid)
ワーカー数の目安
Gunicorn の公式ドキュメントは (2 × CPU コア数) + 1 をワーカー数の目安として推奨しています。
CPU バウンドの処理が多い場合はコア数に近い値を設定します
I/O バウンドの処理が多い場合はやや多めの値を設定します
ただしワーカー1つにつき Django アプリケーション全体がメモリに読み込まれるため、ワーカー数を増やすとメモリ消費も比例して増加します。
1.6.3. worker の種類
Gunicorn は複数のワーカータイプをサポートしており、--worker-class で指定します。
sync(デフォルト)
最もシンプルなワーカーで、1つのワーカーが1つのリクエストを同期的に処理します。
リクエスト処理が完了するまで次のリクエストを受け付けません。--threads オプションでワーカー内にスレッドを作成し、スレッド並行処理を有効にできます。
# sync ワーカー + スレッド
gunicorn myproject.wsgi:application \
--workers 4 \
--threads 4 \
--bind 127.0.0.1:8000
# → 最大 16 リクエスト同時処理(4 ワーカー × 4 スレッド)
gthread
スレッドプールを使用するワーカーで、sync + --threads と同様の動作ですが、より効率的なスレッド管理を提供します。
gevent / eventlet
非同期ワーカーで、グリーンスレッド(ユーザースペースの軽量スレッド)を使って1つのワーカー内で多数の同時接続を処理します。 モンキーパッチにより標準ライブラリの I/O 関数を非ブロッキングに差し替えるため、同期コードをそのまま並行実行できます。 ただし C 拡張のブロッキング呼び出しには効果がなく、デバッグが困難になる場合があります。
# gevent ワーカー
pip install gevent
gunicorn myproject.wsgi:application \
--worker-class gevent \
--workers 4 \
--worker-connections 1000 \
--bind 127.0.0.1:8000
uvicorn.workers.UvicornWorker
ASGI アプリケーションを Gunicorn 上で動かすためのワーカーです。 1 章(「Webサーバ」という言葉の混乱を解く)で触れた通り、Gunicorn のプロセス管理能力と Uvicorn の非同期処理能力を組み合わせます。
# Gunicorn + Uvicorn ワーカー(ASGI アプリケーション用)
gunicorn myproject.asgi:application \
--worker-class uvicorn.workers.UvicornWorker \
--workers 4 \
--bind 127.0.0.1:8000
ワーカータイプの選択目安を以下に示します。
ユースケース |
推奨ワーカータイプ |
|---|---|
Django の同期ビューが中心 |
|
同期アプリで同時接続数を増やしたい |
|
Django の |
|
1.6.4. 運用での使われ方
本番環境における Gunicorn の典型的な構成を、Nginx との組み合わせで示します。
# /etc/nginx/sites-available/myproject
upstream app_server {
server unix:/run/gunicorn/myproject.sock fail_timeout=0;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location /static/ {
alias /var/www/myproject/staticfiles/;
expires 30d;
}
location /media/ {
alias /var/www/myproject/media/;
}
location / {
proxy_pass http://app_server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering on;
proxy_read_timeout 60s;
}
}
# Gunicorn の起動(Unix ソケット経由で Nginx と通信)
gunicorn myproject.wsgi:application \
--workers 4 \
--threads 2 \
--timeout 30 \
--bind unix:/run/gunicorn/myproject.sock \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log \
--capture-output
注釈
Nginx と Gunicorn の間を Unix ソケットで接続しているのは、同一サーバ上での通信では TCP よりもオーバーヘッドが小さいためです。
別サーバに配置する場合は TCP ソケット(--bind 127.0.0.1:8000)を使います。
重要
タイムアウトには階層があります。各層のタイムアウトを適切に設定しないと、意図しない動作が起きます。
proxy_read_timeout 60s(Nginx が Gunicorn からの応答を60秒待つ)--timeout 30(Gunicorn がワーカーの応答を30秒待つ)ビュー内の外部 API タイムアウト(例えば10秒)
Gunicorn のタイムアウトでワーカーがキルされた場合、Nginx は応答を受け取れず 502 Bad Gateway を返します。 Nginx のタイムアウトが先に到来した場合、クライアントは 504 Gateway Timeout を受け取ります。
障害発生時のログ確認順序は次の通りです。
Nginx のエラーログ(
/var/log/nginx/error.log)で 502/504 の原因を確認するGunicorn のエラーログで
[CRITICAL] WORKER TIMEOUTやスタックトレースを確認するアプリケーションログで例外の詳細を確認する
次節では Uvicorn の内部構造を掘り下げ、ASGI サーバがどのようにイベントループを管理し scope を構築しているかを追います。
1.7. uWSGI
1.7.1. 歴史的背景
uWSGI は Gunicorn と並ぶ Python の WSGI サーバですが、その出自と設計思想はかなり異なります。
uWSGI は2009年にイタリアの開発者 Roberto De Ioris によって開発が始まりました。
当時の Python Web デプロイはまだ成熟しておらず、Apache の mod_python や mod_wsgi が主流で、独立した WSGI サーバという選択肢は少ない状況でした。
uWSGI は「Web アプリケーションのデプロイに必要なすべてを1つのプログラムで提供する」という野心的な目標を持って設計されました。
その結果、uWSGI は WSGI サーバの域を大きく超え、次のような機能を単一のバイナリに統合する巨大なプロジェクトに成長しました。
プロセスマネージャ
ロードバランサー
キャッシュサーバ
cron スケジューラ
ルーター
ログ管理
メトリクス収集
C で書かれたコア部分は高いパフォーマンスを発揮し、長らく Django デプロイのデファクトスタンダードの一つでした。
注釈
uWSGI は長期間にわたって広く使われてきましたが、近年はメンテナンスの活発さが低下しています。 Gunicorn のシンプルさと安定性、そして ASGI 対応の Uvicorn の台頭により、新規プロジェクトで uWSGI を選択するケースは減少傾向にあります。 しかし、既存の大規模プロジェクトでは依然として uWSGI が本番環境で稼働しており、運用上の知識が必要な場面は多くあります。
1.7.2. 高機能さ
uWSGI の特徴は、アプリケーションサーバとしての機能に加えて、本来であれば別のツールが担う領域までカバーしている点です。
プロセス管理
uWSGI は Gunicorn のプリフォークモデルと同様のワーカー管理を行いますが、さらに Emperor モードという上位の管理機構を持っています。 Emperor は複数の uWSGI インスタンス(vassal と呼ばれる)を監視・管理し、設定ファイルの変更を検知して自動的にリロードします。 これは systemd や supervisord が担うプロセス管理の一部を uWSGI 自身が取り込んだものです。
# emperor モード
[uwsgi]
emperor = /etc/uwsgi/vassals/
キャッシュフレームワーク
キャッシュフレームワークも内蔵しており、アプリケーションレベルのキャッシュを uWSGI プロセス内の共有メモリで管理できます。通常は Redis や Memcached が担う役割です。
cron 機能
cron 機能も備えており、定期的なタスクを uWSGI の設定ファイルで定義できます。 通常はシステムの crontab や Celery Beat が担う領域です。
内部ルーティング
リクエストの URL やヘッダーに基づいてリクエストを異なるアプリケーションに振り分けたり、静的ファイルを直接配信したりできます。 通常は Nginx が担う機能です。
# uWSGI の設定例(機能の一部)
[uwsgi]
module = myproject.wsgi:application
master = true
processes = 4
threads = 2
socket = /run/uwsgi/myproject.sock
vacuum = true
die-on-term = true
# 内蔵キャッシュ
cache2 = name=mycache,items=1000,blocksize=65536
# 内蔵 cron
cron = 0 2 -1 -1 -1 /usr/bin/python /var/www/myproject/manage.py clearsessions
# 内蔵統計サーバ
stats = 127.0.0.1:9191
# ロギング
logto = /var/log/uwsgi/myproject.log
log-maxsize = 10000000
uWSGI の設計思想と時代背景
「すべてを1つに」という設計は、2010年代前半のデプロイ環境ではツールの組み合わせが少なくて済むという利点がありました。 しかし、Docker / Kubernetes の普及により各機能を独立したコンテナやサービスとして分離する設計が主流になると、この統合アプローチは逆に柔軟性の欠如として認識されるようになりました。
1.7.3. 設定の難しさ
uWSGI の最大の課題は設定の複雑さです。
uWSGI は200以上の設定オプションを持っています。
設定は ini ファイル、XML、JSON、YAML、コマンドライン引数、環境変数で指定でき、さらにコマンドライン引数のプレフィックスが -- でも - でもなく、オプション名にハイフンとアンダースコアの両方が使えるなど、一貫性に欠ける部分があります。
# これらはすべて同じ意味
[uwsgi]
http-socket = :8000
http_socket = :8000
# コマンドラインでも複数の書き方が可能
uwsgi --http-socket :8000
uwsgi --http_socket :8000
警告
ソケットの指定だけでも、3種類があり、それぞれの違いを正確に理解していないと接続できない問題が発生します。
socket:uwsgi プロトコル用(Nginx のuwsgi_passと組み合わせて使う)http-socket:HTTP をパースして WSGI アプリに渡す(開発/テスト用)http:HTTP プロキシとして動作(ワーカーへの到達経路が1段増える)
この3つの違いが分からず接続に失敗する例は非常に多く報告されています。
# Nginx と uwsgi プロトコルで接続(最も一般的)
socket = /run/uwsgi/myproject.sock
# HTTP で直接リクエストを受ける(開発/テスト用)
http-socket = :8000
# HTTP プロキシとして動作(別の意味になる)
http = :8000
Gunicorn との設定の比較で複雑さが際立ちます。
# Gunicorn: 必要なオプションが明確で少ない
gunicorn myproject.wsgi:application \
--workers 4 \
--threads 2 \
--timeout 30 \
--bind unix:/run/gunicorn/myproject.sock
# uWSGI: 同等の構成だが、知っておくべきオプションが多い
uwsgi \
--module myproject.wsgi:application \
--master \
--processes 4 \
--threads 2 \
--harakiri 30 \
--socket /run/uwsgi/myproject.sock \
--vacuum \
--die-on-term \
--max-requests 1000 \
--max-requests-delta 50 \
--lazy-apps
「知らないとはまる」uWSGI のオプションを整理します。
オプション |
役割 |
省略した場合の問題 |
|---|---|---|
|
マスタープロセスを有効化 |
プロセス管理が効かなくなる |
|
SIGTERM でシャットダウン |
SIGTERM が「リロード」として解釈される場合がある |
|
ソケットファイルの自動削除 |
再起動時に古いソケットが残る |
|
タイムアウト(Gunicorn の |
応答しないワーカーが永続する |
|
ワーカーごとにアプリをロード |
fork 後の共有リソース問題が起きる場合がある |
現代のプロジェクトにおける uWSGI の位置づけをまとめると、既存の大規模プロジェクトで安定稼働しているのであれば移行の必要はありませんが、新規プロジェクトでは Gunicorn の設定のシンプルさと活発なメンテナンス、ASGI が必要であれば Uvicorn との組み合わせを選択するのが現在の主流です。 uWSGI の設定ファイルに遭遇した場合は、各オプションが1 章(「Webサーバ」という言葉の混乱を解く)の4層のどの責務に対応するかを意識すると理解しやすくなります。
1.7.4. uwsgi プロトコルへの軽い言及
uWSGI は HTTP ではなく独自のバイナリプロトコル「uwsgi プロトコル」(小文字で表記)を持っています。 このプロトコルは HTTP テキストのパースを省き、ヘッダーをバイナリエンコードすることでオーバーヘッドを削減します。
Nginx は uwsgi_pass ディレクティブでこのプロトコルをネイティブサポートしています。
# Nginx の設定(uwsgi プロトコル使用)
location / {
uwsgi_pass unix:/run/uwsgi/myproject.sock;
include uwsgi_params;
}
# 比較: Gunicorn 向け(HTTP プロキシ)
location / {
proxy_pass http://unix:/run/gunicorn/myproject.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
uwsgi_pass を使う場合、Nginx がクライアントの HTTP リクエストを uwsgi バイナリプロトコルに変換して送り、uWSGI がそれを直接 environ にマッピングします。
HTTP テキストの再パースが不要なため、proxy_pass による HTTP 転送よりわずかに効率的です。
ただし、この差が実測値として有意になるのは非常に高いスループットの場合に限られ、多くのプロジェクトでは HTTP プロキシで十分です。
次節では Uvicorn の内部構造を見ていきます。
1.8. Uvicorn
1.8.1. ASGI サーバとしての役割
Uvicorn は Python の ASGI サーバとして最も広く使われています。
Gunicorn が WSGI サーバとして application(environ, start_response) を呼び出すのに対し、Uvicorn は ASGI サーバとして await application(scope, receive, send) を呼び出します。
uvicorn myproject.asgi:application --host 127.0.0.1 --port 8000
Uvicorn の責務は次の通りです。
TCP 接続の受け付け
HTTP リクエストのパース
scope辞書の構築receiveとsendの非同期 callable の生成await application(scope, receive, send)の呼び出し
Vol.2「最小の ASGI HTTP アプリ」で手書きした ASGI アプリケーション(hello_asgi.py、json_app.py など)をすべて uvicorn filename:application で起動してきましたが、その裏側で Uvicorn が行っていた処理を追います。
クライアントから HTTP リクエストが届くと、Uvicorn はまず TCP データを受信して HTTP をパースします。
パース結果から scope 辞書を構築します。
# Uvicorn が内部的に構築する scope(概念コード)
scope = {
"type": "http",
"asgi": {"version": "3.0", "spec_version": "2.3"},
"http_version": "1.1",
"method": "GET",
"path": "/users/42",
"root_path": "",
"scheme": "http",
"query_string": b"fields=name,email",
"headers": [
(b"host", b"example.com"),
(b"user-agent", b"curl/7.88.1"),
(b"accept", b"*/*"),
],
"server": ("127.0.0.1", 8000),
"client": ("192.168.1.10", 54321),
}
注釈
scope["headers"] がバイト列タプルのリストである理由は、HTTP ヘッダーのバイナリデータをそのまま保持するためです。
文字列への変換はアプリケーション側の責務になります。
Vol.2「scope を理解する」で scope の各キーを詳細に見ましたが、これらの値はすべて Uvicorn が HTTP リクエストをパースして構築したものです。
同時に Uvicorn は receive と send の2つの非同期 callable を生成します。
receive:リクエストボディのチャンクを返す関数(Vol.2「request body を受け取る」で手書きしたread_body(receive)がこれを使っていました)send:レスポンスのイベントを受け取る関数(Vol.2「最小の ASGI HTTP アプリ」でawait send({"type": "http.response.start", ...})として呼んでいたものです)
1.8.2. event loop
Uvicorn の内部は asyncio のイベントループ上に構築されています。
# Uvicorn の起動フロー(概念コード)
import asyncio
class Server:
async def serve(self, sockets):
loop = asyncio.get_event_loop()
for sock in sockets:
server = await loop.create_server(
lambda: HttpProtocol(self.app, ...),
sock=sock,
)
# イベントループが接続を受け付け続ける
await self._serve_forever()
Uvicorn はデフォルトで asyncio のイベントループを使いますが、uvloop がインストールされていればそちらを優先します。
pip install uvloop
uvicorn main:app --loop uvloop
Tip
uvloop は libuv(Node.js のイベントループライブラリ)の Python バインディングで、asyncio のデフォルトイベントループを C 実装で置き換えます。
ベンチマークではデフォルトの asyncio イベントループと比較して2〜4倍のスループット向上が報告されています。
接続を受け付けるとき、Uvicorn は asyncio.Protocol を継承したプロトコルクラスのインスタンスを生成します。
HTTP/1.1 では HttpToolsProtocol(httptools ライブラリ使用時)または H11Protocol(h11 ライブラリ使用時)が使われます。
# httptools を使用(デフォルト、C 拡張で高速)
uvicorn main:app --http httptools
# h11 を使用(Pure Python、デバッグしやすい)
uvicorn main:app --http h11
httptools は Node.js の HTTP パーサー llhttp の Python バインディングで、C で実装されているため高速です。
h11 は Pure Python の HTTP/1.1 実装で、パフォーマンスは劣りますが、動作の追跡やデバッグが容易です。
Vol.1「まずは 1 リクエストだけ処理するサーバを作る」で HTTP パースを手書きしたとき、ヘッダーの終端検出やチャンクエンコーディングの処理が複雑だったことを思い出してください。
httptools と h11 はこの処理を正確かつ効率的に行い、パース結果を Uvicorn のプロトコルクラスに渡します。
リクエスト処理の全体フローは次のようになります。
クライアントから TCP データ到着
│
▼ asyncio イベントループが検知
│
▼ Protocol.data_received(data) が呼ばれる
│
▼ httptools / h11 が HTTP をパース
│
├─ ヘッダー完了 → scope 辞書を構築
│ receive / send callable を生成
│ asyncio.create_task(app(scope, receive, send))
│
├─ ボディ到着 → receive() が返すイベントのキューにチャンクを追加
│
└─ 接続完了 → クリーンアップ
注釈
asyncio.create_task で ASGI アプリケーションがタスクとしてスケジュールされるため、イベントループはブロックされません。
アプリケーション内で await receive() が呼ばれると、ボディのチャンクがキューに届くまで待機し、その間に他のリクエストが処理されます。
1.8.3. HTTP / WebSocket 対応
Uvicorn は HTTP/1.1 と WebSocket の両方を処理します。
HTTP リクエストが届くと scope["type"] = "http" でアプリケーションを呼び出し、WebSocket のアップグレードリクエスト(Upgrade: websocket ヘッダー付きの GET)が届くと scope["type"] = "websocket" で呼び出します。
クライアント: GET /ws/chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13
│
▼ Uvicorn が Upgrade ヘッダーを検出
│
▼ scope["type"] = "websocket" で application を呼び出し
│
▼ アプリが await send({"type": "websocket.accept"}) を呼ぶ
│
▼ Uvicorn が 101 Switching Protocols を返す
│
▼ 以降は WebSocket フレームの送受信
Vol.2「WebSocket の最小実装」で手書きした WebSocket エコーサーバが Uvicorn 上で動作していたのは、Uvicorn が TCP レベルの WebSocket ハンドシェイクとフレーミングを処理し、アプリケーションには websocket.connect、websocket.receive、websocket.send などの ASGI イベントとして渡していたからです。
WebSocket の実装には websockets ライブラリ(デフォルト)または wsproto が使われます。
# websockets ライブラリ使用(デフォルト)
uvicorn main:app --ws websockets
# wsproto 使用
uvicorn main:app --ws wsproto
lifespan プロトコル(Vol.2「lifespan を扱う」)も Uvicorn がサポートしています。
サーバ起動時に scope["type"] = "lifespan" でアプリケーションを呼び出し、lifespan.startup.complete を受け取るまでリクエストの受付を開始しません。
# lifespan を有効にする(デフォルトは auto)
uvicorn main:app --lifespan on
# lifespan を無効にする
uvicorn main:app --lifespan off
--lifespan auto の場合、Uvicorn は lifespan イベントをアプリケーションに送り、アプリケーションが対応していなければ(例外が発生すれば)自動的に無効にします。
1.8.4. 開発時と本番時
開発環境と本番環境では Uvicorn の使い方が大きく異なります。
開発環境では --reload フラグが最も重要です。
# 開発時
uvicorn main:app \
--reload \
--reload-dir ./src \
--host 127.0.0.1 \
--port 8000 \
--log-level debug
--reload:ファイルの変更を監視し、変更があるとサーバを自動再起動しますwatchfiles:Rust 実装のファイル監視ライブラリがインストールされていれば高速な変更検知が可能です--log-level debug:ASGI イベントの詳細(scopeの内容、receive/sendの呼び出しなど)がログに出力されます
本番環境では Uvicorn 単体ではなく、Gunicorn と組み合わせるのが推奨されます。
# 本番時: Gunicorn + Uvicorn ワーカー
gunicorn myproject.asgi:application \
--worker-class uvicorn.workers.UvicornWorker \
--workers 4 \
--bind unix:/run/uvicorn/myproject.sock \
--timeout 30 \
--access-logfile /var/log/uvicorn/access.log \
--error-logfile /var/log/uvicorn/error.log
注釈
この組み合わせが推奨される理由は、Uvicorn 単体のマルチプロセス機能(--workers)が Gunicorn のプロセス管理と比較してシンプルであるためです。
Gunicorn のマスタープロセスはワーカーの死活監視、タイムアウト検出、グレースフルリロード、シグナル管理を成熟した実装で提供します。
Uvicorn の --workers は内部的に multiprocessing を使ったシンプルな実装であり、ワーカーのヘルスチェックやタイムアウト監視は Gunicorn ほど堅牢ではありません。
Uvicorn 単体でマルチワーカーを起動する場合は次のようになります。
# Uvicorn 単体でマルチワーカー(小規模なら可)
uvicorn myproject.asgi:application \
--workers 4 \
--host 127.0.0.1 \
--port 8000
この構成は小規模なデプロイやコンテナ環境(Kubernetes が Pod レベルでヘルスチェックと再起動を担う場合)では十分機能します。 プロセス管理を外部に委譲できる環境では、Gunicorn を挟む層が不要になることもあります。
本番環境における Nginx + Gunicorn + Uvicorn の3層構成をまとめると次のようになります。
クライアント
│ HTTPS (443)
▼
Nginx
│ - TLS 終端
│ - 静的ファイル配信
│ - バッファリング / 圧縮
│ Unix socket
▼
Gunicorn (マスター)
│ - ワーカープロセスの fork / 監視
│ - タイムアウト検出
│ - グレースフルリロード
│ fork
├─── Uvicorn Worker 1 (イベントループ + httptools)
├─── Uvicorn Worker 2 (イベントループ + httptools)
├─── Uvicorn Worker 3 (イベントループ + httptools)
└─── Uvicorn Worker 4 (イベントループ + httptools)
│
│ await app(scope, receive, send)
▼
Django (ASGIHandler) / FastAPI
1 章(「Webサーバ」という言葉の混乱を解く)で整理した4層(プロセスマネージャ、リバースプロキシ、WSGI/ASGI サーバ、アプリケーション)のうち、Gunicorn はプロセスマネージャと ASGI サーバの両方の役割を兼ね、各ワーカー内で Uvicorn のイベントループが ASGI のインタフェースを処理しています。
次節では Nginx をリバースプロキシとして詳しく掘り下げ、アプリケーションサーバとの連携を追います。
1.9. Gunicorn + Uvicorn Worker
1.9.1. なぜ組み合わせるのか
前節で Uvicorn 単体の本番運用の制約に触れましたが、ここでは Gunicorn と Uvicorn を組み合わせる理由をより掘り下げます。
問題の本質は、ASGI サーバとして高性能なイベントループ実行能力と、本番環境で必要な堅牢なプロセス管理能力が、異なるソフトウェアの得意領域であるという点です。
Uvicorn の強み:イベントループ上での HTTP パースと ASGI アプリケーション呼び出しに最適化されている
Uvicorn の弱み:プロセスの死活監視やタイムアウト検出は簡素な実装にとどまっている
Gunicorn の強み:15年以上の本番運用実績を持つプロセス管理の仕組みを備えている
Gunicorn の弱み:WSGI サーバとして設計されたため、そのままでは ASGI アプリケーションを実行できない
この二つを組み合わせることで、それぞれの強みを活かせます。
gunicorn myproject.asgi:application \
--worker-class uvicorn.workers.UvicornWorker \
--workers 4 \
--timeout 30 \
--graceful-timeout 15 \
--bind unix:/run/gunicorn/myproject.sock
Uvicorn 単体のマルチワーカーモード(uvicorn --workers 4)と比較したとき、Gunicorn + Uvicorn Worker 構成が優れている点を具体的に見ていきます。
ワーカーのヘルスチェック
1.6 章(Gunicorn)で述べた通り、Gunicorn のマスタープロセスは各ワーカーが定期的に更新する一時ファイルのタイムスタンプを監視します。ワーカーがイベントループのブロッキングや無限ループに陥ってタイムスタンプの更新が止まると、--timeout 秒後にマスターがそのワーカーを SIGKILL で強制終了し、新しいワーカーを生成します。Uvicorn 単体の --workers モードにはこの機構がないため、ブロッキングで固まったワーカーが永久にリソースを占有し続ける可能性があります。
グレースフルリロード
コードの更新をデプロイした際、kill -HUP <gunicorn_master_pid> でマスタープロセスにシグナルを送ると、Gunicorn は新しいワーカーを順次起動し、古いワーカーが処理中のリクエストを完了してから終了させます。この間、少なくとも一部のワーカーは常にリクエストを受け付けているため、ダウンタイムゼロでのデプロイが可能です。
# ダウンタイムゼロのデプロイ手順
# 1. 新しいコードをデプロイ
git pull origin main
pip install -r requirements.txt
# 2. Gunicorn にグレースフルリロードを指示
kill -HUP $(cat /run/gunicorn/myproject.pid)
# 3. マスターが新ワーカーを起動 → 旧ワーカーが処理完了後に終了
# ログに "Booting worker with pid: ..." が表示される
シグナル管理の成熟度
Gunicorn は多数のシグナルに対して明確に定義された動作を持っています。
シグナル |
動作 |
|---|---|
|
リロード(新ワーカーを起動し、旧ワーカーが処理完了後に終了) |
|
グレースフルシャットダウン |
|
即時停止 |
|
ワーカー数を1増加 |
|
ワーカー数を1減少 |
|
ログファイルの再オープン |
|
実行ファイルのアップグレード |
1.9.2. プロセス管理と ASGI 実行の分担
Gunicorn + Uvicorn Worker 構成における責務の分担を詳細に追います。
Gunicorn マスタープロセス (PID 1000)
│
│ 責務:
│ - TCP ソケットのバインド
│ - ワーカープロセスの fork
│ - ワーカーの死活監視(タイムスタンプチェック)
│ - タイムアウトしたワーカーの強制終了と再生成
│ - シグナル処理(HUP, TERM, INT, TTIN, TTOU, USR1, USR2)
│ - PID ファイルの管理
│
│ fork fork fork fork
▼ ▼ ▼ ▼
UvicornWorker UvicornWorker UvicornWorker UvicornWorker
(PID 1001) (PID 1002) (PID 1003) (PID 1004)
│ │ │ │
│ 責務: │ │ │
│ - イベントループの起動 │ │
│ - httptools/h11 による HTTP パース │
│ - scope 辞書の構築 │
│ - receive/send callable の生成 │
│ - await app(scope, receive, send) │
│ - WebSocket フレーミング │
│ - マスターへのハートビート送信 │
uvicorn.workers.UvicornWorker は Gunicorn の Worker 基底クラスを継承しています。
Gunicorn がワーカーを fork した後、ワーカーの run メソッドが呼ばれます。
UvicornWorker.run は uvloop(利用可能な場合)またはデフォルトの asyncio イベントループを起動し、ソケットからの接続受付を開始します。
# uvicorn/workers.py(概念的な構造)
from gunicorn.workers.base import Worker
class UvicornWorker(Worker):
CONFIG_KWARGS = {
"loop": "uvloop",
"http": "httptools",
}
def run(self):
# Gunicorn から fork された後に呼ばれる
self.config = UvicornConfig(
app=self.app.wsgi(),
loop=self.CONFIG_KWARGS["loop"],
http=self.CONFIG_KWARGS["http"],
)
server = UvicornServer(config=self.config)
# イベントループを起動し、接続を受け付ける
loop = asyncio.new_event_loop()
loop.run_until_complete(server.serve(sockets=self.sockets))
def notify(self):
# Gunicorn マスターへのハートビート
# 一時ファイルのタイムスタンプを更新
self.tmp.notify()
self.sockets はマスタープロセスから fork 時に継承されたソケットです。
1.6 章(Gunicorn)で述べた通り、マスターがバインドしたソケットを各ワーカーが共有し、OS カーネルが接続を分配します。
警告
ハートビートの仕組みに注意が必要な点があります。
ASGI アプリケーション内で await asyncio.sleep(60) のようにイベントループをブロックせずに長時間待機している場合、イベントループ自体は動作し続けるためハートビートも正常に送信され、Gunicorn のタイムアウトは発動しません。
タイムアウトが検出されるのは、イベントループ自体がブロックされた場合(同期ブロッキング呼び出しなど)に限られます。
設定の考え方として、Gunicorn レベルの設定とアプリケーションレベルの設定を分けて管理するのが分かりやすくなります。
# gunicorn.conf.py
# プロセス管理に関する設定(Gunicorn の責務)
bind = "unix:/run/gunicorn/myproject.sock"
workers = 4
timeout = 30
graceful_timeout = 15
max_requests = 1000
max_requests_jitter = 50
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
worker_class = "uvicorn.workers.UvicornWorker"
# UvicornWorker 固有の設定
# uvicorn の loop, http, ws などは UvicornWorker の CONFIG_KWARGS で指定
Tip
max_requests = 1000 はワーカーが1000リクエストを処理した後に自動再起動する設定です。
メモリリークが疑われる場合や、長時間稼働でメモリ消費が増加する場合の予防策として使います。
max_requests_jitter = 50 はワーカー間で再起動のタイミングをずらし、全ワーカーが同時に再起動してリクエストが処理できなくなることを防ぎます。
この構成を systemd で管理する場合の設定例を示します。
# /etc/systemd/system/myproject.service
[Unit]
Description=MyProject ASGI Application
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/myproject
ExecStart=/var/www/myproject/venv/bin/gunicorn \
--config /var/www/myproject/gunicorn.conf.py \
myproject.asgi:application
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
ExecReload に kill -HUP を指定することで、systemctl reload myproject でグレースフルリロードが実行されます。
Restart=on-failure はマスタープロセスが異常終了した場合に systemd が自動再起動します。
1 章(「Webサーバ」という言葉の混乱を解く)の4層構造の中でこの構成を当てはめると次のようになります。
層 |
担当するソフトウェア |
|---|---|
プロセスマネージャ |
systemd |
リバースプロキシ |
Nginx |
プロセス管理(ワーカーの fork・監視・再起動) |
Gunicorn マスター |
ASGI サーバ(イベントループ・HTTP パース・ |
Uvicorn Worker |
アプリケーション |
Django / FastAPI |
各層の責務が明確に分離されているため、障害発生時に「どの層で問題が起きているか」をログから特定しやすくなります。
次節では、この構成の最も外側に位置する Nginx のリバースプロキシとしての役割を詳しく見ていきます。
1.10. どう選ぶか
1.10.1. Django WSGI
Django を WSGI サーバ(Gunicorn)上で動かす構成は、最も成熟した選択肢です。
gunicorn myproject.wsgi:application \
--workers 4 \
--threads 2 \
--timeout 30 \
--bind unix:/run/gunicorn/myproject.sock
この構成が適しているのは次のような場合です。
アプリケーションの大部分が ORM を中心とした CRUD 操作
リアルタイム通信(WebSocket、SSE)の要件がない
外部 API の呼び出しが少ないか逐次処理で十分
管理画面、EC サイト、社内業務システム、CMS など Django の batteries-included なエコシステムをフルに活用するプロジェクト
Tip
Django WSGI 構成の強みは安定性と予測可能性にあります。
同期モデルではリクエスト処理の流れが直線的であり、デバッグが容易です。
WSGIRequest → ミドルウェア → URL 解決 → ビュー → HttpResponse の一方向フローがそのまま動作し、sync_to_async の境界越えやイベントループのブロッキングといった非同期特有の問題が一切発生しません。
サードパーティパッケージ(Django REST Framework、django-filter、django-allauth など)もすべて同期前提で書かれており、互換性の心配がありません。
パフォーマンスの上限はワーカー数 × スレッド数で決まります。
4ワーカー × 2スレッドなら最大8リクエストの同時処理です。
I/O 待ちが多い場合はスレッド数を増やすか、gevent ワーカーに切り替えることで同時接続数を増やせます。
ほとんどの Web アプリケーションでは、この範囲で十分なスループットが得られます。
1.10.2. Django ASGI
Django を ASGI サーバ(Gunicorn + Uvicorn Worker、または Daphne)上で動かす構成は、既存の Django プロジェクトに非同期の要件が加わった場合の選択肢です。
gunicorn myproject.asgi:application \
--worker-class uvicorn.workers.UvicornWorker \
--workers 4 \
--timeout 30 \
--bind unix:/run/gunicorn/myproject.sock
この構成を選択すべきなのは、Django のエコシステム(管理画面、ORM、認証)を維持しつつ、一部のエンドポイントで非同期 I/O の利点を活かしたい場合です。 具体的には次のようなケースが挙げられます。
外部 API を並行呼び出しするダッシュボード
SSE による通知ストリーミング
LLM のトークンストリーミング中継
Django Channels による WebSocket チャット
注意
「ASGI に移行すれば速くなる」わけではありません。
既存の同期ビューは sync_to_async でスレッドプールに逃がされ、パフォーマンス特性は WSGI と大差ありません。
移行のコストに見合うのは、非同期でなければ実現できない機能要件がある場合に限られます。
段階的な移行が可能である点は Django ASGI の大きな利点です。
既存の同期ビューはそのまま動作し、新しいエンドポイントだけを async def で書けます。
プロジェクト全体を一度に書き換える必要はなく、Django が同期・非同期を自動的に判別して適切な実行経路を選択します。
1.10.3. FastAPI
FastAPI を Uvicorn 上で動かす構成は、API サーバを新規に構築する場合の有力な選択肢です。
gunicorn main:app \
--worker-class uvicorn.workers.UvicornWorker \
--workers 4 \
--timeout 30 \
--bind unix:/run/gunicorn/myproject.sock
FastAPI が適しているのは次のような場合です。
JSON API が主体のサービス
マイクロサービスアーキテクチャの一コンポーネント
リアルタイム通信(WebSocket、SSE)が必要なサービス
型安全性と自動ドキュメント生成を重視する開発チーム
非同期 I/O を前提とした設計が自然なサービス
FastAPI は最初から ASGI ネイティブで設計されているため、Django のような同期・非同期の境界越え問題が構造的に少なくなります。
警告
FastAPI が提供しないものを明確に認識しておく必要があります。 以下の機能は FastAPI には含まれておらず、別途用意が必要です。
管理画面
ORM(SQLAlchemy などを別途使用)
マイグレーション(Alembic などを別途使用)
フォーム処理
ユーザー認証の完成品
この組み合わせの選定と統合は開発者の責任であり、Django の「規約に従えば動く」アプローチとは対照的です。
1.10.4. 小規模 / 大規模
プロジェクトの規模によって、選択の重みが変わります。
小規模プロジェクト(個人開発、スタートアップの MVP、社内ツール)
開発速度が最優先です。Django は startproject 一発で認証、管理画面、ORM、マイグレーションが揃い、プロトタイプから本番まで一気通貫で進められます。
FastAPI は API 部分の開発速度は高いものの、周辺機能の選定と統合に時間がかかります。WSGI/ASGI の選択はこの段階ではほとんど影響しません。
開発サーバで十分動作し、本番は Gunicorn + Nginx の最小構成で運用できます。
中規模プロジェクト(チーム開発、明確な API 仕様、複数のフロントエンド)
アーキテクチャの選択が長期的なメンテナンスコストに影響し始めます。 Django REST Framework による API + Django 管理画面という構成は、バックエンドの一貫性を保ちつつ、フロントエンドの分離にも対応できます。 非同期の要件があるエンドポイントが一部であれば、Django ASGI への段階的移行か、該当エンドポイントだけ FastAPI で切り出すかの判断になります。
大規模プロジェクト(マイクロサービス、高トラフィック、複数チーム)
サービスごとに最適な技術を選択できます。
ユーザー管理と管理画面 → Django WSGI
リアルタイム通知 → FastAPI + WebSocket
データ処理 API → FastAPI + 非同期 ORM
各サービスの前段にリバースプロキシまたは API ゲートウェイを配置し、クライアントからは単一のエンドポイントに見せる構成が一般的です。
1.10.5. チーム運用
技術選択はチームのスキルセットと密接に関係します。
Django に慣れたチームが FastAPI に移行する場合、フレームワークの学習コストに加えて、ORM の選定(SQLAlchemy の学習)、マイグレーションツールの選定(Alembic の学習)、認証の実装、テスト戦略の変更など、周辺領域の学習が必要になります。 Django のエコシステムに蓄積された知識と経験を捨てることになるため、移行の動機が「FastAPI の方が新しいから」だけでは正当化しにくいです。
非同期プログラミングの習熟度を確認する
非同期プログラミングの経験がチームにない場合、Django ASGI や FastAPI の async def エンドポイントはブロッキング I/O やイベントループの理解不足による問題を引き起こすリスクがあります。
同期モデルは「上から下に順に実行される」という直感に合致しており、デバッグも容易です。
チーム全体の非同期プログラミングの習熟度が十分でない段階では、Django WSGI の同期モデルを維持し、必要に応じて gevent ワーカーで同時接続数を増やす方が安全な場合があります。
逆に、TypeScript や Go のような非同期プログラミングが標準的な言語の経験があるチームであれば、FastAPI の async def や Pydantic の型ヒントは自然に受け入れられるでしょう。
重要
最終的に、サーバ構成の選択は「解決すべき問題は何か」から逆算して決まります。 本書のVol.1「HTTP は何をやりとりしているのか」から1 章(「Webサーバ」という言葉の混乱を解く)までで追ってきた TCP ソケット、HTTP、WSGI、ASGI、フレームワーク内部、サーバアーキテクチャの知識は、この判断を「なんとなく」ではなく、各層の仕組みを理解した上で行うための基盤です。
次節では本章のまとめとして、本番構成のベストプラクティスを整理します。
1.11. トラブルシューティングの観点
1.11.1. worker timeout
Gunicorn のログに [CRITICAL] WORKER TIMEOUT (pid:1234) が表示される問題は、本番環境で最も頻繁に遭遇する障害の一つです。
このメッセージは、Gunicorn のマスタープロセスがワーカーからのハートビート(一時ファイルのタイムスタンプ更新)を --timeout 秒間受け取れなかったことを意味します。
マスターはそのワーカーを SIGKILL で強制終了し、新しいワーカーを生成します。
クライアント側では、リバースプロキシ(Nginx)がワーカーからの応答を受け取れず 502 Bad Gateway を返します。
ワーカータイムアウトの原因は大きく3つに分類されます。
原因1:ビュー関数の処理時間が --timeout を超える
重い SQL クエリ、大量のデータ処理、外部 API の応答待ちなどが原因になります。
# 30秒のタイムアウトを超える可能性があるビュー
def generate_report(request):
# 数百万レコードを集計する重いクエリ
data = Order.objects.filter(
created_at__year=2025
).values("category").annotate(
total=Sum("amount"),
count=Count("id"),
).order_by("-total")
# さらにPDFを生成...
pdf = generate_pdf(list(data))
return FileResponse(pdf)
注意
タイムアウト値を安易に増やすのは最後の手段です。
タイムアウトが長いほど、異常なリクエストがワーカーを占有する時間も長くなります。
根本的な対策は、重い処理を Celery などのバックグラウンドタスクに移し、ビュー関数はタスクの開始だけを行って即座にレスポンスを返すことです。
外部 API の呼び出しにはアプリケーションレベルのタイムアウト(例:requests.get(url, timeout=10))を必ず設定し、--timeout よりも短くします。
原因2:同期ブロッキングによるイベントループの停止
これは Uvicorn Worker(ASGI)を使用している場合に発生します。async def ビュー内で requests.get() や time.sleep() を呼ぶとイベントループ全体が止まり、ハートビートも送信されなくなります。この問題は単体テストでは発見できず、本番の同時リクエストで初めてタイムアウトとして表面化します。
原因3:メモリ不足によるスワップ発生
ワーカープロセスが大量のメモリを消費し OS がスワップを発生させると、すべての処理が極端に遅くなりハートビートが間に合わなくなります。dmesg や /var/log/syslog で OOM Killer のログを確認し、--max-requests でワーカーの定期再起動を設定してメモリリークの影響を軽減します。
原因の切り分けはログの組み合わせで行います。
確認順序:
1. Gunicorn エラーログ → WORKER TIMEOUT の発生頻度と PID
2. アプリケーションログ → タイムアウト直前にどのエンドポイントを処理していたか
3. Nginx エラーログ → 502 の発生タイミングとの相関
4. システムログ (dmesg, journalctl) → OOM Killer や I/O 待ちの兆候
5. DB のスロークエリログ → タイムアウトと相関する長時間クエリ
1.11.2. worker 数不足 / 過多
ワーカー数の設定ミスは、表面的にはレスポンスタイムの悪化やメモリ不足として現れ、原因の特定が遅れがちです。
ワーカー数が不足している場合
すべてのワーカーがリクエストを処理中で、新しいリクエストがソケットのバックログキューで待たされます。
Nginx のログでレスポンスタイムが通常よりも長くなり、場合によっては proxy_read_timeout に到達して 504 Gateway Timeout が返ります。
ただし、Gunicorn やアプリケーションのログにはエラーが出ないため、アプリケーション側からは問題が見えにくくなります。
# ワーカー数が 2 で、同時に 3 リクエストが来ると
# 3 番目のリクエストはワーカーが空くまで待つ
gunicorn myproject.wsgi:application --workers 2
# Nginx のアクセスログで upstream_response_time が増加
# 172.16.0.1 - - "GET /api/users" 200 0.002 upstream_response_time=3.542
# ↑ 通常 0.05s 程度なのに 3.5s
注釈
監視すべきメトリクスは、Nginx の upstream_response_time(バックエンドの応答時間)と、Gunicorn の --statsd-host で出力できるアクティブワーカー数です。
アクティブワーカー数が常にワーカー総数に近い場合は不足のサインです。
ワーカー数が過多の場合
メモリ消費が問題になります。 Django アプリケーションはワーカーごとにフレームワーク全体、ORM のメタデータ、インポートされたモジュール、テンプレートキャッシュなどをメモリに保持します。 1ワーカーあたり150〜300MBを消費することは珍しくありません。 4GBのメモリで16ワーカーを起動すると、アプリケーションだけで2.4〜4.8GBを占有し、OS やデータベースキャッシュに使えるメモリが不足します。
# 各ワーカーのメモリ消費を確認
ps aux | grep gunicorn
# USER PID %MEM RSS COMMAND
# www 1001 3.2% 256MB gunicorn: worker [myproject]
# www 1002 3.1% 248MB gunicorn: worker [myproject]
# www 1003 3.4% 272MB gunicorn: worker [myproject]
# www 1004 3.3% 264MB gunicorn: worker [myproject]
1.6 章(Gunicorn)で述べた (2 × CPU コア数) + 1 は出発点であり、実際にはアプリケーションの特性に応じて調整が必要です。
ワークロードの特性 |
ワーカー数の目安 |
|---|---|
CPU バウンド(画像処理、PDF 生成)が多い |
コア数に近い値 |
I/O バウンド(DB クエリ、外部 API)が多い |
やや多めの値 |
最終的にはメモリ消費とレスポンスタイムを監視しながら、ワーカー数を増減して最適値を見つけることになります。
1.11.3. 開発サーバと本番サーバの混同
Django の runserver や Uvicorn の --reload モードを本番環境で使用してしまう問題は、初学者だけでなく、急いでデプロイした経験豊富なエンジニアにも起こります。
# 本番環境で見かけてはいけないコマンド
python manage.py runserver 0.0.0.0:8000
uvicorn main:app --reload --host 0.0.0.0 --port 8000
これらが本番で問題を引き起こすパターンを具体的に挙げます。
シングルプロセスによる全リクエストブロック
runserver はシングルプロセスで動作するため、1つのリクエストが遅延すると後続のリクエストがすべて待たされます。
外部 API のタイムアウト待ちが発生すると全リクエストがブロックされてしまいます。
Gunicorn のマルチワーカーであれば、1つのワーカーがブロックされても他のワーカーがリクエストを処理し続けます。
クラッシュ時の手動復旧
runserver はプロセスがクラッシュすると手動で再起動するまでサービスが停止します。
深夜にメモリ不足でプロセスが kill されると、翌朝まで誰も気づかないという事態が起きます。
Gunicorn + systemd であればワーカークラッシュはマスターが自動復旧し、マスターのクラッシュは systemd が自動再起動します。
危険
DEBUG=True のまま runserver を動かしている場合、詳細なエラーページにスタックトレース、ローカル変数の値、SQL クエリ、設定ファイルの内容が表示されます。
SECRET_KEY やデータベースの接続情報が外部に漏洩するリスクがあります。
本番環境では必ず DEBUG=False を設定してください。
検出方法としては、本番サーバのプロセスリストに manage.py runserver や uvicorn --reload が存在しないことを定期的に確認します。
デプロイスクリプトや CI/CD パイプラインで、本番環境の起動コマンドが正しいことを自動チェックする仕組みを入れるのが確実です。
1.11.4. reload 設定の誤用
--reload オプションの本番使用は、開発サーバの混同とは別の次元の問題を引き起こします。
# 本番で --reload を使ってしまう
gunicorn myproject.wsgi:application --workers 4 --reload
uvicorn main:app --workers 4 --reload
--reload はファイル変更を監視するためにファイルシステムのポーリングまたは inotify(Linux)を使用します。
本番環境のプロジェクトディレクトリには数千〜数万のファイル(.pyc、ログファイル、アップロードされたメディアファイルなど)が存在し、これらすべてを監視対象にするとCPU使用率が無視できないレベルで増加します。
警告
さらに深刻なのは、意図しないリロードの発生です。 ログファイルのローテーション、ユーザーがアップロードしたファイルの保存、キャッシュファイルの更新など、アプリケーションの正常な動作によってファイルが変更されるたびにサーバが再起動します。 リクエスト処理中に再起動が走ると、処理中のレスポンスが途切れてクライアントに接続エラーが返ります。 この問題はランダムなタイミングで発生するため、再現が困難です。
Gunicorn のグレースフルリロード(kill -HUP)とファイル監視による自動リロードは、まったく異なるメカニズムです。
方法 |
発動タイミング |
処理中リクエストへの影響 |
|---|---|---|
グレースフルリロード( |
明示的なシグナル送信 |
処理完了後に切り替わる(影響なし) |
|
ファイル変更の検知(無差別) |
即座に再起動(リクエストが中断される) |
# 正しいコード更新の手順(本番)
# 1. コードをデプロイ
# 2. 明示的にリロードを指示
systemctl reload myproject
# → kill -HUP がマスターに送られ、グレースフルリロードが実行される
# 間違った手順
# --reload で起動しているから自動で反映されるだろう → 不安定な再起動が頻発
重要
これらの問題に共通するのは、開発環境と本番環境の境界が曖昧になっていることです。 開発環境では利便性のために犠牲にしている機能(プロセス管理、タイムアウト監視、セキュリティ)が、本番環境ではサービスの安定性とセキュリティを支える必須要素になります。 1 章(「Webサーバ」という言葉の混乱を解く)で整理した4層構造(プロセスマネージャ、リバースプロキシ、WSGI/ASGI サーバ、アプリケーション)の各層が適切に構成されているかを確認するチェックリストを持ち、デプロイのたびに検証することが、これらの問題の予防策になります。