【実務・中級編】 TCP Fast RetransmitとFast Recoveryの仕組み – ネットワーク基礎とWebセキュリティ実践ガイド

タイムアウト待ちなんてしていられない!TCP Fast RetransmitとFast Recoveryで、ネットワークの「詰まり」を瞬時に解消する

ネットワークエンジニアとして現場に立っていると、ふと「なぜこのAPI呼び出しは、パケットロスがわずかなのにこんなにもレスポンスが遅延するのか?」という壁にぶち当たることがあります。

多くのエンジニアは、パケットロス=「RTO(再送タイムアウト)による停止」だと考えがちです。しかし、現代の高速通信を支えるTCPスタックは、そんなに愚かではありません。今日は、ネットワークの「渋滞」を察知し、タイムアウトを待たずに復旧する「Fast Retransmit(高速再送)」と「Fast Recovery(高速回復)」という、TCPの屋台骨を支える知的なアルゴリズムについて、現場の視点から紐解いていきましょう。

なぜ「タイムアウト」は悪手なのか

TCPの再送処理において、最も避けるべきはRTOです。送信側が「パケットが届いていない」と判断してRTOを待つということは、その間、ネットワーク上には何も流れない(アイドル状態になる)ことを意味します。

Web APIのレイテンシを極限まで削りたい我々にとって、RTOの発生は致命的です。そこで登場するのが、「3回重複ACK(Triple Duplicate ACK)」というシグナルです。

3回重複ACKのメカニズム

受信側は、パケットを順番通りに受信することを期待しています。もしパケットが途中で欠落すると、それ以降に届いた正しい順序のパケットに対しても、受信側は「期待しているシーケンス番号(欠落したパケットの番号)」を繰り返し要求し続けます。これを「重複ACK」と呼びます。

送信側がこの重複ACKを「3回」受け取ったとき、それは「ネットワークが混雑しているかもしれないが、少なくとも一部のパケットは届いている(=経路は生きている)」という強力な証拠になります。ここでTCPは、RTOを待たずに即座に再送を開始するのです。これが Fast Retransmit です。

Fast Retransmit & Fast Recoveryのフロー

この挙動を理解するには、シーケンスチャートを頭に描くのが一番です。

1. パケットロス発生: 送信側が Seq 1000 を送るが、途中で消失。
2. 重複ACKの蓄積: 送信側が続く Seq 2000, 3000, 4000 を送ると、受信側は全てに対して「1000をくれ!」というACKを返す。
3. 高速再送トリガー: Seq 1000 に対するACKが3回重なった瞬間、送信側は「1000が消えた」と確定させ、即座に再送する。
4. 混雑制御(Fast Recovery): ここでCWND(輻輳ウィンドウ)を「半分」に減らし、Slow Start(スロースタート)からやり直すのではなく、減らしたウィンドウサイズから通信を維持する。

これにより、通信速度をガクンと落とすことなく、緩やかに再開できるわけです。

実践:ツールで「再送の兆候」を掴む

現場でこの挙動を観測する場合、tcpdump は必須スキルです。以下のコマンドで、重複ACKの発生状況を確認できます。

# 特定のポートへの通信をキャプチャし、重複ACKを監視する
# 'tcp[13] & 0x10 != 0' はACKフラグ、その後のパケットサイズが0かつシーケンス番号が変化しないものを絞り込む
sudo tcpdump -i eth0 port 443 -n 'tcp[13] == 0x10'

また、Linuxカーネルの設定を調整することで、再送の振る舞いを変えることも可能です。sysctl で確認できる net.ipv4.tcp_reordering パラメータは、ネットワークの順序入れ替わり(Reordering)をどの程度許容するかを決定します。

# 現在の設定値を確認
sysctl net.ipv4.tcp_reordering

# もし順序入れ替わりが激しいモバイル回線等を想定するなら、値を少し大きく調整する(デフォルトは3)
sudo sysctl -w net.ipv4.tcp_reordering=5

Pythonで体感する:パケットロスと再送の制御

実際にコードを書く際、TCPレベルの細かな制御はOSカーネルに任せるのが鉄則ですが、socket オプションを使ってバッファサイズ等を調整することで、再送アルゴリズムが効率よく働く環境を整えることは可能です。

import socket

# ソケット作成
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 送信バッファサイズを調整して、ACKが溜まりやすい環境を意図的に作る
# 実務では、高遅延ネットワーク向けに大きめに設定することがある
s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 65536)

# TCP_NODELAYを無効化(Nagleアルゴリズム)して、小パケットを即時送信させる
# APIの応答性を重視する場合はこれを有効にするケースが多い
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

print("ソケットの設定が完了しました。")

現場のエンジニアへのアドバイス

「パケットロスが起きているから、とりあえず帯域を広げよう」というのは、多くのケースで的外れです。ネットワークの物理的な詰まりなのか、TCPの再送アルゴリズムがうまく働いていない(あるいは、過敏に反応しすぎている)のかを見極めることが重要です。

特に、クラウド環境のマルチテナントなネットワークでは、一時的なパケットの入れ替わりが頻発します。この際、TCP Fast Recovery が適切に機能しているか、またはカーネルパラメータが環境に最適化されているかを確認するだけで、APIのレスポンスタイムが劇的に改善することは珍しくありません。

理論を知ることは、トラブルシューティングの「勘」を鋭くします。次に curl でリクエストが詰まったら、その裏で何万回ものパケットが泣きながら再送を繰り返している情景を想像してみてください。その時、解決への糸口は必ず見えてくるはずです。

コメント

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