【実務・中級編】HTTP/1.1のConnection: keep-aliveヘッダーによる持続的接続の最適化 – HTTPプロトコル・通信規格実践ガイド

終わりのない握手(ハンドシェイク)を止めろ: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を扱う上でも揺るぎない土台になる。パケットがどう流れ、どこで溜まっているのか——常に通信の「呼吸」を感じながら設計・運用にあたってほしい。それが、卓越したエンジニアへの近道だ。

コメント

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