終わりのない握手(ハンドシェイク)を止めろ:HTTP/1.1のKeep-Aliveがインフラを変える理由
ネットワークエンジニアとして現場を歩いていると、「なぜかWeb APIのレスポンスが妙に遅い」「サーバーの負荷が急に跳ね上がる」といった相談を受けることがよくある。その原因の多くは、実はアプリケーションコードではなく、背後にあるTCP接続の管理に潜んでいることが多い。
今回は、HTTP/1.1の根幹を成す「持続的接続(Persistent Connection)」、すなわち`Connection: keep-alive`の挙動を深掘りし、それが現代のWebインフラでどう最適化の鍵を握っているのかを解説しよう。
—
1. なぜ「握手」がボトルネックになるのか
HTTP/1.0の時代、ブラウザは画像やCSSを1つ取得するたびにTCP接続を確立(3-way handshake)し、転送が終われば切断していた。しかし、今のWebサイトは数十、数百のファイルが入り乱れる世界だ。毎回「SYN→SYN/ACK→ACK」の往復(RTT: Round Trip Time)を繰り返していたら、ユーザーは画面が表示される前に待ちくたびれて離脱してしまう。
そこで登場したのが、TCP接続の再利用(Keep-Alive)だ。一つのTCPコネクションを「つなぎっぱなし」にすることで、2回目以降のリクエストではハンドシェイクを省略し、いきなりHTTPリクエストを投げ込めるようになる。この「RTTの削減」こそが、Webパフォーマンス最適化の第一歩なのだ。
—
2. Keep-Aliveの挙動を追う:パケット視点のシーケンス
Keep-Aliveの挙動は、HTTPヘッダーのやり取りで決まる。
1. クライアント: `Connection: keep-alive` をヘッダーに含めてリクエストを送信。
2. サーバー: 同様に `Connection: keep-alive` を返せば、接続は維持される。
3. 転送: 最初のレスポンスが届いた後、クライアントは即座に次のリクエストを同じTCPポート経由で送信する。
注意すべきポイント:Content-Lengthの重要性
Keep-Alive中、サーバーは「どこまでが1つのレスポンスか」を識別する必要がある。HTTP/1.0までは「切断=終了」だったが、接続を維持する以上、`Content-Length`ヘッダー(またはチャンク転送)が必須だ。これが欠けていると、クライアントはいつ次のリクエストを送っていいのか判断できず、接続がハングアップしてしまう。
—
3. 実践:ツールでKeep-Aliveを確認する
理論を語る前に、まずは自分の手でパケットの挙動を確かめてみよう。`curl`を使えば一発だ。
-v: 詳細出力, –http1.1: HTTP/1.1を強制
1回のリクエストで接続が閉じないことを確認する
curl -v –http1.1 http://example.com/
出力結果の中に `Connection: keep-alive` があればOKだ。次にPythonの`requests`セッションを使って、コネクションが再利用されているかを確認するコード例を挙げる。
import requests
Sessionオブジェクトを使うことで、内部でTCPコネクションが再利用される
session = requests.Session()
1回目のリクエスト(ここでハンドシェイクが発生)
r1 = session.get(‘https://api.example.com/data’)
print(f”1回目: {r1.status_code}”)
2回目のリクエスト(Keep-Aliveにより、ハンドシェイクなしでリクエスト送信)
r2 = session.get(‘https://api.example.com/data’)
print(f”2回目: {r2.status_code}”)
—
4. インフラ屋の死活問題:タイムアウト設定の最適解
Keep-Aliveは魔法ではない。サーバー側には「接続をいつ切るか」というKeep-Alive Timeoutの設定がある。ここをどう調整するかが、インフラ運用の腕の見せ所だ。
タイムアウトが短すぎると?
接続がすぐに切れるため、頻繁にハンドシェイクが発生し、サーバーのCPU負荷が上昇する(特にTLS/SSLハンドシェイクは重い)。
タイムアウトが長すぎると?
アイドル状態のコネクションがメモリを占有し続ける。特に同時接続数が多いWebサーバー(Nginxなど)では、リソース枯渇を招くリスクがある。
Nginxの設定例:
http {
# 接続を維持する秒数。短すぎず長すぎない調整が必要
keepalive_timeout 65;
# 1つの接続で処理する最大リクエスト数。高負荷時はこれも重要
keepalive_requests 1000;
}
—
5. シニアエンジニアからの教訓
最後に、トラブルシューティング現場からのTipsを一つ。
もし「ブラウザからは正常に見えるのに、API経由だと時々コネクションリセット(ECONNRESET)が発生する」という相談を受けたら、疑うべきはロードバランサーとバックエンドのKeep-Alive時間の不一致だ。
ロードバランサーが「60秒で切る」と言っているのに、バックエンドが「120秒まで待つ」設定になっていると、サーバーが接続を維持しているつもりでも、中間のゲートウェイが強制遮断する。これが「ゴーストコネクション」となり、クライアントにエラーを返す原因になる。
鉄則:
1. ロードバランサー(ELB等)のタイムアウトを最も短く設定する。
2. バックエンドサーバーのタイムアウトを、それより少しだけ短く設定する。
これで、中途半端なタイミングで接続が切れる事故は防げるはずだ。
HTTP/1.1は古くからある規格だが、その挙動を深く理解することは、現代のHTTP/2やHTTP/3を扱う上でも揺るぎない土台になる。パケットがどう流れ、どこで溜まっているのか——常に通信の「呼吸」を感じながら設計・運用にあたってほしい。それが、卓越したエンジニアへの近道だ。
コメント