TCPの死角:`Connection: close`と`TIME_WAIT`が蝕む超高負荷サーバーの現実
ネットワークエンジニアとして幾千ものパケットキャプチャと向き合ってきた私にとって、TCPコネクションの終端(Teardown)ほど、美しさと残酷さが同居する瞬間はない。
HTTP/1.1のデフォルト挙動であるKeep-Aliveは、私たちのWebインフラを幾度となく救ってきた。しかし、アプリケーションの設計ミス、あるいはプロキシやロードバランサーの仕様によってひとたび `Connection: close` が明示されようものなら、トランスポート層の挙動は劇的に変貌する。
今回は、この `Connection: close` が引き起こすパケットレベルの挙動、Linuxカーネルの深部における `TIME_WAIT` の悪夢、そして極限のパフォーマンスを引き出すためのチューニングの極意を、現場の知見を総動員して紐解いていこう。
—
1. パケットレベルで追う `Connection: close` の実態
HTTP/1.1の基本理念は「永続接続(Persistent Connection)」である。1本のTCPセッション上で複数のリクエストとレスポンスを多重化(厳密には直列化)することで、高価なTCPハンドシェイク(3-way handshake)のオーバーヘッドを打ち消してきた。
しかし、レスポンスヘッダーに以下が含まれた瞬間、あるいはクライアント側からその意思表示がなされた瞬間、パーティーは終わりを迎える。
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Connection: close
このヘッダーを受け取った側(あるいは送信した側)のTCPスタックは、アプリケーション層からのデータ送信完了を検知すると、自ら能動的にコネクション切断の狼煙を上げる。
4ウェイ・ハンドシェイク(Teardown)の悲劇
TCPの切断は、お互いが「もう送るデータはない」と合意するプロセスだ。パケットの流麗なダンスを追ってみよう。
[Client] [Server]
| |
| <--- [FIN, ACK] (Server initiates close) ------ |
| |
| --- [ACK] (Client acknowledges FIN) -----------> |
| |
| — [FIN, ACK] (Client closes its side) ——-> |
| |
| <--- [ACK] (Server acknowledges FIN) ----------- |
| |
X (Client enters TIME_WAIT) X (Server enters TIME_WAIT)
ここで重要なのは、「どちらが先に `FIN` を送ったか」という点である。HTTP/1.1において `Connection: close` を契機としてサーバー側から切断を開始した場合、サーバー側が `FIN` を送信し、クライアントからの最後の `ACK` を受けると、サーバー側が `TIME_WAIT` 状態に突入する。
—
2. `TIME_WAIT` の嵐:なぜサーバーリソースが枯渇するのか
「`TIME_WAIT` なんて、2MSL(Maximum Segment Lifetime、通常はLinuxなら60秒固定)経てば勝手に消えるだろ」と高を括っているテックリードは、一度数万QPS規模のAPIサーバーで `Connection: close` を誘発させてみるといい。地獄のようなポート枯渇現象を目の当たりにするはずだ。
4つ組(4-tuple)の呪縛
TCPコネクションは、以下の4つの要素(4-tuple)で一意に識別される。
1. 送信元IPアドレス
2. 送信元ポート番号
3. 宛先IPアドレス
4. 宛先ポート番号
リバースプロキシや負荷分散装置(ALBなど)の後ろにいるバックエンドサーバーを考えてほしい。宛先IPと宛先ポート(例: 80や443)は常に固定だ。さらに、プロキシからのアクセスであれば、送信元IPアドレスさえも限定的な数個のプライベートIPに絞られることがある。
この状態で、リクエストごとに `Connection: close` が強制されたとしよう。
サーバー側は切断のたびに `TIME_WAIT` を抱え込む。Linuxカーネルは、迷子のパケット(遅延パケット)が新しいコネクションに混入するのを防ぐため、その4つ組の組み合わせを60秒間ロックし続ける。
利用可能なエフェメラルポート(Ephemeral Port)の範囲は理論上最大でも `65535 – 1024 = 64511` だ。秒間数千件の `Connection: close` が発生すれば、わずか数秒で利用可能なポートが底をつき、`Cannot assign requested address`(`EADDRNOTAVAIL`)のエラーがシステムログを埋め尽くすことになる。
—
3. 極限のチューニング:カーネルパラメーターとソケットオプション
この圧倒的なボトルネックを回避するため、私たちはLinuxカーネルの深部へとメスを入れなければならない。安易な設定変更はデータ破損やセキュリティリスク(シーケンス番号の予測攻撃など)を孕むため、プロフェッショナルとしての正確な理解が不可欠である。
`/etc/sysctl.conf` による最適化処方箋
以下のパラメータは、超高負荷なHTTP/1.1環境(特にマイクロサービス間の通信やプロキシ配下)において、私が必ず投入する黄金のレシピだ。
— 1. TIME_WAITソケットの再利用 (TCP Timestampsが必須) —
安全にTIME_WAIT状態のソケットを新規接続に再利用する。
これにより、ポート枯渇のリスクを劇的に軽減できる。
net.ipv4.tcp_tw_reuse = 1
— 2. TCP Timestampsの有効化 —
tcp_tw_reuseを機能させるための前提条件。パケットの往復時間を正確に測る機能も兼ねる。
net.ipv4.tcp_timestamps = 1
— 3. FIN-WAIT-2 タイムアウトの短縮 —
相手からのFINを待つ状態の時間を短縮し、ゾンビコネクションのリソースリークを防ぐ。
net.ipv4.tcp_fin_timeout = 15
— 4. ローカルポート範囲の拡張 —
エフェメラルポートの範囲を広げ、同時接続の許容量を物理的に底上げする。
net.ipv4.ip_local_port_range = 1024 65535
— 5. SYNバックログキューの拡大 —
接続要求の爆発的な増加(SYNフラッドや正当なトラフィック急増)に対処する。
net.ipv4.tcp_max_syn_backlog = 8192
注意すべき「禁忌」:`tcp_tw_recycle` の封印
かつて、`net.ipv4.tcp_tw_recycle = 1` という極端な設定が持て囃された時代があった。これは `TIME_WAIT` をわずか1パケットのやり取りで強制消滅させるものだが、NAT環境(同一グローバルIPから複数のクライアントがアクセスする環境)において、タイムスタンプの逆転現象を引き起こし、パケットがランダムに破棄されるという致命的な通信障害を引き起こす。
現代のLinuxカーネル(Linux 4.12以降で完全削除)では設定すらできないが、古いドキュメントを参考にするエンジニアがいるため、絶対に忘れないでほしい。`tcp_tw_recycle` は「悪魔のパラメータ」である。
—
4. TLSハンドシェイクとHTTP/1.1の構造的限界
現代のWebにおいて、TCPの切断と再接続は、単に「パケットが数枚増える」だけでは済まない。その上位層には TLS(Transport Layer Security) が鎮座しているからだ。
接続クローズがもたらす暗号学的ペナルティ
もしアプリケーションの設計不良やプロトコルの誤認によって `Connection: close` が常態化した場合、何が起きるか。
1. TCPの3-way handshake (1 RTT)
2. TLS 1.3のハンドシェイク (1 RTT、またはTLS 1.2なら2 RTT)
3. HTTPリクエストの送信
4. HTTPレスポンス(`Connection: close` 付き)の受信
5. TCPのTeardown (4パケット)
これだけのオーバーヘッドが、たった1つのリクエストごとに発生する。
TLS 1.3であっても、完全な新規接続には最低でも1.5〜2 RTTのレイテンシが強制される。Keep-Aliveが生きていれば、この高価なハンドシェイクコストを「初回のみ」に償却できるのに対し、`Connection: close` はその恩恵を根底から破壊する。
これが、HTTP/1.1が抱える構造的限界(Head-of-Line Blockingとコネクション管理のジレンマ)であり、HTTP/2やHTTP/3(QUIC)へと世界が移行した最大の理由にほかならない。
—
5. まとめ:アーキテクトが取るべき次の一手
`Connection: close` と `TIME_WAIT` の関係性は、ネットワークインフラの基礎でありながら、システム全体のスループットを握る急所でもある。
現場のテックリードやアーキテクトとして私たちが取るべきアクションは明確だ。
1. アプリケーション層の監査:
意図せず `Connection: close` を送出しているコードや、HTTPクライアントのプール設定(Keep-Aliveが無効になっていないか)を徹底的に洗い出す。
2. リバースプロキシの最適化:
NginxやEnvoyなどのエッジサーバーとバックエンド間のアップストリーム接続において、`keepalive` ディレクティブが適切に設定され、コネクションが正しくプールされているか確認する。
3. カーネルチューニングの適用:
高負荷が予想される環境では、`tcp_tw_reuse` と拡張されたエフェメラルポート範囲を確実に適用し、万が一の切断ラッシュに備える。
パケットの挙動に思いを馳せ、カーネルの息遣いを感じること。それこそが、真に堅牢で高速なネットワークインフラストラクチャを構築唯一の道なのだ。
コメント