Skip to content
Published on

ingress-nginx プロダクションチューニング — 性能、タイムアウト、コネクション、keepalive

Authors

はじめに

ingress-nginx をインストールして Ingress を 1 つ 2 つ立ち上げるのは難しくありません。本当の難しさはトラフィックが増えてから始まります。普段は問題ない Ingress がピーク時に断続的に 502 を返し、大容量アップロードは 413 で弾かれ、バックエンドが遅い日には 504 が大量発生します。さらにデプロイのたびに reload が起きてレイテンシが跳ねることもあります。

本記事は ingress-nginx をプロダクションで安定して回すためのチューニングノウハウを整理します。worker とコネクションから keepalive、タイムアウト、バッファリング、圧縮、レートリミット、スケーリング、graceful reload、そして 502/504 のデバッグフローまで、実務ですぐ適用できる形で扱います。ただし、すべての設定の正解はトラフィック特性によって変わるので、数値は出発点として受け取り、メトリクスで検証してください。

worker とコネクションのチューニング

nginx の処理能力は結局、worker プロセス数と各 worker が扱えるコネクション数の積で決まります。ingress-nginx ではこれを ConfigMap で制御します。

apiVersion: v1
kind: ConfigMap
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
data:
  worker-processes: "auto"
  max-worker-connections: "16384"
  max-worker-open-files: "65536"
  • worker-processes を auto にするとコンテナに割り当てられた CPU 数に合わせます。CPU limit が明確なら、その値と整合させてください。
  • max-worker-connections は worker 1 つが同時に開けるコネクションです。クライアントと upstream の両方を数えるので余裕を持たせつつ、ファイルディスクリプタ上限(max-worker-open-files)と一緒に上げる必要があります。

worker が CPU より過剰だとコンテキストスイッチのオーバーヘッドが、少なすぎると処理量がボトルネックになります。一般的には CPU コア数に 1:1 で合わせる auto が無難です。

upstream keepalive

性能チューニングで最も効果が大きい項目の一つが upstream keepalive です。デフォルト設定ではバックエンドへのリクエストごとに TCP 接続を新規に張ることがあり、これは TLS ハンドシェイクと TIME_WAIT ソケットを量産します。

data:
  upstream-keepalive-connections: "320"
  upstream-keepalive-requests: "10000"
  upstream-keepalive-timeout: "60"

upstream-keepalive-connections は各 worker がバックエンドと維持する idle コネクションプールのサイズです。これを有効にするとコネクション再利用によりレイテンシと CPU が同時に下がります。ただしバックエンド(特に keep-alive を短く切るアプリケーションサーバ)と timeout がずれると、かえって race による 502 が発生し得るので、バックエンドの keepalive より短く設定するのが安全です。

3 種のタイムアウト

タイムアウトは 504 と 502 を分ける核心です。最もよく扱う 3 つを明確に区別しましょう。

項目意味アノテーション
connect timeoutバックエンド TCP 接続確立の待機proxy-connect-timeout
send timeoutリクエストをバックエンドへ送る間の待機proxy-send-timeout
read timeoutバックエンド応答を読む間の待機proxy-read-timeout
metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "5"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "60"

connect timeout が長いとバックエンドダウン時の障害検知が遅くなるので、通常は短く(数秒)します。read timeout は遅いバックエンド処理(レポート生成など)を考慮して十分に取りつつ、無制限に伸ばすと worker コネクションが枯渇します。504 が頻発するなら、read timeout と実際のバックエンド処理時間を併せて比較してください。

proxy buffering

バッファリングは遅いクライアントからバックエンドを守る仕組みです。nginx がバックエンド応答をバッファに受けておくと、バックエンドは素早く応答を終えて次のリクエストを処理できます。

data:
  proxy-buffering: "on"
  proxy-buffer-size: "8k"
  proxy-buffers-number: "4"

ただしストリーミング(SSE、長いダウンロード、gRPC ストリーム)ではバッファリングがかえって応答を遅らせます。こうした経路では Ingress 単位でバッファリングを切ってください。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-buffering: "off"

gzip と brotli 圧縮

テキスト応答(JSON、HTML、JS)は圧縮で帯域と体感速度を大きく改善できます。

data:
  use-gzip: "true"
  gzip-level: "5"
  gzip-types: "application/json application/javascript text/css text/plain"

圧縮レベルは CPU と圧縮率のトレードオフで、通常は 4〜6 程度が適切です。brotli はビルドにモジュールが含まれている必要があり、静的アセットでは gzip より高い圧縮率を示します。すでに圧縮済みのコンテンツ(画像、動画)は圧縮対象から除外してください。

大容量アップロード

デフォルトでは 1MB を超えるボディが 413 で弾かれます。ファイルアップロード経路ではボディサイズと一緒にバッファ/タイムアウトを調整する必要があります。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "200m"
    nginx.ingress.kubernetes.io/proxy-request-buffering: "off"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "300"

proxy-request-buffering を off にすると、nginx がリクエストボディを全部受け取る前にバックエンドへストリーミングするので、大容量アップロードでディスク/メモリ使用を減らせます。ただしバックエンドがストリーミングボディを処理できる必要があります。

レートリミットアノテーション

ingress-nginx は Ingress 単位のレートリミットをアノテーションで提供します。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/limit-rps: "20"
    nginx.ingress.kubernetes.io/limit-burst-multiplier: "3"
    nginx.ingress.kubernetes.io/limit-connections: "10"

limit-rps はクライアント IP あたりの毎秒リクエスト数、limit-connections は同時接続数です。burst multiplier で瞬間的なスパイクを一部許容します。ただしこれはコントローラ Pod 単位でカウントされるため、コントローラが複数だと精密な全体制限は難しくなります。厳密な全体レートリミットが必要なら、API Gateway 層を別に置くほうがよいでしょう。

HPA とコントローラのスケール

ingress-nginx コントローラ自体もトラフィックに合わせてスケールする必要があります。コントローラは通常 Deployment または DaemonSet で配置し、Deployment なら HPA を付けられます。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: ingress-nginx-controller
  minReplicas: 3
  maxReplicas: 12
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

コントローラは単一障害点になってはならないので minReplicas を 2 以上(できれば 3)にし、ノード分散のための topologySpreadConstraints や anti-affinity を併せて設定してください。スケールアウト時、新しい Pod がウォームアップする間に一時的な reload コストがある点も考慮が必要です。

graceful reload と drain

デプロイや設定変更時に reload は避けられません。肝心なのは reload と Pod 終了が進行中の接続を切らないようにすることです。

data:
  worker-shutdown-timeout: "240s"
# Pod spec
terminationGracePeriodSeconds: 300
lifecycle:
  preStop:
    exec:
      command: ["/wait-shutdown"]

worker-shutdown-timeout は reload 後に既存の worker が進行中のリクエストを終えるまで待つ時間です。preStop フックと十分な terminationGracePeriod で、Pod が終了する際に in-flight リクエストをドレインさせます。readiness probe を通じて終了中の Pod をロードバランサプールから先に外すことも重要です。

メトリクスに基づく容量見積もり

チューニングは推測ではなくメトリクスで行うべきです。ingress-nginx は Prometheus メトリクスを公開します。

data:
  enable-metrics: "true"

主な観察指標は次のとおりです。

指標意味活用
nginx_ingress_controller_requestsリクエスト数(ステータスコード別)トラフィック/エラー率
request_duration_secondsリクエスト処理時間の分布レイテンシ p50/p95/p99
nginx_ingress_controller_nginx_process_connectionsアクティブコネクションコネクション上限の点検
nginx_ingress_controller_config_last_reload_successfulreload 成功フラグreload 失敗の検知

p99 レイテンシとアクティブコネクションの推移を見ながら worker-connections と keepalive プールを調整し、エラー率の急増時点と reload 時点を重ねて見れば、reload が原因かバックエンドが原因かを切り分けられます。

セッションアフィニティと負荷分散アルゴリズム

ステートを持つアプリケーションやキャッシュ効率のために、同じクライアントを同じバックエンドへ送りたいことがあります。ingress-nginx はクッキーベースのセッションアフィニティを提供します。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/affinity-mode: "persistent"
    nginx.ingress.kubernetes.io/session-cookie-name: "route"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "3600"

ただしセッションアフィニティは負荷分散のバランスを崩しかねません。特定のバックエンドにトラフィックが偏るとスケーリングの意味が薄れるので、本当に必要な経路だけに適用し、できればアプリケーションをステートレスに設計するのがよいでしょう。

負荷分散アルゴリズムもグローバル ConfigMap で調整できます。

data:
  load-balance: "ewma"

デフォルトは round_robin ですが、ewma(Exponentially Weighted Moving Average)は各バックエンドの最近の応答時間を重みにして、より速いバックエンドへトラフィックを送ります。バックエンド応答時間のばらつきが大きい環境で p99 レイテンシを改善することが多いです。

コネクション処理とクライアント側 keepalive

ここまで upstream(バックエンド)方向の keepalive を扱ってきましたが、クライアント方向の keepalive も重要です。クライアントとの接続を適切に維持すると TLS ハンドシェイクのコストを減らせます。

data:
  keep-alive: "75"
  keep-alive-requests: "1000"

keep-alive はクライアント接続を維持する時間(秒)、keep-alive-requests は 1 接続で処理する最大リクエスト数です。CDN やロードバランサが前段にあるなら、そちらの idle timeout と整合させてください。前段 LB の idle timeout が ingress の keep-alive より長いと、すでに閉じられた接続を LB が再利用しようとして 502 が発生し得るので、ingress 側を長めにするのが安全です。

負荷シナリオと落とし穴

最もよくある落とし穴は reload の頻発です。ConfigMap、Secret、または Ingress が頻繁に変わると nginx が reload を繰り返し、reload のたびに worker が新しく立ち上がってメモリとコネクションが揺れます。cert-manager が証明書を頻繁に更新したり、自動化が Ingress を繰り返し編集したりする場合に特に危険です。変更をまとめ、頻度を下げてください。

[ 502 vs 504 原因フローチャート ]

リクエスト失敗
   ├─ 504 Gateway Timeout?
   │     │
   │     ├─ バックエンド応答が遅い ──▶ proxy-read-timeout 確認 + バックエンド処理時間
   │     └─ バックエンド接続遅延 ──▶ proxy-connect-timeout + バックエンドヘルス
   └─ 502 Bad Gateway?
         ├─ upstream keepalive race ──▶ keepalive-timeout をバックエンドより短く
         ├─ バックエンドが早く接続を切る ──▶ バックエンド keep-alive 設定
         ├─ バックエンド OOM/クラッシュ ──▶ Pod ログ/再起動を確認
         └─ reload 中の一時的なもの ──▶ worker-shutdown-timeout/グレースを調整

504 はほぼ常に「遅さ」の問題(タイムアウトまたは遅いバックエンド)で、502 は「切断」の問題(接続が不適切に終了)です。この 2 つをまず区別すれば、原因をはるかに速く絞り込めます。

WebSocket と gRPC の長寿命接続

WebSocket と gRPC ストリーミングは通常の HTTP リクエストと異なるチューニングが必要です。接続が長く維持されるため、read/send タイムアウトが短いと正常な接続が切れます。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/backend-protocol: "GRPC"

WebSocket は ingress-nginx が Upgrade ヘッダを自動的に処理するので追加設定は少ないですが、タイムアウトは十分に伸ばす必要があります。gRPC は backend-protocol を GRPC に指定して初めて HTTP/2 ベースのプロキシが動作します。長寿命接続が多いサービスは worker-connections の上限に早く到達し得るので、コネクションメトリクスを普段より注意深く観察してください。

グローバル設定と Ingress 設定の優先順位

同じオプションを ConfigMap(グローバル)とアノテーション(Ingress)の両方で指定すると、どちらが勝つでしょうか。一般的にはアノテーションがその Ingress に限ってグローバル値を上書きします。これにより、グローバルのデフォルトを保守的にしておき、特定経路だけ例外処理するパターンが可能になります。

階層適用範囲優先順位
ConfigMapコントローラ全体低(デフォルト)
アノテーション個別 Ingress高(上書き)

たとえば proxy-body-size をグローバル ConfigMap で 10m にし、アップロード経路の Ingress だけアノテーションで 200m を指定する、という具合です。こうすると大多数の経路は保守的な上限で保護しつつ、必要な経路だけ緩和できます。ただし snippet 無効化ポリシーや risk-level のようなセキュリティ関連のグローバル設定は、アノテーションで無力化されないよう設計されている点を覚えておいてください。

ウォームアップとスケールアウトの安定化

スケールアウト直後の新しいコントローラ Pod はキャッシュが空で、upstream keepalive プールも空なので、一時的にレイテンシが高くなります。readiness probe が早く通りすぎると、準備できていない Pod にトラフィックが流れて 502 が跳ねることがあります。

# Pod spec
readinessProbe:
  httpGet:
    path: /healthz
    port: 10254
  initialDelaySeconds: 10
  periodSeconds: 5
  successThreshold: 1
  failureThreshold: 3

新しい Pod が十分にウォームアップしてからトラフィックを受けるよう initialDelaySeconds を適切に設定し、HPA のスケールアウト速度を攻撃的にしすぎないのが安定的です。トラフィック急増が予測可能なら(イベント、セールなど)事前スケーリング(prewarming)を検討してください。

Gateway API との関係

性能チューニングの観点でも 2026 年の流れを念頭に置く必要があります。ここで扱った worker/コネクション/keepalive/タイムアウトの概念自体は nginx データプレーンの本質なので、Gateway API 実装(特に NGINX Gateway Fabric のように nginx をデータプレーンに使う場合)でもほぼそのまま適用されます。ただし表現方法はアノテーションではなく標準 CRD やポリシーリソースに変わります。Ingress API が凍結された以上、新しいチューニングポリシーはできれば Gateway API のポリシーモデルで設計しておくほうが長期的に有利です。

タイムアウト予算と端から端への一貫性

タイムアウトは単一コンポーネントではなく、経路全体で一貫している必要があります。クライアント → LB → ingress → バックエンド → DB と続くチェーンで各段階のタイムアウトがずれると、一方はすでに諦めているのに他方は待ち続ける、という無駄が生じます。

[ 端から端へのタイムアウト予算の例 ]

クライアント     LB           ingress        バックエンド    DB
  30s    ──▶   28s   ──▶    25s    ──▶     20s   ──▶   10s
 (最も長い)                                        (最も短い)

原則は、外側ほど長く、内側ほど短くすることです。内側(DB)のタイムアウトが外側(ingress)より長いと、ingress が 504 で切った後もバックエンドは DB 応答を待ち続け、コネクションと資源を占有します。これは負荷が集中したときに資源枯渇を加速します。逆に内側を短くすれば、遅い処理は素早く失敗し、リトライポリシーがきれいに動作します。

ingress の proxy-read-timeout は、この予算の中でバックエンド処理時間より少し長くしつつ、LB の idle timeout よりは短くして、「誰が先に切るか」を予測可能にするのが肝です。

たとえば上記の予算を Ingress に反映すると次のようになります。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "3"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "25"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "25"

このように段階的な予算を明示的にドキュメント化し、各コンポーネントに反映すれば、障害時に「どこで切れたか」を推定するのがずっと簡単になります。タイムアウトは 1 つの数字ではなく、経路全体の契約だと考えてください。

実践的なチューニング適用手順

ここまで扱った項目を一気に適用すると、どの変更がどの効果を生んだか分からなくなります。次の手順で段階的に適用することを推奨します。

[ チューニング適用ワークフロー ]

1. ベースライン測定 ── 現在の p50/p95/p99、エラー率、コネクション、reload 頻度を記録
2. 仮説の設定 ────── どの指標をどの変更で改善するか明確に
3. 単一変更 ──────── 一度に 1 つのオプションだけ変更(ステージング優先)
4. 負荷テスト ────── 実際のトラフィックパターンを模した負荷を投入
5. 比較/判断 ────── ベースライン比で改善/回帰を確認
6. ロールアウト/バック ── 改善なら本番カナリア、回帰なら即ロールバック

最もよくある失敗は、複数のオプションを同時に変えることです。keepalive、タイムアウト、バッファをまとめて調整すると、p99 が良くなっても原因を特定できず、後で回帰が出てもどこに起因するか追跡しづらくなります。一度に 1 つずつ、メトリクスで検証する規律が、結局は最も速い道です。

負荷テストツールには k6、vegeta、wrk などがよく使われます。単純な RPS だけでなく、同時接続数、ボディサイズ、長寿命接続の割合など、実サービスのトラフィック形状を模さないと意味のある結果は得られません。

よく使う ConfigMap オプションのまとめ

最後に、プロダクションでよく調整するグローバルオプションを 1 つの表にまとめます。

オプション役割出発点
worker-processesworker 数auto
max-worker-connectionsworker あたりコネクション16384
upstream-keepalive-connectionsバックエンド idle プール320
keep-aliveクライアント keepalive 秒75
proxy-body-sizeボディサイズ上限用途別
use-gzipgzip 圧縮true
load-balance分散アルゴリズムround_robin または ewma
worker-shutdown-timeoutgraceful 終了待機240s

出発点であって答えではありません。各環境のメトリクスで必ず検証してください。

要点まとめ

チューニングで覚えておくべき原則を整理します。

  • 推測せず、ベースラインを測定してから一度に 1 つずつ変え、メトリクスで検証する。
  • worker-processes(auto)と worker-connections で容量の上限を決める。
  • upstream keepalive は効果が大きいが、バックエンドの keepalive より短くして 502 race を避ける。
  • connect は短く、read/send は処理特性に合わせる。ただし LB より短くして切断主体を予測可能にする。
  • ストリーミング/アップロード経路はバッファリングを切り、テキスト応答は圧縮する。
  • reload の頻発が最もよくある落とし穴。変更をまとめ、GitOps で頻度を下げる。
  • 504 は「遅さ」、502 は「切断」。この区別がデバッグ速度を左右する。

これらの原則は nginx データプレーンの本質なので、Gateway API へ移っても通用します。

おわりに

ingress-nginx チューニングの核心は、データプレーンの動作原理を理解し、推測ではなくメトリクスで検証することです。worker とコネクションで容量を決め、upstream keepalive で効率を引き上げ、3 つのタイムアウトで障害を素早く検知し、バッファリングと圧縮で資源を節約し、graceful reload で無停止を守ります。そして 502 と 504 を区別するフローを覚えておくだけで、障害対応の速度が大きく変わります。これらの感覚は Gateway API の時代へ移っても、そのまま資産になります。

参考資料