目次
- 大原則:計測してから変える
- ボトルネックがNginxにあるとは限らない
- 変更前後の計測手順:同じ条件で、負荷ツールとログの両方を見る
- worker_processesとworker_connections:同時接続の上限を決める
- ワーカー数はコア数、接続数はファイルディスクリプタ上限とセットで
- keepaliveとupstream keepalive:接続の使い回し
- クライアント側:keepalive_timeoutとkeepalive_requests
- バックエンド側:upstreamのkeepaliveは明示しないと効かない
- gzip/brotli圧縮:転送量を減らす
- gzipはデフォルトで無効に近い状態
- sendfile・バッファ・open_file_cache:カーネルとメモリの使い方
- sendfile・tcp_nopush・tcp_nodelay
- client_body_bufferとproxy_buffers
- open_file_cache:大量の静的ファイルを配るときに効く
- トラブル事例:キャンペーン中に502が断続的に発生
- 症状
- 原因
- 対処
- まとめ
「ネットで見つけたnginx.confの“最適設定”をコピーしたが、何が効いているのか、そもそも効いているのかわからない」「アクセスが増えたらエラーログにworker_connections are not enoughと出た」。Nginxは初期設定でもそれなりに速いため、意味を理解しないままパラメータを触ると、効果がないどころか副作用だけを抱えることになります。
この記事では、Nginxの主要なチューニング項目を「なぜその値になるのか」という仕組みから説明し、それぞれの設定例と副作用、そして変更の前後で効果を計測する方法まで扱います。Nginxの基本構築とSSL化はLinux×Nginx入門、リバースプロキシの設定はリバースプロキシとは?で解説済みのため、それらが動いている状態を前提にします。
大原則:計測してから変える
ボトルネックがNginxにあるとは限らない
Nginxをチューニングする前に確認すべきは、遅さの原因が本当にNginxにあるかどうかです。動的なアプリケーションを前段でプロキシしている構成では、応答時間の大半はバックエンドやDBで消費されており、Nginxの設定をどれだけ磨いても数ミリ秒しか変わらないことが珍しくありません。アクセスログに$request_time(Nginxがリクエストを受けてから返し終えるまで)と$upstream_response_time(バックエンドが返すまで)を出力し、両者の差を見れば、Nginx自身が使っている時間が分かります。
# nginx.conf の http ブロック:計測用のログフォーマット
log_format timing '$remote_addr [$time_local] "$request" $status '
'rt=$request_time urt=$upstream_response_time '
'bytes=$body_bytes_sent gzip=$gzip_ratio conn=$connection_requests';
access_log /var/log/nginx/access.log timing;
# 現在の有効な設定をすべて確認(インクルード展開済み)
nginx -T | less
# 接続状況の確認(stub_status を有効にしている場合)
curl -s http://127.0.0.1/nginx_status
# ベースライン計測:100同時接続で30秒(変更前後で同じ条件で実行)
wrk -t4 -c100 -d30s --latency https://example.com/
# 圧縮の効果確認:Accept-Encoding の有無でサイズを比較
curl -sI -H 'Accept-Encoding: gzip' https://example.com/app.js | grep -i -E 'content-encoding|content-length'
curl -sI https://example.com/app.js | grep -i content-length
# 設定変更後のエラー確認
sudo tail -f /var/log/nginx/error.log
rtとurtがほぼ同じならボトルネックはバックエンドで、Nginxのチューニングは後回しです。静的ファイルの配信が遅い、同時接続数が多いときにエラーが出る、転送量が多い、といった場合にNginx側の設定が効きます。
変更前後の計測手順:同じ条件で、負荷ツールとログの両方を見る
チューニングは1項目ずつ変更し、前後を同じ条件で計測して比較します。手順は次のとおりです。
- 変更前に
nginx -Tで現在の設定を保存し、負荷ツールでベースラインを取る。上のwrkの例のように、本番と同程度の同時接続数・時間で実行し、RPSとp95/p99のレイテンシを記録する - 設定を1項目変更し、
nginx -tで文法確認、systemctl reload nginxで無停止反映する - 同じ負荷を再度かけ、RPS・p95/p99・エラー数を比較する。あわせて
topでワーカーのCPU、エラーログで警告の有無を確認する - 効果がなければ元に戻す。「変えたが変わらない」設定を残すと、後で読む人の判断を惑わせる
負荷テストの計画や、p95/p99の読み方は負荷テストのやり方とツール選びを参照してください。Nginxの設定で改善が頭打ちになった場合、次の階層はカーネルパラメータ(somaxconnやポート範囲)で、Linuxカーネルチューニング入門で扱っています。
worker_processesとworker_connections:同時接続の上限を決める
ワーカー数はコア数、接続数はファイルディスクリプタ上限とセットで
Nginxはworker_processesで指定した数のワーカープロセスが、それぞれイベントループで多数の接続を扱います。CPUを待たせない設計なので、ワーカー数はCPUコア数と同じ(auto)にするのが基本で、増やしても並列性は上がりません。各ワーカーが同時に扱える接続数がworker_connectionsで、デフォルトは512です。プロキシ構成では、クライアントとの接続とバックエンドへの接続で1リクエストあたり2つの接続を消費するため、実際に捌けるクライアント数は「ワーカー数 × worker_connections ÷ 2」程度と見積もります。
ここで見落としがちなのが、ワーカープロセスが開けるファイルディスクリプタの上限です。接続1つがファイルディスクリプタ1つを使うため、worker_connectionsを4096にしても、プロセスの上限(ulimit -n、多くの環境で1024)がそれ以下ならエラーになります。worker_rlimit_nofileでNginx自身が上限を引き上げられるので、必ずセットで設定します。
# nginx.conf(メインコンテキスト)
worker_processes auto;
worker_rlimit_nofile 65535; # ワーカーが開けるFD上限。worker_connections以上にする
events {
worker_connections 4096; # 1ワーカーあたりの同時接続数
multi_accept on; # 通知があったら新規接続をまとめて受け付ける
}
接続のモデルやワーカー数の考え方の詳細は同時接続数とワーカー数の見積もりで扱っています。
keepaliveとupstream keepalive:接続の使い回し
クライアント側:keepalive_timeoutとkeepalive_requests
keepalive_timeoutは、クライアントとの接続をリクエスト完了後にどれだけ維持するかで、デフォルトは75秒です。ブラウザからの連続したリクエストがTCPとTLSのハンドシェイクを省略できるため、短くしすぎると再接続が増え、長すぎるとアイドル接続がworker_connectionsを圧迫します。一般的なWebサイトでは15〜65秒程度が多く、ロードバランサーの後ろに置く場合はロードバランサー側のアイドルタイムアウトより長くしておくと、ロードバランサーが再利用しようとした瞬間にNginx側が切っている、という競合を避けられます。keepalive_requestsは1接続で処理するリクエスト数の上限で、デフォルトは1000です(1.19.10より前は100)。
バックエンド側:upstreamのkeepaliveは明示しないと効かない
Nginxからバックエンドへの接続は、デフォルトではHTTP/1.0でリクエストごとに切断されます。アプリケーションサーバー側でTIME_WAITが大量に溜まる、接続確立のオーバーヘッドが応答時間に乗る、といった問題の原因になるため、upstreamブロックでkeepaliveを指定し、プロトコルとConnectionヘッダーを明示します。
upstream app {
server 127.0.0.1:3000;
keepalive 64; # ワーカーごとに保持するアイドル接続の上限
keepalive_requests 1000;
keepalive_timeout 60s;
}
server {
keepalive_timeout 65; # クライアント側
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection ""; # "close" を送らない
proxy_set_header Host $host;
}
}
keepalive 64は「同時接続の上限」ではなく「使い終わった接続を何本までプールに残すか」です。バックエンド側の最大接続数(Gunicornのワーカー数やNode.jsの設定)を超えない範囲で設定してください。TIME_WAITやKeep-Aliveの仕組みそのものはTCPの3ウェイハンドシェイクとTIME_WAITで解説しています。
gzip/brotli圧縮:転送量を減らす
gzipはデフォルトで無効に近い状態
圧縮はテキスト系のレスポンス(HTML、CSS、JS、JSON、SVG)を60〜80%程度小さくできる、費用対効果の高い設定です。しかしNginxのgzipはデフォルトでgzip offで、パッケージの設定でonになっていてもgzip_typesがHTMLしか含まないことが多く、「有効にしたつもりでJSは圧縮されていない」状態が非常によくあります。
gzip on;
gzip_vary on; # Vary: Accept-Encoding を付けてキャッシュ事故を防ぐ
gzip_comp_level 5; # 1〜9。デフォルトは1。5前後で圧縮率とCPUのバランスが良い
gzip_min_length 1024; # 小さすぎるものは圧縮しない(デフォルトは20バイト)
gzip_proxied any; # プロキシ経由のレスポンスも圧縮
gzip_types text/plain text/css text/javascript application/javascript
application/json application/xml image/svg+xml font/woff2;
# ビルド時に .gz / .br を用意している場合、動的圧縮せずそのまま返す
gzip_static on;
# brotli_static on; # ngx_brotli モジュールを導入した場合
Brotliはgzipより圧縮率が高く(テキストで15〜20%程度小さくなることが多い)、主要ブラウザが対応していますが、Nginx本体には含まれておらず、ngx_brotliモジュールを別途組み込む必要があります。動的圧縮はCPUを使うため、圧縮率の高いレベルで毎回圧縮するより、ビルド時に.brと.gzを生成してbrotli_static/gzip_staticで配る方が、CPU負荷ゼロで最大の圧縮率を得られます。画像や動画、woff2フォントは既に圧縮済みなので、gzip_typesに含めても効果はほぼありません(woff2はわずかに縮む場合があります)。
sendfile・バッファ・open_file_cache:カーネルとメモリの使い方
sendfile・tcp_nopush・tcp_nodelay
sendfile onは、静的ファイルをユーザー空間にコピーせず、カーネル内で直接ソケットへ転送する設定で、静的配信では必須に近い設定です。tcp_nopush onはsendfileと組み合わせて、ヘッダーとファイルの先頭を1つのパケットにまとめて送り、パケット数を減らします。tcp_nodelay on(デフォルトで有効)は、Keep-Alive接続で小さなデータを遅延なく送る設定です。3つはセットで書くのが定番で、多くのディストリビューションのデフォルト設定に既に含まれています。
client_body_bufferとproxy_buffers
client_body_buffer_sizeは、リクエストボディ(POSTのフォームやJSON)をメモリに保持するサイズで、デフォルトは64ビット環境で16KBです。これを超えるとNginxは一時ファイルに書き出すため、ファイルアップロードやサイズの大きいJSONを頻繁に受けるAPIではディスクI/Oが発生します。実際のリクエストサイズの分布を見て、大半が収まる値(128KB〜1MB程度)に上げると効果があります。上限はclient_max_body_size(デフォルト1MB、超えると413)で、アップロード機能があるならこちらも忘れずに設定します。
proxy_buffersはバックエンドからのレスポンスをバッファするサイズで、デフォルトは8個 × 4KBまたは8KB(メモリページサイズ依存)です。バックエンドがこれを超える大きなレスポンスを返すと、やはり一時ファイルに書き出され、エラーログにan upstream response is buffered to a temporary fileという警告が出ます。この警告が頻発するなら、proxy_buffers 16 16k;などに拡大します。
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
client_body_buffer_size 128k;
client_max_body_size 20m;
proxy_buffer_size 16k; # レスポンスヘッダー用(大きなCookieがあると不足する)
proxy_buffers 16 16k;
proxy_busy_buffers_size 32k;
# 静的ファイルのメタデータをキャッシュ(後述)
open_file_cache max=10000 inactive=30s; # 最大1万件、30秒アクセスがなければ破棄
open_file_cache_valid 60s; # 60秒ごとに変更がないか再確認
open_file_cache_min_uses 2; # 2回以上使われたものだけキャッシュ
open_file_cache_errors on; # 404 などのエラーもキャッシュ
}
open_file_cache:大量の静的ファイルを配るときに効く
静的ファイルを返すたびに、Nginxはopen()とstat()のシステムコールでファイルの存在とサイズ・更新時刻を確認します。open_file_cacheを有効にすると、ファイルディスクリプタとメタデータを一定時間キャッシュし、これらのシステムコールを省略できます。数千の画像やアセットを高頻度で配信するサイトでは効果がありますが、デプロイでファイルを差し替えた直後に古いメタデータを返す可能性があるため、open_file_cache_valid(再確認の間隔)は短めにしておくのが安全です。設定例は上のhttpブロックの末尾にまとめています。
この設定は、動的なアプリケーションを前段でプロキシするだけの構成ではほとんど効果がありません。「静的ファイルをNginxが直接大量に返す」場合に限って検討してください。
トラブル事例:キャンペーン中に502が断続的に発生
症状
テレビ放映に合わせたキャンペーンサイトで、アクセス集中時に一部のリクエストが502 Bad Gatewayになる。バックエンドのアプリは落ちておらず、CPUにも余裕がある。Nginxのエラーログにはsocket() failed (24: Too many open files) while connecting to upstreamとworker_connections are not enoughが並んでいた。
原因
worker_connectionsを4096に上げていたが、worker_rlimit_nofileを設定しておらず、ワーカープロセスのファイルディスクリプタ上限がOSデフォルトの1024のままだった。プロキシ構成では1リクエストで2つの接続(FD)を使うため、同時500リクエスト程度で上限に達し、バックエンドへの接続を開けずに502を返していた。さらにupstream keepaliveが未設定で、バックエンドへの接続が毎回作られていたことが、FD消費を加速させていた。
対処
worker_rlimit_nofile 65535を追加し、upstreamにkeepalive 64とproxy_http_version 1.1、proxy_set_header Connection ""を設定してリロードした。反映後はcat /proc/$(pgrep -f 'nginx: worker' | head -1)/limits | grep 'open files'でワーカーの上限が実際に上がっていることを確認した。あわせて、事前にwrk -c1000で同時接続1,000の負荷をかけてエラーが出ないことを確かめる項目を、キャンペーン前のチェックリストに追加した。
まとめ
Nginxのチューニングは、まず$request_timeと$upstream_response_timeでNginx自身がボトルネックかを確かめることから始めます。ワーカー数はコア数、worker_connectionsはworker_rlimit_nofileとセットで引き上げ、クライアント側とバックエンド側の両方でkeepaliveを有効にして接続を使い回します。gzipはgzip_typesまで含めて明示し、可能ならビルド時に圧縮した.br/.gzを静的に配ります。sendfile・バッファ・open_file_cacheは、静的配信や大きなリクエスト/レスポンスを扱う場合に効きます。どの設定も、1項目ずつ変えて前後を計測し、効かなければ戻す、という姿勢が最も重要です。キャッシュヘッダーによる転送量削減はHTTPキャッシュヘッダー入門で扱っています。
Webサーバーの性能改善やアクセス集中への備えでお困りの際は、Harmonic Societyのシステム開発・インフラ支援にご相談ください。
Harmonic Society
この記事の内容、自社の業務でも活かせそうですか?
ローカルLLM・AI・クラウドなどの技術導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK。まずは30分の無料相談から。売り込みはしません。
関連記事
Related / 9 articles
Notes & Insights
- プログラミング
DDoS攻撃の仕組みと対策入門|レイヤー別の防御とCDN・クラウドの活用
DDoS攻撃をボリューム型・プロトコル型・アプリ層に分けて仕組みを解説し、自前サーバーで防げない理由、CloudflareやAWS Shieldの標準防御、オリジンIPの隠し方、レートリミットとBot対策、攻撃を受けたときの初動、費用が跳ね上がるDenial of Walletへの備えまでわかります。
- プログラミング
WAFとは?仕組み・導入パターン・誤検知対策|Webアプリを攻撃から守る実践ガイド
WAFがファイアウォールやIDSと何が違うのか、シグネチャとマネージドルールの仕組み、Cloudflare WAF・AWS WAF・ModSecurityの比較、フォーム送信がブロックされる誤検知の調査と例外設定、ログ監視、WAFが代替できないことまで実践的に解説します。
- プログラミング
セキュリティヘッダー入門|CSP・HSTS・X-Frame-Optionsの設定と効果を実践解説
CSP・HSTS・X-Frame-Options・X-Content-Type-Optionsなど主要セキュリティヘッダーが防ぐ攻撃と、CSPのReport-Onlyからの段階導入、nonce/hash、HSTS preloadの不可逆リスク、Nginx・Next.js・Astroでの設定例、確認方法を解説します。
- プログラミング
クラウドの通信費(Egress)入門|データ転送量課金の仕組みと転送コストを抑える設計
クラウドの「受信無料・送信有料」の原則、AZ間・リージョン間・インターネット向けの単価差、NATゲートウェイ処理料の罠、CDNで転送量を減らす方法、バックアップやログ転送の見落とし、請求書で転送料を特定する手順を解説。想定外の請求を防げます。
- プログラミング
秘密情報をGitに入れない仕組み|.gitignore・git-secrets・履歴から漏れた鍵の削除
APIキーや.envをGitにコミットしてしまう典型経路と、.gitignore・.env.exampleの運用、pre-commitでのgitleaks検知、GitHub secret scanningの活用、漏れた鍵の無効化と履歴書き換え(git filter-repo)の手順を解説。仕組みで再発を防げます。
- プログラミング
開発・ステージング・本番環境の分離設計|環境差分をなくす構成とアクセス制御
開発・ステージング・本番それぞれの目的と、構成をコードで揃える方法、環境別の設定注入、本番データを使わないテストデータ戦略、ステージングの保護(Basic認証・IP制限・noindex)、コストを抑える運用までを解説。環境差分による本番障害を防げます。
- プログラミング
ngrok・Cloudflare Tunnelでローカルを公開|Webhook開発とデモ環境の作り方
NAT内のローカル環境にStripeやLINEのWebhookを届けるトンネリングの仕組みを解説。ngrok・Cloudflare Tunnel・localtunnelの比較、固定ドメインと認証、リクエスト検査、公開時のセキュリティ、自宅サーバー公開への応用までわかります。
- プログラミング
ローカル開発環境のHTTPS化|mkcert・hostsファイル・自己署名証明書の正しい使い方
ローカル開発をHTTPS前提にすべき理由(Secure Cookie・Service Worker・OAuth)と、mkcertでローカルCAを作りhostsで独自ドメインを割り当ててVite・Next.js・Dockerで使う手順を解説。証明書警告を無視する癖の危険も理解できます。
- プログラミング
localhost・0.0.0.0・127.0.0.1の違い|ポートとUnixソケットを理解して「つながらない」を解決
localhost・127.0.0.1・0.0.0.0の意味の違い、Dockerで外から接続できない原因、host.docker.internal、ポート競合の調べ方、Unixソケットの利点と権限、1024未満ポートの制約を解説。「つながらない」を仕組みから解決できます。
Harmonic Society
「読んで終わり」にせず、自社の業務で試してみませんか?
AI・ローカルLLM・クラウドの導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK・売り込みなし。
無料・30分・オンラインOK|1営業日以内に返信します