HTTP/1.1の「持続的接続」と「パイプライン処理」:TCPハンドシェイクの呪縛から解放されるための基礎技術
ネットワークエンジニアとして現場に立っていると、Webアプリケーションのレスポンスが「なぜか遅い」という相談をよく受けます。その原因の多くは、実はアプリケーションコードではなく、背後にあるHTTPの通信制御に隠されています。
今回は、現代のWebの礎であるHTTP/1.1の肝、「Connection: keep-alive(持続的接続)」と、あまりに繊細ゆえに歴史の闇に葬られかけた「パイプライン処理」について、現場の知見を交えて深掘りしていきましょう。
—
1. 持続的接続(Keep-Alive)の必然性
HTTP/1.0の頃、Webページは「1リクエスト=1 TCP接続」という極めて非効率な運用でした。画像が10枚あるページを表示するだけで、TCPの3ウェイ・ハンドシェイク(SYN, SYN-ACK, ACK)を10回繰り返していたのです。
HTTP/1.1では、デフォルトで `Connection: keep-alive` が採用されました。これにより、一度確立したTCP接続を使い回し、「TCPのオーバーヘッド(立ち上げコスト)」を最小化できるようになったのです。
なぜこれが重要なのか?
現代のネットワークにおいて、RTT(往復遅延時間)は無視できません。接続を維持することで、2回目以降のリクエストは「待ち時間ゼロ」でデータを送信できます。
- HTTP/1.0: `Connect -> Request -> Response -> Close`
- HTTP/1.1: `Connect -> Request -> Response -> Request -> Response -> Close`
この仕組みのおかげで、ブラウザは画像やCSSを同じコネクションで連続して取得できるようになりました。
—
2. パイプライン処理(Pipelining)の理想と挫折
Keep-Aliveでコネクションを使い回せるとはいえ、HTTP/1.1の基本は「リクエストを送ったら、レスポンスが返ってくるまで次のリクエストは待つ」という直列処理です。
ここで登場したのが「パイプライン処理」です。これは、「レスポンスを待たずに、次のリクエストをTCPバッファに詰め込んで送りつける」という野心的な仕様でした。
なぜ現場では普及しなかったのか?
パイプライン処理には致命的な欠点がありました。それは「先頭行のブロック問題(Head-of-Line Blocking)」です。
1. サーバーがリクエストAの処理に時間がかかると、その後ろに詰まっているリクエストB、Cのレスポンスもすべて滞留する。
2. 途中で接続が切れた際、どのリクエストまでサーバーが処理し、どれが未処理なのかの整合性を取るのが極めて困難。
結果として、大手ブラウザベンダーやサーバー実装者はこの機能を無効化し、HTTP/2の「ストリーム多重化」へと舵を切ることになりました。パイプラインは、HTTP/1.1における「最も美しく、そして最も使い物にならなかった機能」と言えるでしょう。
—
3. 実践:HTTP接続を可視化する
理論を語るだけではエンジニアの腕は磨けません。実際に `curl` を使って、Keep-Aliveがどう動いているか見てみましょう。
-v で通信の詳細を表示。keep-aliveが効いているか確認
curl -v https://example.com/
サーバーからのレスポンスヘッダーに注目:
< Connection: keep-alive
< Keep-Alive: timeout=5, max=100
この「timeout=5」は、5秒間通信がなければ接続を切るという意味です
Pythonでコネクションを効率的に使う(requestsライブラリ)
デフォルトのままPythonでリクエストを投げると、毎回接続を閉じてしまう可能性があります。`Session` オブジェクトを使うことで、Keep-Aliveの恩恵をフルに受けられます。
import requests
Sessionを使うことで、内部的にTCPコネクションを維持(プール)する
session = requests.Session()
1回目のリクエスト: TCP接続を確立
session.get(‘https://api.example.com/resource1’)
2回目のリクエスト: 同じTCPコネクションを再利用
ここでTCPハンドシェイクは発生しない
session.get(‘https://api.example.com/resource2’)
session.close() # 明示的に閉じる
—
4. インフラ運用のTips:デバッグとチューニング
現場でトラブルが起きたとき、まず疑うべきは「タイムアウト設定の不整合」です。
- ロードバランサー(ALB/Nginx)のタイムアウト:
バックエンドのサーバー(Node.jsやPython等)のKeep-Aliveタイムアウトよりも、ロードバランサー側のタイムアウトをわずかに短く設定してください。そうしないと、サーバーが「まだ繋がっている」と思って送ったリクエストを、LBがすでに切断済みでエラー(502 Bad Gateway等)にする「Race Condition(競合状態)」に陥ります。
- Nginxの設定例:
# keepalive_timeout: クライアントとの接続を維持する秒数
keepalive_timeout 65;
# keepalive_requests: 1つの接続で処理する最大リクエスト数
keepalive_requests 100;
—
最後に:HTTP/1.1を知る意味
「HTTP/2やHTTP/3がある今、なぜ古いHTTP/1.1を学ぶのか?」と聞かれることがあります。答えはシンプルです。「現代の通信も、根底では依然としてTCPの挙動に縛られているから」です。
CDNやプロキシサーバーの多くは、いまだにバックエンドとの通信にHTTP/1.1を使っています。このプロトコルの癖を知っているか否かが、数万アクセスを捌く際のボトルネックを見抜く決定的な差になります。
ネットワークは生き物です。コマンドを叩き、パケットを眺め、仕様の裏側にある「なぜそうなったのか」という物語を感じ取ってください。それが、真のインフラスペシャリストへの近道です。
コメント