【実務・中級編】 TCP輻輳制御アルゴリズム(TCP Reno/Cubic) – ネットワーク基礎とWebセキュリティ実践ガイド

TCP輻輳制御の深層:RenoからCubicへ、パケットの迷宮と生き残りのアルゴリズム

おい、少し手を止めて聞いてくれ。
お前が何気なく叩いている curl https://api.example.com/v1/data というコマンド、あるいはフロントエンドから飛ばす fetch() の裏側で、ネットワークがどれだけのドラマを生んでいるか意識したことはあるか?

「APIのレスポンスがやけに重い」「クラウド間通信でスループットが頭打ちになる」。
そんなインフラのトラブルに直面したとき、多くのエンジニアはアプリケーションコードを疑ったり、DBのインデックスをいじったりしがちだ。だが、ちょっと待て。レイヤー4の地下深く、TCPのステートマシンと輻輳制御アルゴリズムの挙動を覗いたことはあるか?

今日は、数々のネットワークの修羅場をくぐり抜けてきた俺が、TCP輻輳制御の王道である TCP Reno と、現代のインターネットを支える TCP Cubic のリアルな挙動について、実務的な視点から徹底的に解説してやる。これを読めば、お前もパケットの息遣いが聞こえるようになるはずだ。

—

1. なぜ「輻輳制御」が必要なのか? ―― 楽観主義の限界

OSI参照モデルのトランスポート層(TCP)は、信頼性のある通信を保証するために存在する。しかし、インターネットは巨大な共有のジャングルだ。お前のサーバーとクライアントの間には、無数のルーターやスイッチが存在し、それぞれがパケットを処理している。

もし、送信側がルーターの処理能力などお構いなしに、限界までパケットを送り続けたらどうなるか? ルーターのバッファ(キュー)は瞬く間に溢れ返り、パケットロスが連鎖的に発生する。これを輻輳(Congestion)と呼ぶ。

この輻輳を防ぎ、ネットワーク全体が崩壊(グリッドロック)するのを防ぐために実装されているのがTCP輻輳制御アルゴリズムだ。送信側は、ネットワークがどれだけ混雑しているかを推測し、自ら送信レートを動的に調整している。その判断基準となるのが、お馴染みの「パケットロス」と「ACK(確認応答)の遅延・欠落」だ。

—

2. 伝統のシグネチャ:TCP Renoの挙動と歯痒さ

まずは、古典的かつ歴史的なマイルストーンである TCP Reno から見ていこう。Renoは、主に「スロースタート」「輻輳回避」「高速リカバリ(Fast Recovery)」という3つのフェーズで動作する。

2.1 Renoの基本パラメーター

  • cwnd (Congestion Window / 輻輳ウィンドウ): 送信側がACKを待たずに送信できるパケットの最大数。
  • ssthresh (Slow Start Threshold / スロースタート閾値): スロースタートから輻輳回避へ切り替える境界線。

2.2 スロースタートと輻輳回避のダンス

接続確立直後、cwnd は小さく(通常は初期ウィンドウとして10セグメント程度)設定される。Renoはここから cwnd を指数関数的に増やしていく(スロースタート)。つまり、ACKが1つ返ってくるたびに、cwnd が1ずつ増えるため、RTT(往復遅延時間)ごとのウィンドウサイズは倍々に膨れ上がっていく。

そして、cwnd が ssthresh を超えると、モードは輻輳回避(Congestion Avoidance)へ移行する。ここでは、指数関数的な増加から線形増加(RTTごとに cwnd を 1 ずつ増やす)へとシフトし、慎重に帯域を探る。

2.3 パケットロス発生時の悲劇

Renoの最大の弱点は、パケットロスを検知したときのリアクションだ。
タイムアウト、あるいは3つの重複ACK(Triple Duplicate ACK)によってロスを検知すると、Renoは以下のように動く。

1. ssthresh を当時の cwnd の半分に絞る(ssthresh = cwnd / 2)。
2. cwnd を一気に 1 (または ssthresh の値)まで落とす。
3. 再びスロースタートからやり直す。

お分かりだろうか? 高速な回線(広帯域かつ遅延が大きい、いわゆるBDP(Bandwidth-Delay Product)が大きいネットワーク)において、たった1つのパケットが消えただけで、Renoは送信レートを急激にゼロ付近まで落としてしまう。これでは、せっかくの太いパイプラインがガラガラになってしまう。これが「Renoの歯痒さ」だ。

—

3. 現代のデファクト:TCP CUBICによるスマートな帯域奪取

Renoの限界を打ち破るために開発され、現在Linux(Kernel 2.6.19以降)や多くのモダンOSでデフォルトとして採用されているのが TCP Cubic だ。

Cubicは、パケットロスが発生したときのウィンドウサイズの縮小カーブを、時間(経過時間)を横軸にとった3次関数(Cubic関数)としてモデル化している。

3.1 Cubicのアルゴリズムの肝

Renoが「ACKが返ってきた回数(ラウンドトリップ)」を基準にウィンドウを増やしていたのに対し、Cubicは「前回ロスが発生したときのウィンドウサイズ ($W_{max}$)」を記憶し、そこを頂点とするような3次カーブを描いてウィンドウを復旧させる。

  • ロス直後: ウィンドウは一旦ガクッと縮小するが、そこから急激に回復する。
  • $W_{max}$ 付近: $W_{max}$ に近づくにつれて増加スピードが緩やかになり、安全に境界を探る。
  • $W_{max}$ 超過後: さらに帯域が空いていると判断すれば、再び緩やかにウィンドウを広げていく。

この特性により、Cubicは「高速・大遅延なネットワーク(大陸間のクラウド通信など)」において、Renoよりも圧倒的に素早く最大の帯域に到達し、かつ安定したスループットを維持できるようになった。

—

4. 実務で直面するトラブルとチューニングTips

さて、ここからが現場のエンジニアとしての腕の見せ所だ。Web APIのパフォーマンスチューニングや、大容量ファイルの転送基盤を設計する際、このTCPの挙動がボトルネックになることが多々ある。

4.1 現在の輻輳制御アルゴリズムの確認と変更方法

Linux環境(UbuntuやCentOS等)であれば、以下のコマンドで現在どのアルゴリズムが使われているか確認できる。

# 現在システムで有効な輻輳制御アルゴリズムを確認
$ sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = cubic

# 利用可能なアルゴリズムの一覧を取得
$ sysctl net.ipv4.tcp_available_congestion_control
net.ipv4.tcp_available_congestion_control = reno cubic bbr

もし古いシステムやコンテナベースで reno になっている場合は、即座に cubic(あるいはさらに進んだ bbr)へ変更を検討すべきだ。一時的な変更は以下のコマンドで行える。

# カーネルパラメータを動的にCubicに変更
$ sudo sysctl -w net.ipv4.tcp_congestion_control=cubic

永続化するには、/etc/sysctl.conf に以下を追記する。

# /etc/sysctl.conf
net.ipv4.tcp_congestion_control = cubic

4.2 Pythonスクリプトを用いたAPIクライアントでの検証・計測

Web APIを設計・運用する際、クライアント側から見たパケットの挙動やスループットを計測するための簡単なスクリプトを用意した。requests ライブラリを使い、コネクションの再利用(Keep-Alive)やタイムアウト、レスポンスタイムの細かな挙動を追うためのベースとして活用してほしい。

import time
import requests

# 接続先のAPIエンドポイント(テスト用に適宜変更してください)
API_ENDPOINT = "https://httpbin.org/delay/1"

def measure_api_performance(request_count: int):
    """
    APIへのリクエストを連続して行い、TCPコネクション確立や
    スロースタートの影響を含めたレスポンスタイムを計測する関数
    """
    print(f"[*] APIパフォーマンス計測開始: {API_ENDPOINT} (総リクエスト数: {request_count})")
    
    # セッションを利用することでTCPコネクションの確立(3-way handshake)を維持し、
    # 輻輳ウィンドウが最適化された状態での挙動を確認する
    with requests.Session() as session:
        for i in range(1, request_count + 1):
            start_time = time.time()
            try:
                response = session.get(API_ENDPOINT, timeout=5)
                duration = time.time() - start_time
                
                print(f"[Request {i:02d}] Status: {response.status_code} | Time: {duration:.4f} sec")
            
            except requests.exceptions.RequestException as e:
                print(f"[Request {i:02d}] 接続エラー発生: {e}")
            
            # 連続リクエストの間隔(必要に応じて調整)
            time.sleep(0.1)

if __name__ == "__main__":
    measure_api_performance(5)

このスクリプトを実行すると、初回のコネクションオープン時と、2回目以降のコネクションが温まった状態(輻輳ウィンドウが拡大した後)でのレイテンシの違いが体感できるはずだ。これこそが、TCP層が裏側でやってくれている最適化の証拠に他ならない。

—

5. まとめ

ネットワークのパケットは、ただ闇雲に目的地へ走っているわけではない。送信側と受信側、そしてその間に横たわる無数のルーターが、パケットのロスという「痛み」を共有しながら、暗黙の了解(輻輳制御アルゴリズム)に基づいて絶妙なバランスで速度を調整し合っている。

Web APIの設計やインフラのサイジングを行う際、「なぜかパケットロス率が0.1%あるだけでスループットが激減するのか?」という疑問にぶぶつかったなら、今回のRenoやCubicのアルゴリズムの挙動を思い出してほしい。

基礎を制する者が、ネットワークを制す。現場でトラブルシューティングに迷ったときは、アプリケーション層だけでなく、必ずレイヤー4の足元に目を向けてみるんだな。それじゃあ、また次の現場で会おう。

コメント

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