HTTPの「接続」を支配する:Keep-AliveとConnectionヘッダーの深淵
ネットワークエンジニアとして現場を渡り歩いていると、「なぜかレスポンスが遅い」「稀に接続がリセットされる」といった不可解な事象に遭遇することがあります。その原因の多くは、HTTPの通信ライフサイクルを支える根幹、すなわち「TCPコネクションの管理」に対する理解の解像度の低さにあります。
今日は、HTTP/1.1の標準でありながら、実は奥が深い「Connectionヘッダー」と「Keep-Alive」の挙動について、現場の知見を交えて解説します。
—
1. HTTP/1.0時代の遺物と「持続的接続」への進化
HTTP/0.9や1.0の時代、ブラウザが画像を10枚取得するたびにTCPの3ウェイ・ハンドシェイク(SYN/SYN-ACK/ACK)と、終了時のFIN/ACKのやり取りが発生していました。これではオーバーヘッドが大きすぎて、Webの高速化など夢のまた夢です。
そこで登場したのがHTTP/1.1の「持続的接続(Persistent Connection)」です。
HTTP/1.1では、明示的に指定しない限り「接続は切らない(Keep-Alive)」のがデフォルトとなりました。これにより、一度確立したTCPコネクションを再利用し、複数のリクエストをパイプラインのように流すことが可能になったのです。
—
2. Connection: close との付き合い方
デフォルトで接続が維持されるといっても、無限に繋ぎっぱなしにするわけにはいきません。サーバー側のリソース(メモリやソケット)を枯渇させないため、あるいはクライアントとの対話を終了させるために、`Connection`ヘッダーを用いて明示的に制御を行います。
現場でよく見る「Connection: close」
クライアントやサーバーが「このリクエスト(レスポンス)が終わったら、TCPセッションを閉じてくれ」と伝えるのが `Connection: close` です。
例えば、バックエンドのプロキシからオリジンサーバーへリクエストを投げる際、あえてこのヘッダーを付与することで、コネクションプールによる意図しない接続の残留を防ぐといった設計手法をとることがあります。
curlで明示的にConnection: closeを指定する例
curl -v -H “Connection: close” https://api.example.com/v1/resource
このコマンドを打つと、レスポンスヘッダーに `Connection: close` が含まれていることが確認できます。これを受け取った側は、その後の通信を行わずTCP切断シーケンスへ移行します。
—
3. プロキシ環境下における「ホップ・バイ・ホップ」の罠
ここからが少し複雑な話になります。`Connection`ヘッダーは、HTTPの仕様において「ホップ・バイ・ホップ(Hop-by-Hop)」ヘッダーという特殊な扱いを受けます。
これは、「そのヘッダーを中継したサーバーまでしか伝播しない」ことを意味します。
例えば、`クライアント -> プロキシ -> オリジンサーバー` という経路がある場合、クライアントが送った `Connection` ヘッダーの設定は、プロキシで一度消費(処理)され、次のホップにはそのまま転送されません。
もし、プロキシ越しに特定のヘッダーをオリジンまで透過させたい場合は、`Connection: [ヘッダー名]` のように指定します。これが、多くのAPI設計で直面する「ヘッダーの隠蔽問題」の正体です。
—
4. 実務で役立つデバッグと検証
開発現場で「コネクションが意図した通りに維持されているか?」を確認するには、`netstat`や`ss`コマンド、そしてクライアントサイドではFetch APIの挙動観察が有効です。
Pythonによる通信のライフサイクル確認
Pythonの `requests` ライブラリはデフォルトで `urllib3` のコネクションプールを使用します。以下のようにセッションを明示的に扱うことで、Keep-Aliveの挙動を制御できます。
import requests
セッションを開始してコネクションを維持する
with requests.Session() as s:
# 1回目のリクエスト(ここでTCP接続が確立)
s.get(‘https://api.example.com/data’)
# 2回目のリクエスト(同じTCP接続が再利用される)
response = s.get(‘https://api.example.com/data’)
# 接続状態を確認(Keep-Aliveされているか)
print(f”接続先ホスト: {response.connection}”)
ブラウザ(Fetch API)でのTips
ブラウザのFetch APIでは、`keepalive: true` というオプションが存在します。これはページがアンロードされる直前(ログ送信など)でも、接続を維持してリクエストを完遂させるための重要な設定です。
// ページ遷移直前でも接続を切らずにデータを送り届ける
fetch(‘/log’, {
method: ‘POST’,
body: JSON.stringify({ event: ‘page_exit’ }),
keepalive: true // これが重要。ブラウザの最適化で接続を破棄させない
});
—
5. シニアエンジニアからの教訓
最後に一つだけ、現場でよくある失敗を共有します。
それは、「負荷分散装置(LB)やプロキシの設定と、アプリケーションのKeep-Aliveタイムアウトが食い違っている」ケースです。
- LBのタイムアウト: 60秒
- バックエンドサーバーのタイムアウト: 30秒
この設定だと、LBが「まだ繋がっている」と思ってリクエストを送った先で、サーバーが既に切断しており、クライアントに `502 Bad Gateway` や `504 Gateway Timeout` が返るという現象が起きます。
鉄則:
1. 常に「Keep-Aliveのタイムアウト値」は、インフラの最上流から最下流まで整合性を保つこと。
2. ログ解析時は、クライアントの接続状態とサーバーの接続状態を「コネクションID」で紐付けて追跡すること。
プロトコルは生き物です。教科書の仕様を暗記するだけでなく、パケットが今どこでどう振る舞っているのか、その「質感」を想像できるようになると、インフラ運用はぐっと面白くなりますよ。
次回は、HTTP/2における多重化(Multiplexing)と、このConnectionヘッダーの概念がどう変容したのかについて深掘りしましょう。それでは、また現場で。
コメント