そのTCPハンドシェイク、本当に毎回必要ですか?HTTP/1.1 `Connection: keep-alive` が支えるWebの裏側と実務的チューニング
こんにちは。これまでに数え切れないほどのパケットキャプチャを眺め、深夜の障害対応でロードバランサーのログと格闘してきたシニアネットワークエンジニアの私です。
Webアプリケーションの開発やインフラの運用をしていると、「なぜかAPIのレスポンスが微妙に遅い」「大量のリクエストを投げたらTCPのポートが枯渇した」といったトラブルに直面したことはありませんか?
原因を探っていくと、アプリケーションのコードではなく、その下を支えるTCPとHTTPの接続管理、すなわち HTTP/1.1の持続的接続(Persistent Connection) の挙動を誤解しているケースが実に多いのです。
今回は、HTTP/1.1の根幹を支える `Connection: keep-alive` ヘッダーに焦点を当て、パケットレベルの挙動から実務でのチューニング、トラブルシューティングまで、現場の知見をたっぷり詰め込んで解説します。
—
1. 歴史的背景:なぜ「毎回つないで切る」のは悪手なのか?
まずは原点回帰です。すべての始まりであった HTTP/0.9 および HTTP/1.0 の世界を思い出してください。当時は、1つのリクエスト(例えばHTMLファイル1つ)を送るたびに、以下の一連のプロセスを踏んでいました。
1. DNS名前解決
2. TCP 3Wayハンドシェイク(SYN, SYN-ACK, ACK)
3. TLSハンドシェイク(HTTPSの場合。鍵交換や証明書検証でさらに往復が発生)
4. HTTPリクエスト送信・レスポンス受信
5. TCP 4Wayハンドシェイク(FINによる接続切断)
もし1つのWebページに、画像、CSS、JavaScriptなど、外部リソースが50個含まれていたとしたらどうでしょう? ページを表示するためだけに、この重たい手続きを50回も繰り返していたのです。
特にTCPには「スロースタート」というメカニズムがあり、接続直後の通信帯域はあえて制限されます。つまり、細切れのTCP接続を乱発することは、ネットワークの帯域をドブに捨てるようなものでした。
そこでHTTP/1.1で標準化されたのが、「一度確立したTCP接続を使い回す」 という画期的なアプローチ、すなわち `Connection: keep-alive` です。
—
2. パケットで見る違い:非持続的接続 vs 持続的接続
百聞は一見にしかず。頭の中で、クライアントとサーバー間のパケットのやり取りを思い描いてみてください。
【従来のHTTP/1.0:非持続的接続(Non-persistent Connection)】
Client Server
| —– [TCP 3Way Handshake] ——-> | (コスト: 1往復)
| —– [TLS Handshake (HTTPS)] —-> | (コスト: 数往復)
| —– GET /index.html ————> |
| <---- 200 OK (HTML body) ---------- |
| ----- [TCP FIN (切断)] -----------> | (ここで接続終了)
|
| —– [TCP 3Way Handshake] ——-> | 画像の数だけ、
| —– GET /logo.png ————–> | これを繰り返す!
| <---- 200 OK (Image body) --------- |
| ----- [TCP FIN (切断)] -----------> |
【HTTP/1.1:持続的接続(Keep-Alive)】
Client Server
| —– [TCP & TLS Handshake] ——> | ★ 最初の一回だけ!
| —– GET /index.html ————> |
| <---- 200 OK (HTML body) ---------- |
| | (接続を維持したまま待機)
| ----- GET /logo.png --------------> | ★ すぐに次のリクエストを流せる!
| <---- 200 OK (Image body) --------- |
| ----- GET /style.css -------------> | ★ ハンドシェイクのオーバヘッドがゼロ
| <---- 200 OK (CSS body) ----------- |
| | (タイムアウト時に切断)
この違いは圧倒的です。TCPのハンドシェイクやTLSの暗号化交渉にかかる数ミリ秒〜数十ミリ秒の遅延(レイテンシ)をごっそり削ぎ落とせるため、体感速度が劇的に向上します。
---
3. `Connection: keep-alive` の仕様とパラメーター
HTTP/1.1では、デフォルトで持続的接続が有効になっています。そのため、現代のWebブラウザやサーバー間の通信では、明示的に `Connection: keep-alive` ヘッダーを書かなくても、裏側では接続が維持されています。
ただし、明示的に制御したい場合や、プロキシサーバーを挟んだ環境などでは、以下のパラメーターや挙動を理解しておく必要があります。
主な制御ヘッダーと値
- `Connection: keep-alive`: 接続を維持することを明示します(HTTP/1.1では省略可能)。
- `Connection: close`: 「このレスポンスを返したら、このTCP接続は即座に切断してくれ」という強い意志表示です。サーバー側、あるいはクライアント側から送信されます。
- `Keep-Alive: timeout=5, max=100` (※主にHTTP/1.0や一部のサーバー実装で見られる拡張ヘッダー)
- `timeout`: サーバー側が「次にリクエストが来るまで、この秒数だけTCP接続を保持するよ」という時間(アイドルタイムアウト)。
- `max`: そのTCP接続上で処理できる最大リクエスト数。
—
4. 実務の現場で直面する「タイムアウト」の罠とトラブル
「接続を維持し続けるなら、ずっとつないでおけばいいのでは?」と思いますよね。しかし、ここがインフラエンジニアの腕の見せ所であり、トラブルの温床になりやすいポイントです。
罠1:TIME_WAITとポート枯渇
サーバーやクライアントが頻繁に `Connection: close` を選択してTCPを切断すると、OSのネットワークスタックには `TIME_WAIT` 状態のソケットが大量に残ります。これが高負荷時に激増すると、利用可能なローカルポートが枯渇し、新しい接続を受け付けられなくなる「ポート枯渇障害」を引き起こします。Keep-Aliveはこの `TIME_WAIT` の発生頻度を劇的に減らしてくれる特効薬です。
罠2:ロードバランサー(LB)とバックエンドのタイムアウトの不整合
実務で最もよくあるのが、「Nginx(あるいはALBなど)と、バックエンドのアプリケーションサーバー(Node.js, Unicorn, Gunicornなど)の間でのタイムアウト設定のミスマッチ」です。
例えば、次のような構成を考えてみてください。
- ALB (AWS) のアイドルタイムアウト: 60秒
- バックエンドNginxのKeep-Aliveタイムアウト: 75秒
この場合、バックエンドが「まだ接続を維持していいや(75秒)」と思っていても、手前のALBが「60秒経ったからこの接続は切る!」と勝手にTCPのFINパケットを送り、接続を断ち切ることがあります。
その結果、クライアントからの次のリクエストがタイミング悪く飛んできたときに、Nginxがすでに死んでいる(あるいは閉じられた)ソケットに対して書き込もうとし、`502 Bad Gateway` や `Connection reset by peer` エラーが突発的に発生します。
【鉄則の設計指針】
> 手前にあるプロキシやロードバランサーの Keep-Alive タイムアウトは、必ずバックエンドのサーバーの設定よりも短く設定してください。(例: ALBは60秒、バックエンドは65秒など)
—
5. 実装・設定コード例
それでは、開発や運用の現場ですぐに役に立つ設定例を見ていきましょう。
① Nginxでの Keep-Alive チューニング設定 (`nginx.conf`)
Webサーバーのパフォーマンスチューニングにおいて、Keep-Aliveの適切な設定は基本中の基本です。
http {
# クライアントとの持続的接続を維持する最大時間(秒)
# 長すぎるとゾンビ接続がリソースを圧迫し、短すぎるとハンドシェイクが増える
keepalive_timeout 65;
# 1つの持続的接続あたりに許可する最大リクエスト数
# 大量のリクエストをさばくAPIサーバー等では多めに設定すると効果的
keepalive_requests 1000;
# アップストリーム(バックエンド)への接続維持設定
upstream backend_servers {
server 127.0.0.1:8080;
# アップストリーム側でアイドル状態の接続をキャッシュしておく数
# これにより、バックエンドへのTCP接続/切断コストを極限まで削る
keepalive 32;
# アップストリーム接続のアイドルタイムアウト
keepalive_timeout 60s;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend_servers;
proxy_http_version 1.1; # HTTP/1.1の利用を強制
proxy_set_header Connection “”; # 「Connection: close」の伝搬を防ぎ、Keep-Aliveを維持する
}
}
}
- 解説: `proxy_set_header Connection “”;` は非常に重要なテクニックです。これを記述しないと、クライアントからのリクエストヘッダーがそのままバックエンドに伝わり、予期せぬ切断を引き起こす原因になります。
—
② Python (Requests) での持続的接続(Session)の活用
スクレイピングや、マイクロサービス間で頻繁にHTTPリクエストを飛ばすバッチ処理を書く場合、`requests.Session` を使わずに `requests.get()` をループさせると、毎回TCPハンドシェイクが実行され、パフォーマンスが劇的に低下します。
import requests
セッションオブジェクトを作成することで、内部のTCPコネクションプールが維持される
session = requests.Session()
ヘッダーに明示的にKeep-Aliveを指定(HTTP/1.1ではデフォルトだが明記も可能)
session.headers.update({“Connection”: “keep-alive”})
urls = [
“https://api.example.com/v1/users/1”,
“https://api.example.com/v1/users/2”,
“https://api.example.com/v1/users/3”,
]
print(“— 持続的接続を使った効率的なリクエスト開始 —“)
for url in urls:
# 同一ホストへのリクエストであれば、同じTCP接続(ソケット)が再利用される
response = session.get(url)
print(f”URL: {url} | Status: {response.status_code} | 接続再利用確認: {response.raw.connection}”)
処理が終わったらセッションを閉じてソケットを解放する
session.close()
- 実務Tips: 大量の子プロセスやマルチスレッドでリクエストを投げる場合は、`urllib3` のコネクションプール数(`pool_connections` や `pool_maxsize`)のチューニングも忘れないようにしましょう。
—
③ curl で Keep-Alive の挙動をデバッグする
開発段階や障害切り分けの際、curlを使って実際にコネクションが維持されているか(再利用されているか)を確認できます。
-v (verbose) をつけて、同じホストへ連続してリクエストを送る
「Re-using existing connection!」というログが出れば、Keep-Aliveが成功しています
curl -v https://httpbin.org/get https://httpbin.org/ip
実行時の出力例(抜粋):
- Connected to httpbin.org (54.175.205.181) port 443 (#0)
- … (TLSハンドシェイクなどのログ) …
> GET /get HTTP/1.1
> Host: httpbin.org
>
< HTTP/1.1 200 OK
< ...
- Connection #0 to host httpbin.org left intact <-- ★接続が維持されている!
- Re-using existing connection! #0 into host httpbin.org <-- ★2回目のリクエストで接続を再利用!
> GET /ip HTTP/1.1
> Host: httpbin.org
>
< HTTP/1.1 200 OK
このように、コマンドライン一つでパケットの挙動をイメージしながらデバッグできるスキルは、インフラエンジニアの大きな武器になります。
---
まとめ
HTTP/1.1の `Connection: keep-alive` は、一見するとただの小さなヘッダー設定に見えますが、その背後ではOSのTCP層、ロードバランサー、アプリケーションサーバーのライフサイクルが密接に絡み合っています。
- 持続的接続のメリット: TCP/TLSハンドシェイクのオーバーヘッドを削り、レイテンシを最小化し、TIME_WAITの枯渇を防ぐ。
- 運用上の注意点: プロキシとバックエンドのタイムアウト値の整合性(手前を短く、奥を長く)を必ず死守する。
- 開発時の注意点: アプリケーション側(PythonのSessionやHTTPクライアントのプール)でも接続を使い回す設計を徹底する。
この仕組みを正しく理解し適切にチューニングすることで、あなたのWebシステムはより堅牢に、そして驚くほど軽快に動作するようになります。
次回の障害シュミレーションやAPI設計の際には、ぜひ今回の内容を思い出してみてください。それでは、快適なネットワークライフを!
コメント