Cover Image for nginxをチューニングしましたよのお話

nginxをチューニングしましたよのお話

Nginx

現在、デプロイしているサービスのSSLターミネーションをするためにnginxでリバースプロキシをしています。
proxy.confにそのまま全部書いていたり、とりあえずlocationで転送しとけばいいやみたいな設定だったので読みづらく、お世辞にもパフォーマンス的に最適化された設定とは言えませんでした。

そこで、geminiちゃんに公式ドキュメントを要約してもらいながらチューニングすることにしました。

いままでの設定例

例がこんな感じです。

設定例
proxy.conf
# 広告ブロック
server {
    listen 443 ssl;
    http2 on;
    server_name ***;

    ssl_certificate /etc/letsencrypt/live/***/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/***/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    location / {
        proxy_set_header Host $host:$server_port; 
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_redirect  http://  $scheme://;
        proxy_pass http://***;
    }
}

# Webアーカイブ
server {
    listen 443 ssl;
    http2 on;
    server_name ***;

    ssl_certificate /etc/letsencrypt/live/***/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/***/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    location / {
        proxy_set_header Host $host:$server_port; 
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_redirect  http://  $scheme://;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_cache_bypass $cookie_session;
        proxy_no_cache $cookie_session;
        proxy_buffers 32 4k;
        proxy_pass http://***;
    }
}

# jellyfin
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name ***;

    ssl_certificate /etc/letsencrypt/live/***/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/***/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self' data:;" always;


    add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), bluetooth=(), camera=(), ch-ua=(), ch-ua-arch=(), ch-ua-bitness=(), ch-ua-full-version=(), ch-ua-full-version-list=(), ch-ua-mobile=(), ch-ua-model=(), ch-ua-platform=(), ch-ua-platform-version=(), clipboard-read=(), clipboard-write=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), speaker-selection=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" always;



    location / {
        proxy_set_header Host $host:$server_port;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_redirect  http://  $scheme://;
        proxy_buffering off;
        proxy_pass http://***;
    }

    location /socket {
        proxy_pass http://***;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade"; 

        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_set_header X-Forwarded-Host $http_host;
    }
}

# Immich
server {
    listen 443 ssl;
    http2 on;
    server_name ***;

    client_max_body_size 50000M;    

    ssl_certificate /etc/letsencrypt/live/***/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/***/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
    send_timeout       600s;
    proxy_set_header Host              $http_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_http_version 1.1;
    proxy_set_header   Upgrade    $http_upgrade;
    proxy_set_header   Connection "upgrade";
    proxy_redirect     off;
    proxy_set_header X-Forwarded-Proto https;

    location / {
        proxy_pass http://***;
    }
}

とまあこのように、プロキシパラメータが重複していたり、意味のない設定が入っていたりと読みづらく遅い設定でした。

変えたもの

公式ドキュメントを読んでいい感じにチューニングしました。

プロキシパラメータの切り出し

共通するパラメータを切り出して、別ファイルで管理することで可読性をあげました。

snippets/proxy-params.conf
proxy_http_version 1.1;

# Hostヘッダーは$hostに統一
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;

# WebSocketのための設定 (mapディレクティブと連携)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_redirect off;

セキュリティヘッダーの切り出し

セキュリティヘッダーも共通化して管理することで、抜けを防いで統一的な設定にしました。
なお、CSP設定は煩雑なのでいまのところ省いています。

snippets/security-headers.conf
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;

SSLパラメータの切り出し

これも目的はセキュリティヘッダーと同じです。
TLSv1.3のみ対応としました。

ssl-params.conf
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';

nginx.confの調整

nginx.confをいじって最適化を行いました。

multi_acceptの有効化

multi_acceptをonにして、複数接続を効率よく受けられるようにしました。

各種パラメータの統一

全体的なパフォーマンスパラメータを統一して管理しやすくしました。
また、有効と思われるパラメータを追加しています。

nginx.conf
# パフォーマンス最適化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;

# 圧縮設定
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

Websocket用のmap設定

mapディレクティブを使用して、Websocketが必要な接続だけ自動で判定して適用する設定にしました。

nginx.conf
# $http_upgrade変数を元に$connection_upgrade変数の値を決定する
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      '';
}

リバースプロキシの設定

プロキシ先をupstreamで定義

これまではリクエストごとにバックエンドへ新しい接続を確立していましたが、upstreamディレクティブで定義することで、keepaliveを使用できるようにして全体的なパフォーマンスを向上させました。

conf.d/01-upstreams.conf
upstream photos_backend {
    server ***;
    keepalive 16;
}

upstream pydio_cells_backend {
    server ***;
    keepalive 128;
}

共通設定の読み込み

ほとんどのパラメータを共通化したおかげで、読みやすい設定ファイルになりました。
別途必要なパラメータを追加するだけで良いので、管理がしやすく重複パラメータもなくなりました。

conf.d/02-proxy.conf
server {
    listen 443 ssl;
    http2 on;
    server_name ***;

    ssl_certificate /etc/letsencrypt/live/***/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/***/privkey.pem;

    # --- 共通設定の読み込み ---
    include snippets/ssl-params.conf;
    include snippets/security-headers.conf;

    location / {
        include snippets/proxy-params.conf;
        proxy_pass http://adguard_backend;
    }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name ***;

    ssl_certificate /etc/letsencrypt/live/***/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/***/privkey.pem;

    include snippets/ssl-params.conf;
    # Jellyfinは公式の推奨設定を使うため、共通スニペットは使わず元の設定を維持
    add_header X-Content-Type-Options "nosniff" always;
    add_header Content-Security-Policy "default-src https: data: blob: ; img-src 'self' https://* ; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://www.gstatic.com https://www.youtube.com blob:; worker-src 'self' blob:; connect-src 'self'; object-src 'none'; frame-ancestors 'self'; font-src 'self' data:;" always;
    add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), bluetooth=(), camera=(), ch-ua=(), ch-ua-arch=(), ch-ua-bitness=(), ch-ua-full-version=(), ch-ua-full-version-list=(), ch-ua-mobile=(), ch-ua-model=(), ch-ua-platform=(), ch-ua-platform-version=(), clipboard-read=(), clipboard-write=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), speaker-selection=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()" always;

    # ストリーミングのためにバッファリングを無効化
    proxy_buffering off;

    # WebSocket用の /socket location は不要
    # proxy-params.conf がWebSocketリクエストを自動で処理
    location / {
        include snippets/proxy-params.conf;
        proxy_pass http://jellyfin_backend;
    }
}

この他にもclient_max_body_sizeやproxy_read_timeoutなどを適宜追加して、ファイルサーバーなどの大容量転送が必要なサービスが長時間アップロード処理ができるようにしています。

また、upstream設定より先にproxy設定が読み込まれてしまうとundefinedでエラーになるため、01と02で読み込み順を指定しています。

カーネルチューニング

ついでと言ってはなんですが、プロキシサーバー側で10Gbps通信を効率よくさばくためにカーネルパラメータもいじりました。

sysctl.conf
# === ネットワークパフォーマンスチューニング (10Gbps向け) ===

# TCP/IPスタックのコア設定
# ---------------------------------
# TCP接続要求のバックログキュー。高トラフィックに備え大幅に増やす。
net.core.somaxconn = 16384

# ネットワークデバイスドライバが保持できるパケットの最大数。
net.core.netdev_max_backlog = 65536

# UDPバッファの最大サイズ(デフォルト)
net.core.rmem_default = 262144
net.core.wmem_default = 262144


# TCPバッファサイズのチューニング (BDP: Bandwidth-Delay Productを考慮)
# ---------------------------------
# レイテンシが多少あっても10Gbpsを使い切れるように、バッファサイズを大きく確保します。
# (最大32MBに設定)
net.core.rmem_max = 33554432
net.core.wmem_max = 33554432

# TCP 受信/送信バッファ (min, default, max)
# 4KB, 16KB, 16MB -> 4KB, 64KB, 32MBへ変更
net.ipv4.tcp_rmem = 4096 65536 33554432
net.ipv4.tcp_wmem = 4096 65536 33554432


# TCPの挙動に関するチューニング
# ---------------------------------
# TCP輻輳制御アルゴリズムにBBRを使用 (要カーネルサポート)
# パケットロスに強く、スループットを最大化します。
net.ipv4.tcp_congestion_control = bbr

# TCPウィンドウのスケーリングを有効化 (必須)
net.ipv4.tcp_window_scaling = 1

# TIME_WAIT状態のソケットの高速な再利用
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30

# TCP Slow Startの後、輻輳ウィンドウをより速く成長させる
net.ipv4.tcp_slow_start_after_idle = 0

# 最大TCP SYNバックログを増やす
net.ipv4.tcp_max_syn_backlog = 8192

ここはgeminiくんに書いてもらいました。

最終的なファイルたち

/etc/nginx/
├── nginx.conf
|
└── conf.d/
|   ├── 01-upstreams.conf
|   |
|   └── 02-proxy.conf
|
└── snippets/
    ├── ssl-params.conf
    ├── proxy-params.conf
    └── security-headers.conf

なんということでしょう!
ごちゃごちゃして読みづらかった設定が、こんなにも綺麗になりました!

数字で比較をとっていたわけではないのでプラシーボかもしれませんが、少なくとも個人的には爆速になったと感じています。
特に、pydioやarchiveboxなどのサービスがキビキビ動くようになった気がします。

おわり

設定ファイルはきれいにしておきましょう。
もしかしたら不適切な設定があるかもしれませんが、速くなったので気にしないことにします。