【実務・中級編】HTTP/1.1におけるConnection: keep-aliveヘッダーの役割 – HTTPプロトコル・通信規格実践ガイド

「なぜ毎回握手するのか?」― HTTP/1.1のConnection: keep-aliveが変えたWebの景色

ネットワークエンジニアとして現場に立っていると、若手から「Webサイトの表示が遅い」という相談を受けることがよくあります。パケットキャプチャを開いてみると、そこにはお決まりの「無駄な儀式」が繰り返されている。そう、TCPの3ウェイ・ハンドシェイク(SYN, SYN/ACK, ACK)です。

HTTP/1.0の時代、ブラウザは1つのリクエストを送るたびにTCP接続を張り、レスポンスを受け取ると即座に切断していました。もしWebページの中に100個の画像があれば、100回もTCPの接続・切断を繰り返していたのです。想像してみてください。毎回わざわざ玄関先まで行って挨拶し、用事を済ませたら帰宅する。これを100回繰り返すようなものです。非効率極まりない。

この悪習を断ち切り、エンジニアの「インフラ・オーバーヘッド」という名の敵を討ったのが、HTTP/1.1で標準化された `Connection: keep-alive` です。

—

1. TCP接続の「再利用」という魔法

`Connection: keep-alive` は、一言で言えば「一度開いたTCP接続を閉じずに使い回す」ための仕組みです。

通信フローで比較すると一目瞭然です。

  • HTTP/1.0 (非持続的接続):

1. SYN → SYN/ACK → ACK (ハンドシェイク)
2. HTTP Request
3. HTTP Response
4. FIN/ACK (切断)
これをリクエストの数だけ繰り返す。

  • HTTP/1.1 (持続的接続 / Keep-Alive):

1. SYN → SYN/ACK → ACK (ハンドシェイク)
2. HTTP Request 1 → Response 1
3. HTTP Request 2 → Response 2
4. …
5. FIN/ACK (一定時間経過後、または明示的な切断)

この「再利用」により、TCPのハンドシェイクにかかるRTT(往復遅延時間)が大幅に削減されます。特にTLS(HTTPS)を利用している場合、これに加えて暗号化のネゴシエーションが毎回発生するため、keep-aliveの効果は絶大です。

—

2. 実務で見る「keep-alive」の挙動

実際にブラウザやAPIクライアントがどのようなヘッダーを投げているか、`curl`を使って覗いてみましょう。

-v オプションで詳細な通信内容(ヘッダー)を表示
curl -v https://example.com/

レスポンスヘッダーの中に、以下のような記述があるはずです。

HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: timeout=5, max=100
Content-Type: text/html
…

ここで重要なパラメーターが2つあります。

  • `timeout`: 接続を維持する秒数。この時間を過ぎても次のリクエストが来なければ、サーバー側から接続をクローズします。
  • `max`: 1つのTCP接続で処理するリクエストの最大数。これを超えると、たとえタイムアウト前でも接続は切断されます。

インフラエンジニアとしては、この `timeout` の設定には注意が必要です。あまりに長く設定しすぎると、接続がサーバー側に残り続け、リソース(ファイルディスクリプタ)を枯渇させる原因になります。

—

3. コードレベルでの制御:Fetch API と Python

現代のWeb開発では、ブラウザのFetch APIやPythonの`requests`ライブラリがよしなにやってくれますが、明示的に意識する場面もあります。

Fetch API (ブラウザ)

ブラウザの場合、`keep-alive`はデフォルトで「オン」に近い挙動をしますが、リソースのプリフェッチなどで明示的に指定する場合もあります。

fetch(‘https://api.example.com/data’, {
method: ‘GET’,
// keepaliveをtrueにすることで、ページ遷移時でも接続を維持してリクエストを飛ばす
keepalive: true,
headers: {
‘Connection’: ‘keep-alive’
}
});

Python (requests)

`requests`で効率よくAPIを叩くなら、`Session`オブジェクトを使いましょう。これを使わないと、毎回新しいTCP接続が生成され、keep-aliveの恩恵を一切受けられません。

import requests

Sessionオブジェクトを使うことで、裏側でTCP接続が維持(プール)される
with requests.Session() as session:
# 最初の接続でTCPハンドシェイクが発生
session.get(‘https://api.example.com/resource1’)

# 2回目以降は既存のTCP接続を再利用するため高速!
session.get(‘https://api.example.com/resource2’)

—

4. 現場のシニアからのアドバイス:デバッグの極意

最後に、トラブルシューティングの際のTipsを一つ。

もし「通信が異常に遅い」と感じたら、まず `netstat` や `ss` コマンドでコネクションの状態を確認してください。`TIME_WAIT` が異常に積み上がっていたり、逆にサーバー側で `ESTABLISHED` なのに応答がない場合は、keep-aliveのタイムアウト設定とロードバランサー(ALBやNginx)の接続維持設定の不一致が疑われます。

特に、クライアント側のkeep-aliveタイムアウトより、ロードバランサーのタイムアウトが短い場合、サーバーが先に切断してしまい、クライアントが「死んだ接続」にリクエストを投げてエラー(502 Bad Gatewayや接続リセット)になるケースは非常に多いです。

「接続はサーバー側で少し早めに閉じる」。これが、堅牢なシステムを作るための黄金律です。

HTTP/1.1のkeep-aliveは地味な存在ですが、Webの体感速度を支える縁の下の力持ちです。この仕組みを理解しているだけで、APIの設計やインフラのサイジングの精度は一段と上がります。ぜひ、今日のデバッグから意識してみてください。

コメント

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