言語を切り替える
テーマを切り替える

Nginx リバースプロキシ完全ガイド:upstream・バッファ・タイムアウト

スマホが激しく振動する——本番環境のアラートだ。

ログを開くと、全部 502 Bad Gateway だった。バックエンドサービスは落ちていないのに、Nginx 設定のタイムアウトが短すぎて、トラフィックのピークが来るとリクエストが処理を終える前に強制的に中断されていた。あの proxy_read_timeout 60s という行は、当初なんとなく書いた値だった。

あの障害のあと、一週間かけて Nginx リバースプロキシの3つのコアモジュールを徹底的に理解した。upstream 負荷分散、proxy buffer のバッファ、timeout のタイムアウト設定だ。この3つを正しく設定すれば、リバースプロキシは10倍のトラフィックに耐えられる。間違えれば、あのアラートのように悲惨なことになる。

この記事では、踏んだ落とし穴、デバッグの経験、理解した原理をすべて整理した。バックエンドや運用に携わっている人、あるいは単に Nginx 設定パラメータの背後にあるロジックを理解したい人なら、きっと多くの時間を節約できるはずだ。


Upstream 負荷分散:単なる「リクエストの分配」ではない

まずは基本構文の話

upstream 設定ブロックは Nginx 負荷分散の核心だ。最も基本的な書き方は、きっと見たことがあるだろう。

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080;
}

server {
    location / {
        proxy_pass http://backend;
    }
}

一見シンプルだ——バックエンドサーバーのグループを定義して、proxy_pass で指すだけ。だが正直に言うと、これが書けるだけでは足りない。実際の本番環境で考えるべきことは多い。サーバーが落ちたらどうするか?スペックのよいマシンにはもっと多くのトラフィックを振れないか?長時間接続は維持すべきか?

4つの負荷分散アルゴリズム、それぞれに向くケースがある

Nginx がデフォルトで使うのはラウンドロビン(round-robin)で、順番に一台ずつ分配する。公平だが、賢くはない。

バックエンドが長時間接続のケース——たとえば WebSocket やデータベース接続プール——なら、ラウンドロビンでは特定のサーバーの接続数が突然急増することがある。こういうときは最小接続(least_conn)アルゴリズムの方が向いている。

upstream backend {
    least_conn;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

各サーバーのアクティブな接続数を追跡し、新しいリクエストを現在最も空いている一台に送る。以前 WebSocket でリアルタイムメッセージを配信するプロジェクトがあり、ラウンドロビン設定だと特定のサーバーのメモリが一気にパンクした——least_conn に変えたら、負荷の分布がずっと均等になった。

もう一つ、遭遇したことがあるかもしれないケースがある。ユーザーがログインしたあと、後続のリクエストは必ず同じサーバーに届かなければならない(session がローカルに保存されているため)。IP Hash はこのためにある。

upstream backend {
    ip_hash;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

同じクライアント IP のリクエストは、固定のバックエンドにハッシュされる。ただ正直に言うと、この方式には欠点がある——そのサーバーが落ちると session が失われる。本当に信頼できるのはやはり Redis で session を保存することで、ip_hash は暫定策と考えるべきだ。

4つ目は一貫性ハッシュ(hash)で、分散キャッシュのケースでよく使う。

upstream backend {
    hash $request_uri consistent;
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

Nginx は weight 単位ごとに 160 個の仮想ノードを作成し、リクエスト URI のハッシュに応じて特定のサーバーへ振り分ける。利点はキャッシュのヒット率が高いこと——同じ URI は常に同じマシンに届く。

重み設定:マシンのスペックが不均一な場合

バックエンドサーバーのスペックがバラバラというのはよくある状況だ。32G メモリ・8 コア CPU のマシンもあれば、16G・4 コアだけのマシンもある。公平なラウンドロビン?それでは性能のよいマシンが無駄になる。

upstream backend {
    server 192.168.1.10:8080 weight=3;
    server 192.168.1.11:8080 weight=2;
    server 192.168.1.12:8080 weight=1;
}

weight=3 のマシンは3倍のリクエストを受け取る。スペックのよいマシンが多く働き、悪いマシンは少なく働く——これが理にかなっている。

もう一つ backup というパラメータがある。予備サーバーだ。

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
    server 192.168.1.12:8080 backup;
}

backup サーバーは普段は負荷に参加せず、前の2台が両方落ちたときだけ稼働する。ちょっと「ベンチ入り選手」のようなものだ——主力が退いてはじめて出番が来る。

Keepalive 接続プール:パフォーマンスを倍にする秘密

ここは多くの人が見落としやすい。Nginx のデフォルトの動作は、リクエストごとにバックエンドへ新しい TCP 接続を張り、応答が終わったら閉じる、というものだ。問題なさそうに聞こえる?問題は大ありだ。

TCP 接続の確立には3ウェイハンドシェイクが、切断には4ウェイハンドシェイクが必要だ。高並行のケースでは、このオーバーヘッドはかなり恐ろしい。一方、keepalive 接続プールは接続を再利用し、この部分のオーバーヘッドを省ける。

設定例:

upstream backend {
    server 192.168.1.10:8080;
    keepalive 32;  # 各 worker が 32 個のアイドル接続を維持
}

server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

2点に注意。

  1. keepalive 32 は各 worker プロセスが維持する最大アイドル接続数を設定する
  2. 必ず proxy_http_version 1.1Connection "" を設定する——HTTP/1.0 は持続的接続をサポートしない

以前ある API サービスをテストしたところ、keepalive がないと QPS はおよそ 2000 だったが、追加すると一気に 4000 以上になった。倍増は誇張ではなく、本当の話だ。

2倍
QPS パフォーマンス向上
Source: 実測データ:keepalive 接続プールを有効化した後

ただし注意。keepalive の値は高くしすぎないこと。以前テスト環境で keepalive を 100 に設定したところ、バックエンドが 1 つの ECS コンテナしかなく、接続数で一気にパンクした。本番環境のおおよその計算式はこうだ。

keepalive ≈ 総QPS ÷ 1リクエストの平均所要時間 ÷ worker プロセス数

たとえば QPS 10000 を見込み、平均応答時間 50ms、worker が 4 つの場合:

10000 × 0.05 ÷ 4 = 125

keepalive は 125 程度が妥当だ。

ヘルスチェック:落ちたら自動で除外

Nginx のオープンソース版にはパッシブなヘルスチェックしかない——リクエストが失敗してはじめてサーバーを unhealthy と判定する。

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

server {
    location / {
        proxy_pass http://backend;
        proxy_next_upstream error timeout http_502 http_503 http_504;
        proxy_next_upstream_tries 3;
    }
}

proxy_next_upstream は、どういう場合に次のサーバーへリトライするかを定義する。接続エラー、タイムアウト、または 502/503/504 が返ってきた場合だ。proxy_next_upstream_tries 3 は最大 3 台まで試すことを意味する。

だが正直に言うと、パッシブなチェックには遅延がある——リクエストが失敗するまでサーバーが落ちたことに気づけない。可用性への要求が高い業務なら、NGINX Plus のアクティブなヘルスチェックの方がよい。

upstream backend {
    server 192.168.1.10:8080;
    server 192.168.1.11:8080;
}

server {
    location / {
        proxy_pass http://backend;
        health_check interval=5s fails=3 passes=2;
    }
}

5 秒ごとに能動的にヘルスチェックリクエストを送り、連続 3 回失敗すると unhealthy と判定し、連続 2 回成功してはじめて復帰する。


Proxy Buffer:バッファは味方か、それとも邪魔者か

バッファ機構はそもそも何のためにあるのか

まず概念から。Nginx はバックエンドから応答を受け取ったあと、直接クライアントへ送るのではなく、まずバッファに保存する。

なぜか?クライアントのネットワーク速度は予測できない(unpredictable)からだ。バックエンドはすぐにデータを吐き出せても、クライアントの回線が遅ければ、Nginx は「詰まった」状態でクライアントがゆっくり受信するのを待たねばならない。バッファがあれば、Nginx は応答を一気に保存し、その後ゆっくりクライアントへ送れる——バックエンドは待たずに済み、次のリクエストを早く処理できる。

だがバッファには代償もある。メモリを消費するのだ。応答ボディが大きく、並行数が高いときは、メモリ消費がかなりのものになる。

3つのコアパラメータ、関係を整理する

proxy_buffer_size 4k;
proxy_buffers 8 32k;
proxy_busy_buffers_size 64k;

この3つのパラメータは、当初見たとき頭が痛くなった——名前が似ていて、意味も入り組んでいる。あとで図を描いてやっと理解した。

  • proxy_buffer_size:レスポンスヘッダーを保持するバッファ。1リクエストにつき1つ
  • proxy_buffers:レスポンスボディを保持するバッファ配列。形式は 個数 1個あたりのサイズ
  • proxy_busy_buffers_size:クライアントへ送信中の部分のバッファ。総 buffers サイズの半分を超えてはいけない

例を挙げよう。proxy_buffers 8 32k の総サイズは 8×32k = 256k。proxy_busy_buffers_size 64k はちょうど4分の1で、ルールに合っている。

これらのパラメータはいつ調整すべきか?

バックエンドのレスポンスヘッダーが特に大きい場合(Cookie を大量に詰め込んだ場合など)、“upstream sent too big header” が出ることがある。解決策は proxy_buffer_size を大きくすること。

proxy_buffer_size 16k;

応答ボディがよく大きくなる場合(API が長い JSON を返す場合など)は、buffers を大きくできる。

proxy_buffers 16 64k;

特殊なケース:バッファを無効にする

バッファがかえって厄介になるケースもある。

Server-Sent Events(SSE):バックエンドがイベントストリームを継続的に配信する場合、Nginx がバッファして送らないと、クライアントが受け取るのは遅延したメッセージになる。設定でバッファを無効にする。

location /events {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 86400s;
}

proxy_read_timeout 86400s(1日)にしているのは、SSE が長時間接続であり、タイムアウトで切断されてはいけないからだ。

WebSocket:同様に、双方向のリアルタイム通信。

location /ws {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 86400s;
}

大きなファイルのアップロード:クライアントが 1G のファイルをアップロードする場合、Nginx が先に全部受信してからバックエンドへ転送すると、メモリが一気にパンクする。このときはリクエストのバッファを無効にする。

location /upload {
    proxy_pass http://backend;
    proxy_request_buffering off;
    client_max_body_size 1G;
}

proxy_request_buffering off で Nginx は直接ストリーミング転送し、受信しながら送信する。


Timeout:タイムアウト設定の背後にあるロジック

3つのタイムアウトパラメータ、それぞれが担当する

proxy_connect_timeout 10s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;

名前は似ているが、実は役割分担が明確だ。

  • proxy_connect_timeout:Nginx が TCP 接続の確立を待つ時間。バックエンドサーバーの応答が遅い場合(ネットワーク輻輳、ファイアウォール遮断など)、この時間を超えると諦める。
  • proxy_read_timeout:接続確立後、Nginx がバックエンドからのデータ返却を待つ時間。2回の読み取り操作の間隔がこの値を超えるとタイムアウトとみなす。
  • proxy_send_timeout:Nginx がリクエストボディをバックエンドへ送信する時間の上限。

ここに混同しやすい点がある。proxy_read_timeout は総タイムアウトではなく、2回の読み取り操作の間隔だ。バックエンドが返すのに5分かかるが、処理中に絶えず少しずつデータを送ってくる(ハートビートなど)なら、proxy_read_timeout 60s で足りる。だがバックエンドが5分間まったく沈黙するなら、proxy_read_timeout 300s に設定しなければならない。

タイムアウトと 502/504 の関係

深夜3時のあのアラートで学んだ重要な教訓がある。

  • 502 Bad Gateway:Nginx がそもそもバックエンドに接続できていない——バックエンドが落ちている、ポートが不通、ファイアウォールに遮断されている可能性がある
  • 504 Gateway Timeout:Nginx は接続できたが、バックエンドの処理が長すぎてデータを返さない

例を挙げよう。proxy_connect_timeout 10s のとき、バックエンドが接続リクエストに応答するのに 15 秒かかれば、Nginx は 502 を返す。だが接続がすぐ確立され、バックエンドが 2 分かけて処理してからデータを吐き出し、proxy_read_timeout 60s だった場合は、504 を返す。

ケースごとのタイムアウト戦略

API サービス:一般に 30〜60 秒で十分だ。API の応答は速いはずで、タイムアウトを短く設定すれば遅いリクエストを素早く発見できる。

proxy_connect_timeout 5s;
proxy_read_timeout 30s;
proxy_send_timeout 30s;

ファイル処理:レポートのエクスポートや PDF 生成といった操作は数分かかることがある。タイムアウトは緩める。

proxy_connect_timeout 10s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;

ストリーミングサービス:動画ライブ配信、WebSocket、SSE——これらは長時間接続で、1日に設定しても問題ない。

proxy_read_timeout 86400s;

502/504 調査の実践

原因分析

私が遭遇したいくつかのケース:

  1. 上流サービスが本当に落ちている:プロセスのクラッシュ、ポートの占有、メモリのオーバーフロー
  2. 接続数の枯渇:バックエンドサーバーの接続プールが満杯で、Nginx が接続できない
  3. タイムアウト設定が短すぎる:あの深夜のアラートのように、proxy_read_timeout を 60s に設定したが、バックエンドの処理に 2 分かかった
  4. ファイアウォール/ネットワークの問題:セキュリティグループのルール未設定、iptables がリクエストを遮断

ログによる診断方法

最初のステップは常に error_log を見ることだ。

error_log /var/log/nginx/error.log warn;

よくあるエラーメッセージ:

upstream timed out (110: Connection timed out) while reading response header from upstream

これは 504、読み取りタイムアウトだ。

connect() failed (111: Connection refused) while connecting to upstream

これは 502、接続拒否——バックエンドが Listen していない。

より高度なやり方は、カスタムログフォーマットで upstream のステータスを出力することだ。

log_format upstream_status '$status $upstream_status $upstream_response_time';

access_log /var/log/nginx/access.log upstream_status;

200 200, 200, 502 0.5, 1.2, 3.0 のような出力が見える——どのバックエンドがどのステータスコードを返し、どれだけ時間がかかったかが明確に分かる。

典型的な解決策

ケース1:バックエンドの処理が遅く、頻繁に 504

解決:proxy_read_timeout を大きくし、同時にバックエンドが本当に処理を完了できるか確認する。Nginx だけを調整するのではなく、バックエンドのタイムアウトも同期して調整すること。

ケース2:接続拒否、502

解決:バックエンドのプロセスが稼働しているか、ポートが Listen しているか、ファイアウォールのルールが許可しているかを確認する。

netstat -tlnp | grep 8080
ps aux | grep your_app

ケース3:高並行下での接続数枯渇

解決:バックエンドの接続プールの上限を大きくするか、Nginx の upstream keepalive を有効にして接続作成のオーバーヘッドを減らす。


パフォーマンス最適化のベストプラクティス

Worker 設定

Nginx はマルチプロセスモデルだ。worker_processes でプロセス数を設定し、一般に CPU コア数と同じにする。

worker_processes auto;

auto は自動的に CPU コア数を検出する。マシンが 8 コアなら、8 つの worker プロセスができる。

worker_connections は各 worker が処理できる最大接続数だ。

events {
    worker_connections 4096;
}

理論上の最大同時接続数 = worker_processes × worker_connections。8 コア × 4096 = 32768。ただし実際の値はシステムのファイルディスクリプタ制限の影響を受ける。

TCP 最適化の3点セット

sendfile on;
tcp_nopush on;
tcp_nodelay on;

この3つのパラメータを組み合わせて使うと、パフォーマンスを大きく向上させられる。

  • sendfile on:カーネルレベルのファイル転送を有効にし、ユーザー空間のバッファを経由しない
  • tcp_nopush on:sendfile と連携し、データパケットを1つずつではなくまとめて送信する
  • tcp_nodelay on:小さなデータパケットを即座に送信し、バッファが満たされるのを待たない

静的ファイルサービスでテストしたところ、この3つのスイッチを入れるとスループットが 30% 以上向上した。

30%+
スループット向上
Source: 実測データ:sendfile + tcp_nopush + tcp_nodelay を有効化

その他の最適化

gzip 圧縮:テキスト応答を圧縮して転送し、帯域を節約する。

gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1024;

ファイルディスクリプタ制限:高並行下では足りなくなることがある。システムの制限を確認する。

ulimit -n

1024 しかなければ、引き上げる必要がある。/etc/security/limits.conf を編集する。

* soft nofile 65535
* hard nofile 65535

完全な設定例

本番環境向けの推奨設定テンプレート:

# 基本設定
worker_processes auto;

events {
    worker_connections 4096;
    multi_accept on;
}

http {
    # TCP 最適化
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    # Keepalive
    keepalive_timeout 30;
    keepalive_requests 100;

    # バッファ設定
    proxy_buffering on;
    proxy_buffer_size 4k;
    proxy_buffers 8 32k;
    proxy_busy_buffers_size 64k;

    # タイムアウト設定
    proxy_connect_timeout 10s;
    proxy_read_timeout 60s;
    proxy_send_timeout 60s;

    # gzip
    gzip on;
    gzip_types text/plain text/css application/json;

    upstream backend {
        least_conn;
        server 192.168.1.10:8080 weight=3;
        server 192.168.1.11:8080 weight=2;
        server 192.168.1.12:8080 backup;
        keepalive 32;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://backend;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;

            proxy_next_upstream error timeout http_502 http_503 http_504;
            proxy_next_upstream_tries 3;
        }

        # SSE 専用設定
        location /events {
            proxy_pass http://backend;
            proxy_buffering off;
            proxy_read_timeout 86400s;
        }
    }
}

まとめ

ここまで色々と述べてきたが、核心は3つだけだ。

  1. upstream 設定:適切な負荷分散アルゴリズムを選び、keepalive 接続プールを有効にし、ヘルスチェックを設定する
  2. バッファ設定:3つのパラメータの関係を理解し、特殊なケースではバッファを無効にする
  3. タイムアウト設定:3つのパラメータがそれぞれ何を担当するかを理解し、ケースに応じて異なる戦略を使う

深夜3時のあの障害で学んだことがある。Nginx 設定はパラメータを埋めれば終わり、ではない。どのパラメータの背後にも設計のロジックがあり、原理を理解してはじめて落とし穴を避けられる。

Nginx を触り始めたばかりなら、まずデフォルト設定から始め、問題が出たら的を絞って調整するのをおすすめする——当初の私のように、なんとなく proxy_read_timeout 60s と書いて本番に出すのはやめよう。すでに落とし穴を踏んだ経験があるなら、この記事は散らばった経験を体系へとつなげる助けになるはずだ。

この記事を書き終えたとき、今の自分の本番環境設定を見てみた——keepalive 32、proxy_read_timeout 120s、least_conn 負荷分散。深夜3時のアラートは、二度と現れていない。


FAQ

proxy_read_timeout は総タイムアウトですか、それとも2回の読み取りの間隔ですか?
2回の読み取り操作の間隔です。バックエンドが処理中にデータを送り続ける場合(ハートビートなど)、総処理時間が5分でも proxy_read_timeout 60s で足ります。しかしバックエンドが5分間まったく沈黙する場合は、必ず 300s に設定してください。
proxy_buffering はいつ無効にすべきですか?
3種類のケースでは必ず無効化します。

• Server-Sent Events(SSE):リアルタイム配信。バッファするとメッセージが遅延します
• WebSocket:双方向のリアルタイム通信。ストリーミング転送が必要です
• 大きなファイルのアップロード:メモリ枯渇を避けるため、受信しながら転送します
keepalive の値はどれくらいが適切ですか?
計算式:keepalive ≈ 総QPS × 1リクエストの平均所要時間 ÷ worker プロセス数。たとえば QPS 10000、応答時間 50ms、worker 4つなら keepalive は 125 程度です。高くしすぎないでください。以前 100 に設定してバックエンドをパンクさせたことがあります。
502 と 504 の違いは何ですか?
502 Bad Gateway は Nginx がバックエンドに接続できない(サービス停止、ポート不通、ファイアウォール遮断)ことを示します。504 Gateway Timeout は接続できたが応答がタイムアウトした(バックエンドの処理が遅い)ことを示します。診断方法はまったく異なり、502 はプロセスとポートを、504 はタイムアウト設定とバックエンドの処理時間を確認します。
負荷分散アルゴリズムはどれを選べばよいですか?
ケースに応じて選びます。

• ラウンドロビン(デフォルト):ステートレスなサービス。公平に分配
• least_conn:長時間接続のケース(WebSocket、データベース接続プール)
• ip_hash:セッション維持が必要な場合(暫定策。Redis を使う方がよい)
• hash:分散キャッシュ。ヒット率を高める
upstream sent too big header はどう解決しますか?
proxy_buffer_size を大きくします。バックエンドのレスポンスヘッダーが大きすぎる(Cookie を大量に詰め込んだ場合など)と、デフォルトの 4k バッファを超えます。proxy_buffer_size 16k に変更すれば通常は解決します。
sendfile + tcp_nopush + tcp_nodelay はなぜパフォーマンスを向上させるのですか?
sendfile はユーザー空間を経由せずカーネルで直接転送し、tcp_nopush はまとめて送信してパケット数を減らし、tcp_nodelay は小さなデータを待たずに即座に送信します。3つを組み合わせると、静的ファイルのスループットが実測で 30% 以上向上します。

7分で読めます · 公開日: 2026年3月30日 · 更新日: 2026年6月28日

関連記事

コメント

GitHubアカウントでログインしてコメントできます