はじめに:なぜ、Web API設計やインフラ運用で「TCPの奥底」を知る必要があるのか
「なあ、最近リリースしたあの新しいWeb API、なぜか特定の海外リージョンからの呼び出しで時々テールレイテンシ(p99)が跳ね上がるんだよな。アプリケーションのログを見ても、データベースのクエリは一瞬で終わっているのに……」
夜遅くのオフィスで、後輩のインフラエンジニアが頭を抱えてモニターを見つめていました。あなたも似たような経験はないでしょうか。CPU使用率もメモリも正常、アプリケーション層のコードも綺麗に書かれている。それなのに、なぜかレスポンスが妙に遅い、あるいはプチプチとタイムアウトが起きる。
この謎を解くカギは、私たちが普段何気なく使っているHTTPやgRPCの下層、すなわちOSI参照モデルのトランスポート層で黙々と働いている TCP(Transmission Control Protocol)の「輻輳制御(Congestion Control)」 に隠されています。
Web APIの設計や大規模なインフラ運用において、TCPの挙動を無視することは「エンジンの仕組みを知らずにF1マシンのチューニングをする」ようなものです。今日は、パケットがインターネットの荒海をどのように泳ぎ、ルーターのバッファ溢れ(パケットロス)に直面したときになぜ急ブレーキを踏むのか、その泥臭いメカニズムを実務的な視点から紐解いていきます。
—
1. TCP輻輳制御の基本理念:スロースタートと輻輳回避のダンス
インターネットは「ベストエフォート型」のネットワークです。誰も全体のトラフィック量を一元管理していません。世界中のルーターやスイッチが、それぞれの限界ギリギリでパケットをルーティングしています。もし、新米エンジニアが書いたスクリプトのように、接続した瞬間から限界までパケットを送りまくったらどうなるでしょうか?
当然、ルーターのキュー(バッファ)はパンクし、パケットは次々と破棄(ドロップ)されます。それを防ぐためにTCPに備わっているのが、スロースタート(Slow Start) と 輻輳回避(Congestion Avoidance) という、いわば「慎重さと大胆さのダンス」です。
1.1 スロースタート(Slow Start):控えめな始まりから指数関数的増加へ
TCPコネクションが確立した瞬間、送信側は急に全開でデータを送りません。cwnd(Congestion Window:輻輳ウインドウ)という「相手からのACKを待たずに送信できるデータ量の上限」を小さな値(現代のLinuxカーネルでは初期値として10セグメント程度)からスタートさせます。
送信側がデータを送ると、受信側から確認応答である ACK が返ってきます。この ACK を受信するたびに、cwnd はなんと指数関数的(2倍、4倍、8倍……)に膨れ上がっていきます。「最初は慎重に、しかし相手が受け取れることが分かったら一気にアクセルを踏む」のがスロースタートの正体です。
1.2 輻輳回避(Congestion Avoidance):リニアな探索への切り替え
無限に指数関数的増殖を続けたら、いつか必ずネットワークが爆発します。それを防ぐために、あらかじめ ssthresh(Slow Start Threshold:スロースタート閾値)というリミッターを設けておきます。
cwnd がこの ssthresh を超えた瞬間、アルゴリズムはスロースタートから輻輳回避モードへとシフトします。ここからはアクセルを踏み込むペースが緩やかになり、ACK が返ってくるたびに cwnd は 1RTT(Round Trip Time)あたり1セグメントずつ、直線的(リニア)にしか増加しなくなります。ネットワークの「空き容量」を慎重に探るフェーズに入るわけです。
—
2. パケットロス発生時の挙動:TCPが「混雑」を検知した瞬間
では、ネットワークの許容量を超えてルーターのバッファが溢れ、パケットが消滅(ロス)したときは何が起きるでしょうか?
TCPはパケットロスを検知するために、主に2つのシグナルを使います。
1. タイムアウト(Retransmission Timeout: RTO): 送信したパケットに対する ACK が一定時間帰ってこない。
2. 重複ACK(Duplicate ACK): 順序が狂ったパケットを受け取った受信側が、直前の正しい連続した ACK を何度も送りつけてくる(通常、3回の重複ACKがトリガーとなります)。
高速リカバリ(Fast Recovery)と急ブレーキ
重複ACKを3回検知した(3 DupACK)場合、TCPは「まだ一部のパケットは届いているが、一部が脱落した」と判断し、パケットの再送(Fast Retransmit)を行うと同時に、高速リカバリ(Fast Recovery)に入ります。
ここで ssthresh は「当時の cwnd の半分」に絞られ、cwnd 自体もその新しい ssthresh の値付近まで急降下させられます。つまり、「ネットワークが混雑しているサインを検知したら、送信レートを即座に半減させる」という荒技です。これが、私たちがインフラで目撃する「急激なスループットの低下」の正体です。
—
3. 実務で役立つ通信フロー(シーケンス)
ここまでの流れを、シンプルなシーケンス図(テキスト表現)で確認しておきましょう。
[Client / API Caller] [Router / Network] [Server / API Backend]
| | |
| -------- SYN (cwnd=10) -------------> | ------------------------> |
| <------- SYN-ACK -------------------- | <------------------------ |
| -------- ACK (Connection Established) | ------------------------> |
| | |
| [Slow Start Phase: 指数関数的に増加] | |
| -------- Data (Seq 1-10) -----------> | |
| -------- Data (Seq 11-20) ----------> | |
| <------- ACK 11 --------------------- | |
| <------- ACK 21 --------------------- | |
| | |
| [Congestion Avoidance: リニア増加] | |
| -------- Data (Seq 21-30) ----------> | |
| xxxxxxxx (Packet Drop at Router) xxxx | |
| -------- Data (Seq 31-40) ----------> | |
| <------- Dup ACK 31 (1回目) --------- | |
| <------- Dup ACK 31 (2回目) --------- | |
| <------- Dup ACK 31 (3回目) --------- | |
| | |
| [Fast Retransmit & Recovery] | |
| -------- Retransmit Data (Seq 21) --> | |
| <------- New ACK (Recovery Complete)- | |
| | |
この泥臭いキャッチボールが、私たちが叩くたった1つの GET /api/v1/resource の裏側で繰り広げられているのです。
—
4. 現場のインフラ・コード実例:TCPパラメータのチューニングと確認
では、このTCPの挙動を、実際の開発やインフラ運用の現場でどのようにコントロールすればよいのでしょうか。Linux環境における設定と、APIクライアントからのデバッグ手法を見ていきます。
4.1 Linuxカーネルパラメータの確認・変更 (sysctl)
現代のLinuxでは、デフォルトの輻輳制御アルゴリズムとして従来の CUBIC に加え、Googleが開発した遅延ベースのアルゴリズム BBR(Bottleneck Bandwidth and RTT) が利用できます。BBRはパケットロスではなく「帯域と遅延」をベースに制御するため、高レイテンシ・ロス率の高い回線(グローバル通信など)で圧倒的なパフォーマンスを発揮します。
現在のカーネルがサポートしているアルゴリズムの確認:
# 利用可能な輻輳制御アルゴリズムの一覧を取得する
sysctl net.ipv4.tcp_available_congestion_control
# 出力例: reno cubic bbr
現在適用されているアルゴリズムの変更(例としてBBRを即時適用):
# 一時的に輻輳制御をBBRに変更する
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
# 永続化する場合は /etc/sysctl.conf に以下を追記する
# net.core.default_qdisc = fq
# net.ipv4.tcp_congestion_control = bbr
4.2 PythonによるAPIリクエストとTCPソケットの状態確認
Web APIを設計・運用する際、クライアント側(例えばPythonの requests や httpx)からコネクションの挙動を意識することは稀ですが、低レイヤーの挙動を調査する際には ss コマンドやPythonのソケットオプションが強力な武器になります。
以下は、PythonでAPIにアクセスしつつ、ソケットの輻輳情報(TCP_INFO)を覗き見るデバッグスクリプトの例です。
import socket
import urllib.request
# デバッグ対象のAPIエンドポイント
url = "http://example.com/api/health"
print(f"Connecting to {url} and inspecting TCP socket stats...")
# HTTPリクエストの送信
req = urllib.request.Request(url)
with urllib.request.urlopen(req) as response:
# レスポンスボディの読み込み
body = response.read()
print(f"Status Code: {response.status}")
# 内部のソケットオブジェクトを取り出す(urllibの内部実装に依存するため簡易的な例)
# 実務では psutil ライブラリや ss コマンドの併用を推奨します
sock = response.fp.raw._sock
# TCP_INFOソケットオプションからパケットロスやRTT(往復遅延時間)を取得する
# ※ Linux環境でのみ動作します
try:
# 11は SOL_TCP, 2は TCP_INFO を表す
tcp_info = sock.getsockopt(socket.SOL_TCP, socket.TCP_INFO, 92)
print("--- TCP Socket Info (Raw bytes captured) ---")
print(f"Captured {len(tcp_info)} bytes of TCP metrics.")
except Exception as e:
print(f"Could not retrieve TCP_INFO: {e}")
4.3 現場で使えるデバッグコマンド (ss, tcpdump)
インフラのトラブルシューティングでパイルアップ(滞留)が起きたとき、私たちが最初に叩くのは ss コマンド(旧 netstat の後継)です。
# 現在確立されているTCPコネクションの輻輳ウインドウ(cwnd)やRTTを詳細表示する
ss -i 'sport = :http or dport = :http'
このコマンドの出力結果には、cwnd: や rtt: といった生々しい数値が表示されます。もし特定のクライアントとの間で rtt が異常に高く、retrans(再送カウント)がガンガン増えているのであれば、それはアプリケーションのバグではなく、途中のネットワーク経路における輻輳、あるいはMTU(Maximum Transmission Unit)のミスマッチによるパケット断片化が原因だと特定できます。
—
おわりに:レイヤーを跨いだ視点を持つエンジニアへ
Web APIの設計やインフラ運用において、「アプリケーションが動けば下層のことはクラウドやOSが勝手にやってくれる」という時代は終わりつつあります。グローバル展開するSaaS、マイクロサービス間の複雑な通信、そして高負荷なトラフィックをさばく現場では、今回紹介した TCPの輻輳制御(スロースタートと輻輳回避) のような基盤技術の知識が、障害切り分けのスピードを何倍にも高めてくれます。
「なぜこのリクエストだけ遅いのか?」
そう悩んだときは、目の前のソースコードから少しだけ視線を下げ、パケットがネットワークの荒海をどう泳ぎ、どこでブレーキを踏んでいるのかを想像してみてください。その深い洞察力こそが、あなたを真のシニアエンジニアへと導く羅針盤となるはずです。
コメント