パケットはなぜ瞬時に流れないのか?HTTP/1.1 Keep-Aliveとタイムアウトの深淵
ネットワークエンジニアの悲劇は、往々にして「リクエストは正常なのに、なぜかサイトがもたつく」という謎のレイテンシと向き合う瞬間に訪れる。
パケットキャプチャを開き、TCPのシーケンス番号とタイムスタンプを追いかけていく。すると、そこには美しくない現実が広がっている。HTTP/1.0の時代を引きずった過剰なTCPハンドシェイクの嵐、TLSのフルハンドシェイクが刻む無駄な往復(RTT)、そして不適切に設定されたタイムアウト値によって枯渇していくサーバーのソケットプール。
HTTP/2やHTTP/3といった次世代プロトコルが叫ばれて久しい現在でも、インターネットの基盤を支えるトラフィックの相当部分は、いまだにHTTP/1.1の上を流れている。そして、そのHTTP/1.1のパフォーマンスを極限まで引き出せるか否かは、エンジニアが「Keep-Alive」と「タイムアウト」という、地味ながらも極めてクリティカルなパラメータの本質を理解しているかにかかっている。
今回は、TCPのステートマシン、Linuxカーネルの内部挙動、そして実務の現場でインフラエンジニアが直面する設計の急所まで、パケットレベルの解像度で深く掘り下げていこう。
—
1. Keep-Aliveのメカニズム:TCPコネクションの「再利用」がもたらすパラダイムシフト
HTTP/0.9やHTTP/1.0の初期において、Webの基本は「1リクエスト・1レスポンスにつき1つのTCPコネクション」だった。
ブラウザがHTMLを取得し、その中に含まれる10個の画像やスタイルシートを読み込もうとすると、何が起きるか?
ブラウザはサーバーに対して10回のTCP 3ウェイハンドシェイク(SYN -> SYN-ACK -> ACK)を行い、HTTPリクエストを投げ、レスポンスを受け取ると即座に`FIN`パケットを送ってコネクションを切断していた。
ここで思い出してほしいのが、TCPのスロースタート(Slow Start)アルゴリズムとTIME_WAIT状態の呪縛だ。
1. RTT(Round Trip Time)の重複: コネクションを張るたびに、データの送受信が始まる前にネットワークの往復遅延がコストとして発生する。
2. TCPウィンドウの制約: 新規コネクションは混雑回避のために小さな混雑ウインドウ(Cwnd)からスタートするため、瞬時に帯域を使い切れない。
3. TIME_WAITの枯渇: クライアントやサーバーが頻繁にコネクションを切断すると、OSのローカルポートがTIME_WAIT状態に張り付き、高負荷時には「Cannot assign requested address」という致命的なエラーを引き起こす。
HTTP/1.1 Persistent Connectionの登場
この無駄を根絶するためにHTTP/1.1で標準化されたのが、Persistent Connection(持続的接続)、すなわち「Keep-Alive」だ(※なお、HTTP/1.0でも独自拡張としてKeep-Aliveヘッダーは存在したが、HTTP/1.1でデフォルトの挙動となった)。
[TCP 3ウェイハンドシェイク] (1回のみ)
Client ——————————> Server (SYN)
Client <------------------------------ Server (SYN-ACK)
Client ------------------------------> Server (ACK)
[HTTPリクエスト/レスポンス 1回目]
Client ——————————> Server (GET /index.html)
Client <------------------------------ Server (200 OK + Body)
[HTTPリクエスト/レスポンス 2回目 (同一TCPコネクションを再利用)]
Client ------------------------------> Server (GET /style.css)
Client <------------------------------ Server (200 OK + Body)
[アイドルタイムアウトまたは明示的切断]
Client ------------------------------> Server (FIN)
Client <------------------------------ Server (FIN-ACK)
一度確立したTCPセッション上で複数のHTTPトランザクションを連続して処理することで、TCPハンドシェイクのオーバーヘッドとスロースタートのペナルティを最初の1回に圧縮できる。これが、Webパフォーマンス最適化の第一歩である。
---
2. サーバー側のタイムアウト設定:コネクションプール効率のジレンマ
TCPコネクションを維持し続ける(Persistentにする)ことは素晴らしい。しかし、ここにインフラアーキテクトを悩ませる「リソースのジレンマ」が存在する。
サーバー側でコネクションを繋ぎっぱなしにするということは、Linuxカーネル上でファイルディスクリプタ(FD)とTCPソケットバッファを専有し続けることを意味する。もし、クライアントがブラウザを閉じたり、モバイル端末がトンネルに入って通信不能になったりしたとき、そのコネクションはどうなるのか?
ここで重要になるのが、サーバー側のKeep-Aliveタイムアウト設定だ。
代表的なWebサーバーにおける設定例(Nginx)
Nginxを例に取ってみよう。Nginxのデフォルト設定は堅牢だが、高トラフィック環境ではチューニングが必須となる。
http {
# クライアントとのKeep-Alive接続を維持する最大時間
# ブラウザ側のアイドル時間がこれを超えると、サーバーからFINを送る
keepalive_timeout 65s;
# 単一のKeep-Aliveコネクション上で処理できる最大リクエスト数
# これを超えると、サーバー側からコネクションを切断し、クライアントに再接続を促す
keepalive_requests 1000;
# クライアントからのリクエストヘッダーを読み込むタイムアウト
client_header_timeout 10s;
# クライアントからのリクエストボディを読み込むタイムアウト
client_body_timeout 10s;
}
このパラメータチューニングの妙味は、「リソースの有効活用(コネクションプール効率)」と「サーバーのメモリ・FD枯渇防止(耐障害性)」のトレードオフにある。
- タイムアウトを短くしすぎた場合 (`keepalive_timeout 2s` など):
コネクションがすぐに切断されるため、ページ内の複数アセットを取得する際に再びTCPハンドシェイクとTLSハンドシェイクが発生し、レイテンシが劇的に悪化する。
- タイムアウトを長くしすぎた場合 (`keepalive_timeout 300s` など):
悪意あるクライアントや、放置されたセッションによってサーバーのプロセス(またはワーカー)が埋め尽くされ、新たな正当なユーザーからの接続を受け付けなくなる(いわゆるSlowloris攻撃やリソース枯渇状態)。
適切な設定値は、サービスの性質(APIサーバーなのか、静的アセットを配信するCDNエッジなのか、動的なECサイトなのか)によって厳密にプロファイルされるべきである。一般的に、モダンなWebアプリケーションであれば、`keepalive_timeout` は 15秒〜75秒 の間に設定されることが多い。
—
3. パケット・トランスポート層の最適化:RTT削減とTCPバッファチューニング
Keep-Aliveを有効に活かすためには、トランスポート層(TCP)および暗号化層(TLS)の挙動を深く理解し、Linuxカーネルパラメータを適切に調教する必要がある。
TLSハンドシェイクとの協調
HTTPS(HTTP over TLS)環境下では、Keep-Aliveはさらに強力な意味を持つ。
TLS 1.2であれば「TCPハンドシェイク(1 RTT) + TLSハンドシェイク(2 RTT)」が必要であり、最初のページ表示までに少なくとも3往復の遅延が発生する。Keep-Aliveが効いている状態であれば、2回目以降のリクエストはこの重厚長大なハンドシェイクを完全にバイパスできる。
さらに、TLS 1.3ではハンドシェイクが1 RTTに短縮されたが、それでもTCPの再利用に勝る効率はない。また、TLSのセッション再開(Session Resumption / Session Tickets)とHTTP/1.1のKeep-Aliveを組み合わせることで、インフラのオーバーヘッドは限界まで削ぎ落とされる。
Linuxカーネルチューニング(sysctl.conf)の実践
高負荷なHTTP/1.1サーバー運用において、`/etc/sysctl.conf` に記述すべき実践的なパラメータを見てみよう。
— TCP接続の再利用とキューの最適化 —
TIME_WAIT状態のソケットを迅速に再利用(安全な範囲で有効化)
net.ipv4.tcp_tw_reuse = 1
SYNパケットを受け取った際、キューが溢れた場合にSYN cookiesを使用してDDoSを防ぐ
net.ipv4.tcp_syncookies = 1
接続初期の輻輳ウィンドウ(Initial Congestion Window: initcwnd)を拡げる
デフォルトの3〜4パケットから10パケットへ増やし、スロースタートの初速を上げる
(※現代のLinuxカーネルではデフォルトで10になっていることが多いが明示的に確認)
TCPの送受信バッファの動的チューニング(メモリの許す限り自動拡大・縮小)
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
Keep-Aliveプローブを送信し始めるまでのアイドル時間(デフォルトは7200秒と長すぎるため短縮)
net.ipv4.tcp_keepalive_time = 300
プローブを再送信する間隔
net.ipv4.tcp_keepalive_intvl = 15
応答がない場合に切断とみなすまでのプローブ試行回数
net.ipv4.tcp_keepalive_probes = 5
ここで重要なのは、OSレベルのTCP Keep-Alive (`tcp_keepalive_time`) と、HTTP層のKeep-Alive (`keepalive_timeout`) は全く別物であるという点だ。
- HTTP Keep-Alive: アプリケーション層(HTTPトランザクション間)でTCPコネクションを維持するためのもの。
- TCP Keep-Alive: トランスポート層において、ルーターのステートフルファイアウォールやNATのタイムアウトを防ぎ、死活監視を行うためのもの。
この2つを混同すると、「OSのTCPコネクションは生きているのに、Webサーバー側のタイマーで切断されてしまう」といった不可解な挙動の原因になる。
—
4. セキュリティの罠:HTTPヘッダーインジェクションとパイプラインの暗黒面
プロトコル仕様の裏側には、常にセキュリティ上のリスクが潜んでいる。Keep-Aliveやパイプライン処理を語る上で避けて通れないのが、HTTPリクエストスマグリング(HTTP Request Smuggling)の脅威だ。
HTTP/1.1パイプラインの破滅
HTTP/1.1は、レスポンスを待たずに次のリクエストを連続して送出できる「パイプライン(Pipelining)」という機能をサポートしていた。
しかし、これが現場で大惨事を引き起こした。
フロントエンドのロードバランサー(リバースプロキシ)とバックエンドのアプリケーションサーバーの間で、「Content-Length」ヘッダーの解釈や「Transfer-Encoding: chunked」の処理にわずかな不一致があると、攻撃者は巧妙に細工したリクエストを送り込み、プロキシとバックエンドの間でリクエストの境界線を見失わせることができた。
結果として、1つのコネクション上で後続のリクエストが別のユーザーのセッションとして混同され、キャッシュのポイズニングや不正なデータアクセスを引き起こす――これがHTTPリクエストスマグリングの悪夢である。
現代のインフラにおける鉄則:
> 「HTTP/1.1のパイプラインは、事実上すべてのモダンブラウザおよび主要なリバースプロキシで無効化(または実装から排除)されている。」
NginxやApacheなどのプロダクション環境では、パイプライン要求を受信してもシリアルに処理せず、強制的に直列化するか、機能を無効化する設定がデフォルトとなっている。HTTP/1.1を使う上では、純粋な「Persistent Connection(Keep-Alive)」のみを許可し、パイプラインは使わせない設計がセキュリティの基本防衛ラインとなる。
—
5. まとめ:パケットの息吹を感じるアーキテクチャへ
HTTP/1.1のKeep-Aliveとタイムアウト設定。それは単なる設定ファイルの1行ではなく、クライアントのブラウザ、途中のネットワーク機器、ロードバランサー、そしてLinuxカーネルのメモリ空間に至るまで、すべてのコンポーネントが調和して初めて成り立つ精巧なシステムである。
「なぜこの設定値なのか?」
「このタイムアウトを迎えたとき、パケットはどのようなシーケンスを描くのか?」
教科書通りのマニュアルを離れ、tcpdumpやWiresharkでパケットの息吹を感じ、カーネルのステートマシンにと思いを馳せること。それこそが、障害に強く、極限まで最適化されたインフラストラクチャを構築する唯一にして最高の道なのである。
コメント