「なぜ、そのAPIは遅いのか?」―TCP 3ウェイ・ハンドシェイクをハックするKeep-Aliveの真実
ネットワークエンジニアとして現場を歩いていると、APIのレスポンスタイムに悩む若手から「コードは完璧なはずなのに、なぜか初回リクエストだけが極端に遅い」という相談をよく受ける。
その原因の9割は、アプリケーション層のロジックではなく、OSの足元である「TCPコネクションの断片化」にある。Web APIを設計する際、URIの美しさやJSONの構造に頭を悩ませるのも大切だが、そのデータが物理的にどう運ばれているかを想像できなければ、真の「美しいAPI」とは言えない。
今回は、HTTP/1.1の生命線であり、パフォーマンス最適化の要である Keep-Alive と、その裏側で蠢くコネクション再利用のメカニズムについて、現場の視点から深掘りしていこう。
—
1. TCPハンドシェイクという「儀式」のコスト
HTTP/1.1では、デフォルトで Connection: keep-alive が有効になっている。これは、リクエストごとにコネクションを破棄せず、使い回すための仕組みだ。
なぜこれが重要か? それは、TCP通信を始めるために必ず行われる「3ウェイ・ハンドシェイク(SYN -> SYN/ACK -> ACK)」が、往復のネットワーク遅延(RTT)を浪費するからだ。
もしあなたがレイテンシ10msの環境で、APIを一つ叩くたびにコネクションを張って切断していたら、実際のデータ通信が行われる前に、すでに20ms以上の時間を「儀式」に費やしていることになる。HTTPSであれば、ここにさらにTLSハンドシェイクのオーバーヘッドが加わる。これでは、どんなに高速なバックエンドを組んでも勝負にならない。
シーケンスから見る「再利用」の恩恵
- コネクションを切断する場合:
SYN->SYN/ACK->ACK-> [データ] ->FIN->ACK->FIN->ACK - コネクションを維持する場合:
SYN->SYN/ACK->ACK-> [データ] -> [データ] -> [データ]
ご覧の通り、2回目以降のリクエストは「ハンドシェイクのコストをゼロ」にできる。この差は、高トラフィックなAPIサーバーにおいて、CPU負荷とレイテンシの両面で劇的な違いを生む。
—
2. 実践:Keep-Aliveを制御するパラメーターの匙加減
「コネクションを維持すればいいのか、じゃあ永遠に繋いでおけばいいじゃないか」と思うかもしれないが、それは大きな罠だ。インフラには「リソースの限界」がある。
Keep-Alive の設定で最も重要なのは、Keep-Alive Timeout と Max Requests のバランスである。
Nginxでの設定例
/etc/nginx/nginx.conf に以下の設定を入れるのが定石だ。
# クライアントとのコネクションを維持する時間(秒)
keepalive_timeout 65;
# 1つのコネクションで処理する最大リクエスト数
# 高負荷環境ではこれを調整することで、メモリの解放を促す
keepalive_requests 1000;
ここでのポイントは、keepalive_timeout を短すぎると効果がなく、長すぎると「アイドル状態の接続」がサーバーのメモリを食いつぶすという点だ。APIの特性に合わせて、「どれくらいの頻度でリクエストが来るか」を監視しながらチューニングする必要がある。
—
3. クライアント側の実装:Fetch APIとPythonでの挙動
Web APIを呼び出すクライアント側でも、この挙動を意識しておく必要がある。
Python (requestsライブラリ) の場合
requests はデフォルトで Session オブジェクトを使うことでコネクションを再利用する。これを理解していないと、毎回新しいセッションを作ってしまい、ハンドシェイクの嵐に見舞われることになる。
import requests
# Sessionを使うことで、内部的にコネクションプールが維持される
session = requests.Session()
# この通信で張られたTCPコネクションは、次のリクエストでも再利用される
response1 = session.get('https://api.example.com/v1/resource')
response2 = session.get('https://api.example.com/v1/resource')
ブラウザ (Fetch API) の場合
ブラウザは賢い。同一ドメインへのリクエストであれば、ブラウザ側が自動的にコネクションをプールしてくれる。しかし、ドメインが異なれば別コネクションになるため、APIのドメインを細かく分けすぎると、かえってコネクションのオーバーヘッドが増えるという逆転現象が起きる。これぞまさに設計上のアンチパターンだ。
—
4. 現場でのトラブルシューティング:どうやって確認するか?
「本当にコネクションが再利用されているのか?」と疑うことは、プロの第一歩だ。まずは curl でヘッダーを覗いてみよう。
# -v オプションで詳細な通信フローを表示
curl -Iv https://api.example.com/v1/resource
出力結果の中に Connection: keep-alive が含まれているか、そして * Re-using existing connection というログが出るかを確認する。もし毎回 * Connected to ... というログが出るなら、コネクション再利用が効いていない証拠だ。
最後に:ネットワークを「意識する」開発者であれ
優れたAPIアーキテクトは、コードが動くことだけを考えない。その背後でパケットがどう流れ、OSがどうリソースを管理し、ネットワーク機器がどうトラフィックを捌いているかを、まるで自分の手の中にあるかのようにイメージしている。
Keep-Alive の最適化は、地味な設定かもしれない。しかし、そのわずかなチューニングが、アクセス集中時のサーバーダウンを防ぎ、ユーザーに「爆速」の体験を提供するための、最も信頼できる防波堤となるのだ。
皆さんのAPIが、今日も効率よく、美しくネットワークを駆け巡ることを願っている。
コメント