【テクニカル・上級編】HTTP/1.1におけるConnection: keep-aliveの挙動 – HTTPプロトコル・通信規格実践ガイド

パケットの息づかいを感じろ:HTTP/1.1 `Connection: keep-alive` の深層と、限界を超えたチューニングの実践

ネットワークの現場に身を置く者なら、深夜の静寂の中で `tcpdump` や `Wireshark` の画面に流れるパケットの波を見つめる瞬間の高揚感を理解してもらえるはずだ。

SYN、SYN-ACK、ACK――。この3ウェイ・ハンドシェイクという名の「儀式」が交わされるたび、私たちはトランスポート層の向こう側にあるリソースを消費し、物理的な距離と光速の壁に挑んでいる。

Webの黎明期、HTTP/0.9や初期のHTTP/1.0が支配していた時代、すべてのリクエストとレスポンスのペアに対して、この重厚なTCPコネクションの確立と切断が繰り返されていた。1つのHTMLとそこにぶら下がる数十個の画像やスタイルシートを取得するために、どれほどのSYNパケットが虚空に放り投げられ、どれほどのTIME_WAITがLinuxカーネルのメモリを蝕んでいたことか。

この不条理なオーバーヘッドに終止符を打ったのが、HTTP/1.1で標準化された `Connection: keep-alive` である。本稿では、この小さなヘッダーがいかにしてTCPの挙動を根本から変え、現代のインフラにおいていかに厳密なチューニングと管理を要求されるのか、パケットとカーネルの深部から紐解いていこう。

—

1. パケットレベルで見る持続的接続(Persistent Connection)の正体

HTTP/1.1のデフォルト挙動、あるいは明示的な `Connection: keep-alive` ヘッダーが指定された通信において、何が起きているのか。まずはワイヤー上の動きを追う。

従来のHTTP/1.0では、ひとつのレスポンスが返されると、サーバー側(あるいはクライアント側)から `FIN` パケットが送出され、TCPコネクションは無慈悲に四散していた。これに対し、Keep-Aliveが有効な場合、クライアント(ブラウザやAPIクライアント)は最初のTCPハンドシェイクを完了させた後、同じソケット上で次々とHTTPリクエストをパイプライン(あるいはシリアル)に流し込む。

[Client] [Server]
|— [SYN] ——————————>|
|<-- [SYN-ACK] ---------------------------| |--- [ACK] (TCP Handshake Completed) ---->|
| |
|— [HTTP GET /index.html] ————->|
|<-- [HTTP/1.1 200 OK + Body] ------------| | | |--- [HTTP GET /style.css] -------------->| <-- コネクションを再利用! |<-- [HTTP/1.1 200 OK + Body] ------------| | | | (一定時間アイドル状態が継続) | |--- [FIN] ------------------------------>| <-- Keep-Alive Timeoutによる切断 |<-- [ACK / FIN] -------------------------| ここで重要なのは、「TCPコネクションの生存期間(Lifetime)」と「HTTPリクエストのライフサイクル」が切り離されるという点だ。

TLS(Transport Layer Security)が絡む環境では、この恩恵はさらに劇的になる。TLS 1.2以前であれば、TCPハンドシェイクの後にさらに数往復のハンドシェイク(Client Hello, Server Hello, Key Exchange, Certificate verificationなど)が上乗せされる。Keep-Aliveは、この重い暗号化コンテキスト(Session IDやSession Ticketsによる再開を考慮してもなお)を維持したまま、複数のアプリケーション層データを安全なトンネル内に流し込み続けるための生命線なのだ。

—

2. タイムアウトのジレンマ:Keep-Alive Timeout と Max Requests

永続的接続は万能の魔法ではない。サーバー側のリソース(ファイルディスクリプタ、メモリ上のソケットバッファ、CPU)は有限である。もしクライアントがリクエストを投げ終えた後、ブラウザを開いたまま何時間も放置したらどうなるか? サーバー側でコネクションを維持し続ければ、やがて `nofile`(ファイルディスクリプタ上限)に達し、新規のユーザーを門前払いすることになる。

ここで登場するのが、Keep-Alive Timeout と Max Requests(最大リクエスト数) という2つの防衛線だ。

Nginxにおける堅牢なタイムアウト設計

現場の最前線で使われるNginxを例に取ろう。デフォルトの設定では不十分であることが多く、トラフィックの性質に応じたチューニングが必須となる。

http {
# クライアントとのKeep-Alive接続を維持する最大時間
# デフォルトの75秒はモダンなWebアプリには長すぎる場合がある。
# トラフィックが多い環境では15秒〜30秒程度に短縮し、ゾンビコネクションを迅速に刈り取る。
keepalive_timeout 15s;

# 1つのKeep-Aliveコネクション経由で処理できる最大リクエスト数
# これを超えるリクエストが来ると、サーバー側からコネクションを切断し、
# クライアントに新しいコネクションの張替えを促す(メモリリーク対策としても有効)。
keepalive_requests 1000;

# アップストリーム(バックエンドのアプリケーションサーバー)とのKeep-Alive設定
upstream backend_cluster {
server 10.0.1.10:8080;
server 10.0.1.11:8080;

# アップストリーム側にキャッシュするアイドル状態のコネクション数
# ワーカープロセスごとの値である点に注意。
keepalive 32;

# アップストリームとのコネクションの最大寿命
keepalive_timeout 60s;
}
}

この設定の妙味は、フロントエンド(クライアント-Nginx間)とバックエンド(Nginx-App間)で異なるポリシーを適用できる点にある。バックエンドへのコネクションは、頻繁な再確立のオーバーヘッドを避けるためにプールされ、一定数と一定時間が厳格に管理される。

—

3. カーネルパラメータの調律:TCPスタックの深部をいじる

アプリケーション層で `Connection: keep-alive` をどれほど美しく設定しても、OSのカーネルパラメータ(Linuxであれば `sysctl`)がデフォルトのままであれば、高負荷耐性は望めない。

特に注目すべきは、TCPのキープアライブ(Keep-Alive)機構と、タイムアウトに伴う `TIME_WAIT` 状態のハンドリングだ。

/etc/sysctl.conf の推奨チューニング例

— アプリケーション層のKeep-Aliveとは別物の、TCP層のキープアライブ設定 —
アイドル状態になってからプローブ(生存確認パケット)を送り始めるまでの時間 (秒)
net.ipv4.tcp_keepalive_time = 300

プローブを送信する間隔 (秒)
net.ipv4.tcp_keepalive_intvl = 15

応答がない場合に、切断とみなすまでのプローブ送信回数
net.ipv4.tcp_keepalive_probes = 5

— TIME_WAITの高速リサイクルとメモリ最適化 —
TIME_WAIT状態のソケットをタイムスタンプベースで新規接続に再利用することを許可
(※厳密なRFC 1323に従うが、NAT環境下等で問題になる場合もあるためネットワークトポロジ要確認)
net.ipv4.tcp_tw_reuse = 1

TCPソケットの送受信バッファの動的チューニング範囲 (最小値, デフォルト値, 最大値 [バイト])
大規模な帯域幅遅延積 (BDP) を持つネットワークにおいてスループットを最大化する
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

同時接続数が数百万に達する極限環境におけるファイルディスクリプタ上限の拡張
fs.file-max = 2097152

ここで混同しやすいのが、「HTTPのKeep-Alive」 と 「TCPのKeep-Alive」 の違いだ。

  • HTTP Keep-Alive: アプリケーション層の仕組み。1本のTCPコネクション上で複数のHTTPトランザクションを直列に処理する。
  • TCP Keep-Alive: トランスポート層の仕組み。長期間パケットのやり取りがないアイドル状態のコネクションが、死んでいないか(ルーターの電源が落ちていないかなど)を確認するために、OSが黙々と無音のACKパケットを投げ合う機能。

HTTP/1.1のコネクション管理を語る上では、前者が主役であるが、ロードバランサーやNATゲートウェイの「アイドルタイムアウト(通常は60秒〜300秒程度でセッションテーブルから消去される)」に打ち勝つためには、後者のTCP層のキープアライブや、適切なHTTP `keepalive_timeout` の設計が不可欠となる。

—

4. セキュリティと脆弱性の罠:HTTP Keep-Alive が孕むリスク

プロトコルの効率化を追求する裏側で、セキュリティの専門家たちは常に「悪用の可能性」に目を光らせている。`Connection: keep-alive` が絡むことで顕在化する代表的な脆弱性が HTTPリクエストスマグリング(HTTP Request Smuggling) である。

現代のWebアーキテクチャは、フロントエンドにCDNやリバースプロキシ(Nginx, Varnish, AWS ALBなど)、バックエンドにアプリケーションサーバー(Node.js, Unicorn, Tomcatなど)という多層構造をとることが多い。

ここで、フロントエンドとバックエンドの間で「リクエストの境界」の解釈にズレが生じると致命的な問題が発生する。

[悪意あるクライアント] —> (HTTP Keep-Aliveコネクション) —> [フロントエンド (Nginx)] —> [バックエンド (App)]

攻撃者は、1つのKeep-Aliveコネクションの中に、`Content-Length` ヘッダーと `Transfer-Encoding: chunked` ヘッダーを巧妙に競合・混入させたリクエストを流し込む。
フロントエンドは `Content-Length` を基準にリクエストの終わりを解釈したが、バックエンドは `Transfer-Encoding` を優先して解釈した場合、フロントエンドにとっては「1つのリクエストの後半部分」に見えるデータが、バックエンドにとっては「全く別の、身元不明の2番目のリクエストの頭文字」として誤認されてしまう。

結果として、後続の正当なユーザー(被害者)が同じKeep-Aliveコネクション経由で送信したリクエストの文脈が書き換えられ、キャッシュポイズニングや不正なコマンド実行、セッションハイジャックへと繋がる。

回避のためのベストプラクティス

1. HTTP/1.1の厳格なパース: プロキシサーバーやアプリケーションフレームワークは、RFC 7230(現 RFC 9112)に厳密準拠し、`Content-Length` と `Transfer-Encoding` が両方存在する場合は即座に `400 Bad Request` で弾く。
2. HTTP/2 または HTTP/3 への移行: HTTP/1.1のテキストベースの曖昧なパースに起因するスマグリングの多くは、バイナリフレーミングと厳密なストリーム管理を行う HTTP/2 以降では根本的に根絶される。もしインフラの近代化が可能であれば、アップストリーム間も含めてHTTP/2化を強く推奨する。

—

結びにかえて:パケットの向こう側のユーザー体験を想う

`Connection: keep-alive` というわずか数文字のヘッダー、そしてそれに付随する数行の設定。それは単なる「ネットワークの最適化手法」に留まらない。

私たちが適切にタイムアウトを調整し、カーネルバッファをチューニングし、セキュリティのほころびを塞ぐとき、その向こう側では何が起きているか?
何千、何万というユーザーのブラウザ画面で、フォントや画像、スクリプトが一瞬にして描き出され、レイテンシのストレスを感じさせることなくシームレスなデジタル体験が提供されている。

プロトコルの内部挙動を愛し、パケットの1オクテットにまで目を光らせるインフラエンジニアの仕事とは、まさにこの「見えない基盤」に命を吹き込み続けることに他ならない。さあ、次はあなたのターミナルで、実際のトラフィックを観測してみようではないか。

コメント

タイトルとURLをコピーしました