【実務・中級編】HTTP/1.1の永続接続(Keep-Alive)の設計概念 – HTTPプロトコル・通信規格実践ガイド

HTTP/1.1の「永続接続」という発明:なぜTCPハンドシェイクを繰り返してはいけないのか

Webのエンジニアリングにおいて、パフォーマンスのボトルネックを特定しようとすると、必ず「TCPのオーバーヘッド」という壁に突き当たります。

今日扱うのは、HTTP/1.1の根幹を成す「永続接続(Persistent Connection)」、通称Keep-Aliveです。最新のHTTP/3が普及しつつある現代においても、この設計思想を理解していないと、APIのレイテンシや負荷分散の設計で致命的なミスを犯すことになります。

現場の最前線で戦う皆さんに向けて、なぜこの仕組みが不可欠なのか、そしてどうデバッグすべきかを紐解いていきましょう。

—

1. 「3ウェイ・ハンドシェイク」という重すぎる代償

HTTP/0.9や1.0の時代、ブラウザは「1つのリクエストごとにTCP接続を1回確立し、レスポンスを受け取ったら即切断」という非効率な挙動をしていました。

想像してみてください。小さな画像を10個読み込むだけで、毎回TCPの「SYN → SYN/ACK → ACK」という3往復のやり取りを行い、さらにHTTPSであればTLSのハンドシェイクまで加わります。パケットが光速で移動しても、物理的な距離によるRTT(往復遅延時間)は削れません。

この「接続のたびに発生する無駄なオーバーヘッド」を解消するために導入されたのが、永続接続です。

2. Connection: keep-alive の正体とシーケンス

HTTP/1.1では、デフォルトで接続が維持されます。クライアントが `Connection: keep-alive` ヘッダーをリクエストに付与することで、「このリクエストが終わっても接続を閉じないでくれ」とサーバーに伝えます。

通信フローのイメージ

1. Client -> Server: `GET /api/data HTTP/1.1` + `Connection: keep-alive`
2. Server -> Client: `HTTP/1.1 200 OK` + `Content-Length: 1024` + `Connection: keep-alive`
3. Client -> Server: (同じTCPコネクションを使用して次のリクエストを送信)

ここでのポイントは、Content-Lengthの重要性です。接続を閉じないということは、サーバーが「どこでレスポンスが終わりか」を明示しないと、クライアントはいつまで待てばいいか分からず、コネクションがタイムアウトするまでハングアップしてしまうからです。

3. 実践:コネクションを観察する

理屈だけでは実務には使えません。実際に自分の手元で、接続が再利用されているかを確認しましょう。

curl でのデバッグ

`-v` オプションをつけて、接続が閉じられずに再利用されているかを確認します。

-v でヘッダーを確認
2回連続でリクエストを送っても、TCP接続が再利用されるはずです
curl -v -H “Connection: keep-alive” http://example.com/api/v1/resource \
http://example.com/api/v1/resource

出力の中に `Re-using existing connection` という文字列が見えれば成功です。

Python (requests) での活用

requestsライブラリの `Session` オブジェクトは、内部で接続プールを管理しており、Keep-Aliveを自動的に活用します。高負荷なAPI叩きを行う際は、毎回 `get()` を呼ぶのではなく、必ずSessionを使ってください。

import requests

Sessionを使うことでTCP接続を維持し、オーバーヘッドを劇的に削減する
session = requests.Session()

for i in range(5):
# 1回目の接続確立後、2回目以降は既存のコネクションが使い回される
response = session.get(“https://api.example.com/data”)
print(f”Status: {response.status_code}”)

最後に明示的に閉じる
session.close()

4. インフラ屋が意識すべき「タイムアウト」の罠

現場で最もトラブルになりやすいのが、「Keep-Alive Timeout」と「Max Keep-Alive Requests」の不整合です。

NginxやApacheの設定ファイルでは、以下のようなパラメータが存在します。

nginx.conf の例
http {
# 接続を維持する時間(秒)。長すぎるとサーバーのメモリを圧迫する
keepalive_timeout 65;

# 1つの接続で処理できる最大リクエスト数。高負荷APIでは重要
keepalive_requests 100;
}

  • Keep-Alive Timeout: サーバーが接続を切断するまでの待ち時間。
  • Max Keep-Alive Requests: 1つのコネクションで受け付けるリクエスト上限。

ここを適切に設定しないと、クライアントが「接続は生きている」と思ってリクエストを送った瞬間に、サーバー側で強制切断され、`502 Bad Gateway` や `Connection Reset by Peer` が発生します。特にロードバランサー(ALBやNGINX)を挟んでいる場合、それぞれのタイムアウト値の「食い違い」が障害の温床になります。

まとめ:ネットワークの「再利用」こそが高速化の鍵

HTTP/1.1の永続接続は、単なる機能ではなく「いかにTCPのコストを回避するか」という設計思想の結晶です。

  • APIクライアントを書く時: 必ず接続プール(Session)を利用する。
  • インフラを構築する時: ロードバランサーからバックエンドまで、タイムアウト値を一貫させる。
  • トラブルが起きた時: `netstat` や `ss` コマンドで、大量の `TIME_WAIT` が発生していないか確認する。

この基本を叩き込んでおけば、次にくるHTTP/2の「ストリーム多重化」やHTTP/3の「QUIC」がなぜ優れた技術なのか、その進化の必然性がより深く理解できるはずです。現場のコードを一行書くたびに、「今、パケットはどう流れているか」を想像してみてください。それがエンジニアとしての生存戦略です。

コメント

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