こんにちは。現場でパケットの叫びを聞き続けて20年、シニアネットワークエンジニアの筆者です。
モダンなWebアプリケーションを開発していると、どうしても「レイテンシ」や「スループット」という言葉に敏感になりますよね。クラウドネイティブな時代、我々は Fetch API や axios を叩き、バックエンドでは gRPC や WebSockets が飛び交っています。しかし、その足元を支える「TCP」というプロトコルが、どれほど泥臭く、そして健気に「渋滞」と戦っているか、意識したことはあるでしょうか?
「なぜか特定の環境でAPIのレスポンスがガクンと落ちる」「帯域は太いはずなのに速度が出ない」――こうした問題の裏側には、必ずと言っていいほど TCP輻輳制御(Congestion Control) の挙動が隠れています。
今日は、OSI参照モデルのトランスポート層(第4層)で繰り広げられる、パケットたちの知られざる生存戦略について、実務に即した視点で解説していきましょう。
—
1. なぜ「輻輳制御」が必要なのか?
ネットワークの世界は、常に「欲張りな送信者」と「限界のあるルーター」の戦いです。
もしTCPに制御がなければ、送信側はリンクの限界までパケットを叩き込みます。すると、途中のルーターのバッファが溢れ(バッファオーバーフロー)、パケットがドロップします。ドロップすれば再送が発生し、さらに帯域を圧迫する……。これが「輻輳崩壊」です。
これを防ぐために、TCPは 「ネットワークがどれくらい混んでいるか」を、パケットの届き具合(ACKの返り方)から推測 します。
重要な2つのウィンドウ
rwnd(Receiver Window): 受信側が「今はこれだけしか受け取れないよ」と申告するサイズ。cwnd(Congestion Window): 送信側が「ネットワークの混み具合から判断して、これくらいなら送っても大丈夫だろう」と自制するサイズ。
実際の送信データ量は、min(cwnd, rwnd) で決まります。輻輳制御アルゴリズムの真髄は、この cwnd をいかにダイナミックに、かつ賢く増減させるかにあります。
—
2. 伝統的なアルゴリズムの3つのフェーズ
TCPの輻輳制御は、主に「スロースタート」「輻輳回避」「高速再送・高速回復」というステップを踏みます。
① スロースタート (Slow Start)
「スロー」という名前ですが、実は指数関数的に加速する最もアグレッシブな段階です。
1つのパケット(セグメント)を送ってACKが返ってきたら、次は2個、その次は4個……と、cwnd を倍々に増やしていきます。
- 実務上のポイント: 初期の
cwnd(initcwnd)は、かつては1〜3でしたが、現在のLinuxカーネルではデフォルトで 10 に設定されています(RFC 6928)。これは、一般的なWebページの初期表示データを1ラウンドトリップ(RTT)で送り切るための最適解です。
② 輻輳回避 (Congestion Avoidance)
cwnd が一定のしきい値(ssthresh)に達すると、慎重なモードに切り替わります。
倍々ゲームをやめ、1往復ごとに cwnd を 1 つずつ増やす「線形増加」に入ります。
③ 高速再送 (Fast Retransmit) と高速回復 (Fast Recovery)
パケットが1つだけ落ちた場合、受信側は「抜けているパケット」を催促するために、同じACK番号を何度も返します。
「3回の重複ACK(Duplicate ACKs)」 を受け取った瞬間、送信側はタイマーを待たずに即座に再送を行います。これが「高速再送」です。
—
3. アルゴリズムの進化:Tahoe, Reno, そして Cubic
時代の変遷とともに、この cwnd の制御ロジックは進化してきました。
- TCP Tahoe: パケットロスを検知したら、
cwndを 1 まで一気に落とす。まさに「石橋を叩き壊して渡る」古風な仕様。 - TCP Reno: 現在の基礎。重複ACKによるロスなら、
cwndを半分にするだけで済ませる(高速回復)。 - TCP Cubic: 現在のLinux(Ubuntu, CentOS等)のデフォルト。
- 特徴:時間の3次関数を用いて
cwndを制御します。帯域が広い環境では一気に増やし、限界に近づくと緩やかに探りを入れる、非常に洗練されたアルゴリズムです。
—
4. インフラ・開発現場で使える実践テクニック
ここからは、シニアエンジニアが現場で使う「確認と設定」のTipsです。
Linuxサーバーの輻輳制御アルゴリズムを確認する
あなたのサーバーが今、どのアルゴリズムを使っているか知っていますか? 以下のコマンドで一発です。
# 現在利用可能なアルゴリズムを表示
sysctl net.ipv4.tcp_available_congestion_control
# 現在適用されているアルゴリズムを表示(多くの場合 'cubic' または 'bbr')
sysctl net.ipv4.tcp_congestion_control
もし、Googleが開発した最新の BBR(パケットロスではなく遅延をベースに制御するアルゴリズム)に変えたい場合は、以下のように設定します。
# BBRを有効化する設定(root権限が必要)
# /etc/sysctl.conf に追記することで恒久化可能
sudo sysctl -w net.core.default_qdisc=fq
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
Pythonでソケットの状態を覗き見る
Web APIのパフォーマンスが出ないとき、アプリケーション層からTCPの状態をサンプリングすることがあります。Pythonの getsockopt を使うと、現在の cwnd などの生の情報が取得できます。
import socket
import struct
# ダミーの接続(例:自社のAPIサーバー)
target_host = "example.com"
target_port = 80
with socket.create_connection((target_host, target_port)) as sock:
# HTTPリクエストを送信
sock.sendall(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
# TCP_INFO 構造体を取得(Linux固有)
# 構造体のフォーマットはカーネルのバージョンにより異なります
tcp_info_raw = sock.getsockopt(socket.IPPROTO_TCP, socket.TCP_INFO, 1024)
# 構造体をアンパック(抜粋:状態、オプション、cwndなど)
# 以下のオフセットは一般的な環境の例です
unpacked_info = struct.unpack("B" * (len(tcp_info_raw)), tcp_info_raw)
# Linuxの tcp_info 構造体で cwnd は特定のインデックスにある
# 実際には 'ss' コマンドで見るのが手っ取り早いが、コード内での監視に有効
print(f"Current Congestion Window (cwnd): {unpacked_info[28]}")
デバッグの神ツール: ss コマンド
netstat はもう古い。現場では ss コマンドを使い倒しましょう。-i オプションをつけると、TCPの内部パラメータが丸見えになります。
# 特定のポートへの接続状況と、その内部パラメータ(cwnd, rtt等)を表示
ss -ti sport = :443
出力結果に含まれる cwnd:10 や ssthresh:7 という数字を追うことで、「あ、今スロースタートで詰まってるな」とか「頻繁に再送が発生して cwnd が絞られているな」といった、物理レイヤーに近い問題の切り分けが可能になります。
—
5. Web API設計者へのメッセージ
「APIのレスポンスが遅い」と感じたとき、コードを最適化する前に、「TCPコネクションの使い回し(Keep-Alive)」 ができているか確認してください。
新規にTCPコネクションを張るたびに、TCPは「スロースタート」からやり直しになります。つまり、せっかくの太い帯域も、接続直後は数パケットずつしか送れません。
特にデータ量の多いJSONを返すAPIや、高頻度な通信を行うマイクロサービス間では、initcwnd の壁を意識することが、パフォーマンス改善の大きな鍵となります。
まとめ
TCP輻輳制御は、インターネットという巨大な共有資源を、みんなで公平に、かつ効率的に使うための「知恵」の結晶です。
- スロースタート: 最初は慎重に、でも急加速。
- 輻輳回避: 限界が見えたら一歩ずつ。
- Cubic/BBR: 現代の高速ネットワークに最適化された賢い制御。
パケットがネットワークを駆け抜けるその一瞬一瞬に、こうした緻密な計算が行われている。それを想像しながらコードを書くようになれば、あなたも立派な「ネットワークに強いエンジニア」です。
次は、Wiresharkを開いて、実際に cwnd が増減するグラフを眺めてみてください。きっと、昨日よりもパケットが愛おしく見えるはずです。
コメント