【実務・中級編】HTTP/1.1におけるKeep-Aliveのデフォルト化と接続管理 – HTTPプロトコル・通信規格実践ガイド

TCPハンドシェイクの「無駄」を斬る:HTTP/1.1におけるコネクション再利用の真実

ネットワークエンジニアとして現場に立っていると、「なぜかWeb APIのレスポンスが遅い」「高負荷時にSYNパケットの嵐が止まらない」といった相談をよく受ける。その多くは、実はHTTP/1.1の「接続管理」という基本を軽視した設計に起因していることが多い。

今日は、教科書の定義をなぞるのではなく、パケットがワイヤー上を駆け巡る「現場のリアル」という視点から、HTTP/1.1におけるKeep-Alive(持続的接続)の本質を紐解いていこう。

—

1. なぜ「切断」が悪なのか:TCPハンドシェイクのコスト

HTTP/0.9や1.0の時代、ブラウザはリクエストを送るたびにTCPの3ウェイ・ハンドシェイク(SYN, SYN-ACK, ACK)を行い、レスポンスを受け取るとすぐさまFINを投げて接続を破棄していた。

現代のWebサイトやAPIのように、1ページあたり数十、時には数百のオブジェクトを読み込む環境でこれをやるとどうなるか。パケットの往復(RTT)が毎回発生し、慢性的で致命的なレイテンシの増大を招く。 さらに、TCPには「スロースタート」というアルゴリズムがある。接続のたびに通信帯域の予測を一からやり直すため、帯域をフル活用するまでに時間がかかるのだ。

HTTP/1.1でKeep-Aliveがデフォルト化されたのは、この「毎回握手して、即座に別れる」という非効率な儀式に終止符を打つためだった。

—

2. ConnectionヘッダーとPersistent Connectionの挙動

HTTP/1.1において、コネクションは明示的に「閉じる」と言わない限り生き続ける。ここで重要なのが `Connection: keep-alive` ヘッダーと `Connection: close` ヘッダーだ。

  • デフォルト状態: HTTP/1.1では、クライアントが特に何も言わなければ、サーバーはコネクションを維持しようとする。
  • Connection: close: サーバーまたはクライアントが「もうこのコネクションは使い切ったから閉じるよ」と伝えるための合図だ。

実務で見るデバッグのコツ

`curl` を使って、このコネクションがどう扱われているかを覗いてみるのが最も手っ取り早い。

-v オプションでヘッダー情報をすべて表示
HTTP/1.1 の通信を確認する
curl -v https://example.com/api/data

出力結果の注目ポイント:
< Connection: keep-alive <- 接続が維持されていることを示す < Keep-Alive: timeout=5, max=100 <- 5秒間無通信なら切断、100リクエストで再接続 ここで重要なのが `timeout` と `max` パラメーターだ。

  • timeout: サーバーが接続を維持する限界時間。これを超えても次のリクエストが来なければ、サーバーは容赦なくFINを送る。
  • max: 同一コネクションで処理できる最大リクエスト数。メモリ保護の観点から設定されることが多い。

—

3. インフラエンジニアが知っておくべき「落とし穴」

コードレベルでAPIを叩く際、この「持続的接続」を意識しないと、思わぬ落とし穴にはまる。

Python (requests) での例

`requests` ライブラリはデフォルトでコネクションプール機能を持っているが、明示的に `Session` を使わないと毎回コネクションが生成されてしまう。

import requests

毎回セッションを作ると、そのたびにTCPハンドシェイクが発生する(NG)
def bad_request():
for _ in range(10):
requests.get(“https://api.example.com/v1/resource”)

セッションを使うと、同一ホストへの通信でTCPコネクションが再利用される(Good)
def good_request():
with requests.Session() as session:
for _ in range(10):
# 2回目以降はハンドシェイクなしでパケットが飛ぶ
session.get(“https://api.example.com/v1/resource”)

—

4. Web API設計者への提言:Keep-Aliveを活かすために

インフラ側で `Keep-Alive` を正しく運用するためには、以下の3点に注意してほしい。

1. タイムアウト値のチューニング:
ロードバランサー(ALBやNginx)のKeep-Aliveタイムアウト値を、バックエンドのアプリケーション処理時間より短く設定してしまうと、処理中に接続が切れる「競合状態」が発生する。必ず 「バックエンドの処理時間 < サーバー側のタイムアウト値」 となるよう設計すること。
2. コネクションプールの監視:
接続が維持され続けるということは、サーバー側のリソース(ファイルディスクリプタ)を占有し続けるということだ。トラフィックが急増した際、コネクションが解放されずに「Too many open files」エラーが出るケースは後を絶たない。
3. HTTP/2やHTTP/3へのアップグレード:
HTTP/1.1のKeep-Aliveは「順序立ててリクエストを処理する」という制約上、Head-of-Line Blocking(先頭行ブロッキング)という課題を抱えている。もしAPIのレスポンス速度を極限まで追求するなら、多重化を標準で備えるHTTP/2以降への移行を検討する時期に来ている。

—

最後に:ネットワークを「育てる」視点

Keep-Aliveは、単なるプロトコルの設定項目ではない。それは「一度結んだ縁をどう長く大切に使うか」というリソース管理の哲学そのものだ。

デバッグの際は、パケットキャプチャでSYNパケットが異常に多く飛んでいないかを確認し、コネクションの寿命を適切に制御する。この地味な作業の積み重ねが、数ミリ秒のレイテンシを削り、結果としてユーザー体験を劇的に向上させる。

インフラは、ただ動かすだけでは不十分だ。パケットの呼吸を感じ、ベストな通信経路を整えてやる。それが、我々エンジニアの矜持ではないだろうか。

何か詰まったら、まずは `netstat` や `ss` コマンドで接続状態を確認することから始めてみてほしい。現場からは以上だ。

コメント

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