「なぜ毎回握手するのか?」― 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の設計やインフラのサイジングの精度は一段と上がります。ぜひ、今日のデバッグから意識してみてください。
コメント