TCP輻輳制御 徹底入門 - スロースタートからCUBIC・BBR・bufferbloatまで

TCP輻輳制御 徹底入門 - スロースタートからCUBIC・BBR・bufferbloatまで

作成日:
読了:約41分
更新日:
この記事を読む人におすすめPR / Amazonアソシエイト

当サイトは Amazon.co.jp を宣伝しリンクすることで紹介料を得る手段を提供する、Amazonアソシエイト・プログラムの参加者です。価格・在庫はリンク先の最新情報をご確認ください。

回線は 1Gbps 出るはずなのに、東京とフランクフルトの間ではどうしても数十Mbps しか出ない。自宅で大きなファイルをアップロードし始めた途端、同じ回線のビデオ会議がガタガタになる。ロードバランサの台数を増やしても、テールレイテンシだけが下がらない。

こうした「回線の太さでは説明できない遅さ」の多くは、TCP の輻輳制御(congestion control)の性質で説明がつきます。輻輳制御は、送信側が「ネットワークはいまどれだけ受け入れられるか」を推測しながら送信量を調整する仕組みで、インターネットが破綻せずに動いている理由そのものでもあります。

この記事では、輻輳制御を一次ソースの RFC と実装を軸に整理します。1986年に実際に起きた輻輳崩壊から出発し、スロースタートと AIMD、高速再送と高速回復、損失ベースの限界と CUBIC、帯域と遅延をモデル化する BBR、bufferbloat と AQM、ECN と L4S、QUIC での扱い、そして Linux での確認方法と実務でのチューニング指針までを見ていきます。

TCP そのものの基礎(3ウェイハンドシェイク、シーケンス番号、ACK)は TCP と UDP の基本 で扱っているので、必要に応じて先に目を通してください。

なぜ輻輳制御が要るのか: 1986年の輻輳崩壊

TCP は当初、輻輳制御を持っていませんでした。受信側が申告するウィンドウ(後述の rwnd)を超えなければ送ってよい、という設計です。受信ホストが溢れないことは保証されますが、途中のルーターが溢れないことは誰も保証していません。

この設計の危うさは、John Nagle が RFC 896「Congestion Control in IP/TCP Internetworks」(1984年1月、現在は Historic)で指摘しています。混雑したネットワークでまだ届いていないデータグラムの再送が始まると、「すべてのパケットが数回ずつ送信され、スループットは通常のごく一部にまで落ちる」状態に陥る、というものです。これが輻輳崩壊(congestion collapse)です。パケットが落ちる、再送が増える、さらに混む、という正のフィードバックが回り、自己維持してしまうのが厄介な点です。

そして実際に起きました。Van Jacobson の論文「Congestion Avoidance and Control」(SIGCOMM '88)の冒頭は、こう始まります。

In October of '86, the Internet had the first of what became a series of 'congestion collapses'. During this period, the data throughput from LBL to UC Berkeley (sites separated by 400 yards and two IMP hops) dropped from 32 Kbps to 40 bps.

400ヤード(約370メートル)しか離れていない LBL とカリフォルニア大学バークレー校の間で、スループットが 32 Kbps から 40 bps へ、およそ1000分の1に落ちたという記述です。この「1000分の1」がきっかけとなって、スロースタート、輻輳回避、RTT 推定の改善、指数バックオフといった、今日まで使われているアルゴリズム群が生まれました。

原理の整理としては、Sally Floyd の RFC 2914「Congestion Control Principles」(2000年9月、BCP 41)が現在も参照されます。輻輳制御が担うのは大きく2つ、輻輳崩壊の回避とフロー間の公平性です。

NOTE

輻輳制御は「自分が速くなるため」の仕組みではなく、第一義的には「全員が共倒れしないため」の仕組みです。あとで触れるチューニングの話でも、この前提を外すとただの迷惑な実装になります。

cwnd と rwnd: 2つのブレーキ

TCP の送信量を縛るものは2つあります。混同されやすいので最初に整理します。

名前誰が決めるか何から守るか
rwnd(受信ウィンドウ)受信側受信バッファのあふれ(フロー制御)
cwnd(輻輳ウィンドウ)送信側経路上のルーター・リンクのあふれ(輻輳制御)

rwnd は TCP ヘッダのウィンドウフィールドで相手から明示的に通知されます。一方 cwnd はパケットの中には一切現れません。送信側が ACK の返り方や損失から「たぶんこれくらいまでなら送れる」と推測して、自分の中だけで持っている変数です。

実際に送信できる未確認データ量は、この2つの小さいほうで決まります。

送信可能量 = min(cwnd, rwnd)

高遅延の経路では rwnd 側がボトルネックになることもあります。帯域遅延積(BDP)は次の式で、これだけの量が「飛んでいる」状態を作れないと回線を使い切れません。

BDP [bytes] = 帯域 [bytes/s] x RTT [s]
 
例: 1 Gbps、RTT 200ms(東京 - フランクフルト相当)
  = 125,000,000 x 0.2 = 25,000,000 bytes = 約 25 MB

TCP のウィンドウフィールドは16ビットなので素の最大値は 65,535 バイトしかなく、これでは 25MB にまったく届きません。そのためウィンドウスケールオプション(RFC 7323)が必要になります。現代の Linux は自動チューニングで受信バッファを伸ばすので通常は問題になりませんが、「BDP に見合った量を飛ばせているか」はロングファットパイプのトラブルシュートで最初に確認すべき点です。

スロースタート: 探索は指数的に

RFC 5681「TCP Congestion Control」(2009年9月、Draft Standard、RFC 2581 を置き換え)が、輻輳制御の基本4アルゴリズムを定義しています。スロースタート、輻輳回避、高速再送、高速回復の4つです。

接続直後、送信側は経路の容量をまったく知りません。そこで小さな cwnd から始めて、ACK が返るたびに増やしていきます。RFC 5681 では初期ウィンドウ(IW)を SMSS のサイズに応じて 2〜4 セグメント相当と定めていますが、現在の実装はもっと大きな値を使います。

RFC 6928「Increasing TCP's Initial Window」(2013年4月、Experimental)が提案したIW10です。

IW = min(10 * MSS, max(2 * MSS, 14600))

Linux はこれを採用しています。カーネルソースの include/net/tcp.h に、次のようにそのまま書かれています。

/* TCP initial congestion window as per rfc6928 */
#define TCP_INIT_CWND		10

スロースタートの増え方は、ACK 1つにつき最大 SMSS バイト cwnd を増やす、というものです。1 RTT ごとに cwnd がおよそ倍になるので、増加は指数的です。「スロー」という名前は、増え方が遅いという意味ではなく、いきなり全開で送らず小さく始めるという意味です。

RTT 1回目:  cwnd = 10   [##########]
RTT 2回目:  cwnd = 20   [####################]
RTT 3回目:  cwnd = 40   [########################################]
RTT 4回目:  cwnd = 80   ...

指数的に増やし続けると、いずれ経路の容量を越えます。そこで ssthresh(slow start threshold)という閾値を持ち、cwnd が ssthresh 以上になったらスロースタートを抜けて輻輳回避に移ります。

輻輳回避と AIMD

輻輳回避フェーズでは、増やし方が線形になります。RFC 5681 は、ACK ごとの近似式としてこう書いています(同 RFC の式(3))。

cwnd += SMSS * SMSS / cwnd

cwnd が大きいほど1回の増分が小さくなるので、1 RTT あたりの増加はおよそ 1 SMSS に収まります。

一方、損失を検知したときは大きく削ります。RFC 5681 の式(4) は次のとおりです。

ssthresh = max(FlightSize / 2, 2 * SMSS)

この「ゆっくり増やして、まずいときは一気に半分」という組み合わせが AIMD(Additive Increase Multiplicative Decrease、加算増加・乗算減少)です。AIMD はネットワーク全体を安定させるだけでなく、複数のフローが同じボトルネックを共有したときに帯域を公平に収束させる性質を持つことが知られています。

時間軸で見ると、おなじみのノコギリ波になります。

cwnd
 ^
 |        /|      /|      /|
 |       / |     / |     / |      <- 輻輳回避(加算増加)
 |      /  |    /  |    /  |
 |     /   |   /   |   /   |
 |    /    |  /    |  /    |
 |   /     | /     | /     |      <- 損失検知で半減(乗算減少)
 |  /      |/      |/      |
 | /
 |/  <- スロースタート(指数増加)
 +------------------------------> time

高速再送と高速回復

損失を「再送タイマーの満了(RTO)」でしか検知できないと、待ち時間が非常に長くなります。そこで RFC 5681 は、3つの重複 ACK(duplicate ACK)の到着をもって損失とみなす高速再送(fast retransmit)を定めています。

送信:  seg1  seg2  seg3  seg4  seg5  seg6
              X(seg2 が消失)
受信 ACK:
  seg1 -> ACK 2
  seg3 -> ACK 2 (dup 1)
  seg4 -> ACK 2 (dup 2)
  seg5 -> ACK 2 (dup 3)  <- ここで seg2 をタイマー待ちせず即再送

重複 ACK が届くということは、後続のセグメントはネットワークから出ていっているという意味でもあります。そのため高速再送のあとはスロースタートまで戻らず、ssthresh の水準から輻輳回避を続けます。これが高速回復(fast recovery)です。RTO が起きた場合は経路の状態がまったく読めないため、cwnd を1セグメントまで落としてスロースタートからやり直します。

重複 ACK を数えるやり方には弱点があります。末尾のセグメントが落ちた場合は後続が無いので重複 ACK が発生せず、RTO まで待つことになります。また経路の並べ替え(reordering)を損失と誤認しがちです。これを改善したのが RFC 8985「The RACK-TLP Loss Detection Algorithm for TCP」(2021年、Proposed Standard)です。

  • RACK(Recent ACKnowledgment): 重複 ACK の個数ではなく、セグメントごとの送信時刻と SACK 情報を使い「十分時間が経ったのにまだ ACK されていない」を損失とみなす、時間ベースの判定
  • TLP(Tail Loss Probe): 末尾の損失に対して探索用のパケットを送り、ACK を引き出して RTO を回避する

Linux では RACK-TLP が有効になっており、macOS でも実装されています。手元の macOS 26.5 で netstat -s -p tcp を見ると、RACK 由来のカウンタが並んでいるのが確認できます。

$ netstat -s -p tcp | grep -i rack
	0 segment retransmitted in RACK recovery episodes
	0 SACK based rescue retransmit

損失ベースの限界と CUBIC

AIMD には、現代の高速・長距離ネットワークでは無視できない弱点があります。cwnd が大きいほど、元の水準に戻るまでにかかる時間が長くなることです。1 RTT に 1 セグメントずつしか増やせないので、cwnd を半分にされた後の回復に何千 RTT もかかります。

この不利さの程度は、BBR のドラフト(draft-ietf-ccwg-bbr-06)が RFC 9438 を引きながら具体的な数字で書いています。

for CUBIC, sustaining 10Gbps over 100ms RTT needs a packet loss rate below 0.000003% (i.e., more than 40 seconds between packet losses), and over a 100ms RTT path a more feasible loss rate like 1% can only sustain at most 3 Mbps

RTT 100ms で 10Gbps を維持するには損失率を 0.000003% 未満に抑える必要があり、現実的な 1% の損失率では最大 3 Mbps しか出ない、という指摘です。ここでの損失は輻輳とは無関係な無線区間のエラーなどでも起きるので、「回線は太いのに出ない」現象の正体になりえます。

CUBIC はこの問題に答えるアルゴリズムです。RFC 9438「CUBIC for Fast and Long-Distance Networks」(2023年8月、Proposed Standard)で標準化され、RFC 8312 を置き換え、RFC 5681 を更新しています(Reno より積極的に cwnd を増やすことを認めるためです)。

CUBIC は cwnd を時間の3次関数で決めます。

W_cubic(t) = C * (t - K)^3 + W_max
  • t: 直近の輻輳イベントからの経過時間(秒)
  • W_max: 輻輳イベント時のウィンドウサイズ
  • K: 輻輳がなければ W_max まで戻るのにかかる時間
  • C: スケーリング定数。RFC 9438 は C = 0.4 を SHOULD としています

減少側の係数 beta_cubic は 0.7 が SHOULD で、Reno の 0.5 より削り方が穏やかです。

3次関数を使う効能は、形を見ると直感的に分かります。

cwnd
 ^
 |                            ,-'''''  <- 凸部: 未知の領域を積極的に探索
 |                       ,.-''
 |     W_max ...........,''
 |                    ,'
 |                  ,'
 |  凹部: W_max 付近では慎重に近づく
 | ,'
 |'
 +-----------------------------------> t(輻輳イベントからの経過時間)

W_max の手前では増加が緩やか(凹)で、前回詰まった水準の周辺をしばらく丁寧に探ります。W_max を超えたあとは急峻(凸)になり、容量が増えていた場合に素早く追随します。そして増加量が経過時間の関数であって、ACK の回数の関数ではない点が重要です。RTT が短いフローと長いフローの間の不公平(RTT unfairness)が Reno より小さくなります。

CUBIC は Linux の標準の輻輳制御アルゴリズムです。カーネルの net/ipv4/Kconfig を見ると、デフォルト選択が DEFAULT_CUBIC になっています。

choice
	prompt "Default TCP congestion control"
	default DEFAULT_CUBIC
...
config DEFAULT_TCP_CONG
	string
	default "cubic" if DEFAULT_CUBIC
	...
	default "cubic"

macOS でも CUBIC が既定です。手元の macOS 26.5 で確認すると、稼働中のソケットがすべて CUBIC 側に計上されていました。

$ sysctl net.inet.tcp.cubic_sockets net.inet.tcp.newreno_sockets
net.inet.tcp.cubic_sockets: 319
net.inet.tcp.newreno_sockets: 0

BBR: 損失ではなく経路のモデルを見る

CUBIC も結局は「損失が起きたら減らす」という損失ベースのアルゴリズムです。損失が起きる時点ではすでにボトルネックのバッファが埋まっており、遅延が膨らんでいます。つまり損失ベースの輻輳制御は、構造的にバッファを埋めにいく性質を持ちます。

BBR(Bottleneck Bandwidth and Round-trip propagation time)は、Google が 2016年に ACM Queue で発表したアルゴリズムで、発想がまったく違います。損失をシグナルにするのではなく、ACK から観測した配送レートとRTTから経路のモデルを組み立て、「ボトルネック帯域ちょうどのペースで、キューをほとんど作らずに送る」ことを狙います。

Linux メインラインの net/ipv4/tcp_bbr.c の冒頭コメントが、核となる式をそのまま書いています。

bottleneck_bandwidth = windowed_max(delivered / elapsed, 10 round trips)
min_rtt = windowed_min(rtt, 10 seconds)
pacing_rate = pacing_gain * bottleneck_bandwidth
cwnd = max(cwnd_gain * bottleneck_bandwidth * min_rtt, 4)

帯域は直近10ラウンドトリップの最大値、最小 RTT は直近10秒の最小値で推定します。「最大帯域」と「最小 RTT」は同時には測れない(帯域を測ろうと詰め込むとキューができて RTT が伸びる)ため、BBR は状態機械で交互に探索します。

      +---> STARTUP ----+     STARTUP : 指数的に増やしてパイプを満たす
      |       |         |     DRAIN   : 作ってしまったキューを吐き出す
      |       v         |     PROBE_BW: 定常状態。帯域を上下に探る
      |     DRAIN   ----+     PROBE_RTT: 送信量を絞って min_rtt を測り直す
      |       |         |
      |       v         |
      +---> PROBE_BW ---+
      |     ^     |     |
      |     +-----+     |
      |                 |
      +--- PROBE_RTT <--+

BBR にはもう1つ重要な前提があります。pacing(ペーシング)です。cwnd 分をバースト的に吐き出すのではなく、計算したレートに従ってパケットを時間的に均して送ります。Linux では fq qdisc あるいは TCP 内部のペーシングがこれを担い、BBR は pacing なしでは設計どおりに動きません。

BBRv3 の現状

IETF では Congestion Control Working Group で draft-ietf-ccwg-bbr として標準化作業が進んでいます。2026年9月22日時点の最新版は draft-ietf-ccwg-bbr-06(2026年7月6日)で、Intended Status は Experimental、ドキュメントの状態は WG Document です。まだ RFC にはなっていません。このドラフトが規定しているのは BBR の第3版、すなわち BBRv3 です。

ドラフトが掲げる設計目標は次のとおりです。

  • フロー本来のスループット(あるいはその公平分)を O(log(BDP)) 時間で達成すること。平均 1% 程度までの損失率のもとでも達成すること
  • キュー圧(キューイング遅延と損失)を低く保つこと

具体的な目標値として、ボトルネックバッファに溜まる推定量を推定 BDP の 1.5 倍以下に抑えること、1ラウンドトリップあたりの損失率の上限を BBR.LossThresh = 2% とすることが挙げられています。BBRv1 が損失にほぼ反応しなかったのに対し、BBRv3 は損失率の閾値とヘッドルームという概念を持ち込んでいる点が大きな違いです。

ECN については、ドラフトは明確に態度を保留しています。

This experimental version of BBR does not specify a specific response to Classic [RFC3168], Alternative Backoff with ECN (ABE) [RFC8511] or L4S [RFC9330] style ECN. However, if the connection claims ECN support by marking packets using either the ECT(0) or ECT(1) code point, the congestion controller response MUST treat any CE marks as congestion.

Linux メインラインの取り込み状況については注意が必要です。2026年9月22日に確認した範囲では、net/ipv4/tcp_bbr.c はコメント・状態機械ともに BBRv1 相当の内容のままで、BBR.LossThresh のようなドラフト由来の機構は入っていません。直近のコミットも SPDX 修正や API 追随といった周辺の変更です。BBRv3 のコードは Google の google/bbr リポジトリの v3 ブランチで別途配布されています。ディストリビューションのカーネルで「bbr」を選んだ場合に何が動くかは、そのカーネルの出自で変わります。

WARNING

BBR を「速くなるアルゴリズム」として無条件に薦める記事が多くありますが、CUBIC など損失ベースのフローと同じボトルネックを共有したときの帯域の取り合いは、バッファの深さや BBR のバージョンに強く依存します。draft-ietf-ccwg-bbr-06 も公平性の保証は述べていません。切り替えるなら、実トラフィックでの計測とロールバック手段をセットにしてください。

bufferbloat: バッファは大きいほどよい、ではない

BBR が生まれた背景には bufferbloat(バッファ膨張)という問題があります。メモリが安くなるにつれ、ルーターや宅内機器、モバイル基地局に過剰に大きなバッファが積まれるようになりました。すると損失ベースの輻輳制御は、そのバッファを埋め切るまで送信量を増やし続けます。結果として、パケットは落ちないのに遅延が数百ミリ秒から数秒に膨らむという状態が生まれます。

[正常な状態]
送信 --> [ボトルネック 小さなキュー ] --> 受信      RTT: 20ms
 
[bufferbloat]
送信 --> [ボトルネック ####巨大なキュー#### ] --> 受信  RTT: 800ms
                        ^
                        大きなアップロード1本が埋め尽くし、
                        同じ回線の DNS 問い合わせやビデオ会議の
                        パケットもこの行列の最後尾に並ばされる

「大きなファイルを1つアップロードし始めた瞬間、家中のネットが重くなる」という体感は、帯域が足りないのではなく、この行列のせいであることがほとんどです。

対策は、キューを機械的に短く保つことです。これを担うのが AQM(Active Queue Management、能動的キュー管理)です。

CoDel と FQ-CoDel

CoDel(Controlled Delay)は RFC 8289(2018年1月、Experimental)で規定されています。キューの長さではなく、パケットがキューに滞在した時間(sojourn time)を見るのが特徴です。既定のターゲットは 5ms、インターバルは 100ms で、滞在時間がターゲットを継続的に超えたらパケットを落として送信側に減速を促します。バースト的な一時的キュー(good queue)と、恒常的に滞留する持続的キュー(bad queue)を区別できるのが利点です。

FQ-CoDelは RFC 8290(2018年1月、Experimental)で規定されており、CoDel にフロー単位のキューイングを組み合わせたものです。パケットをハッシュでフローごとのキューに振り分け(既定は1024バケット)、DRR で回します。さらに「新しい(まばらな)キュー」を優先するため、DNS 問い合わせや VoIP のような少量のフローが、大容量ダウンロードの行列に巻き込まれずに済みます。

Linux での qdisc の確認と切り替えは次のようにします。

# 現在の既定 qdisc
sysctl net.core.default_qdisc
 
# インターフェースに実際に付いている qdisc
tc qdisc show dev eth0
 
# fq_codel に切り替える(永続化は /etc/sysctl.d/ に書く)
sudo sysctl -w net.core.default_qdisc=fq_codel

カーネルの net/sched/Kconfig を見ると、ビルド時のデフォルトは pfifo_fast で、「多くのディストリビューションは /proc/sys/net/core/default_qdisc で既定値を設定済み」と書かれています。つまり実機の既定値はディストリビューション依存で、必ず手元で確認すべき項目です。

なお BBR を使う場合は fq を組み合わせるのが定石です。BBR は pacing を前提としており、fq はレートに従った送出をサポートするためです。

ECN と L4S: 落とさずに知らせる

AQM はキューを短く保つためにパケットを落とします。しかし「落とす」以外に輻輳を伝える手段があれば、そのほうが無駄がありません。それが ECN(Explicit Congestion Notification、明示的輻輳通知)です。

RFC 3168「The Addition of Explicit Congestion Notification (ECN) to IP」(2001年9月、Proposed Standard)が定義しています。IP ヘッダの2ビットを使い、ルーターはパケットを落とす代わりに CE(Congestion Experienced)のコードポイントを立てます。TCP 側は、ハンドシェイク時に ECN 対応をネゴシエートし、ヘッダの ECE(ECN-Echo)と CWR(Congestion Window Reduced)の2つのフラグで折り返します。

コードポイント意味
Not-ECTECN 非対応。輻輳時は破棄される
ECT(0) / ECT(1)ECN 対応(ECN-Capable Transport)
CE経路上で輻輳を経験した

古典的な ECN の弱点は、フィードバックの粒度が粗いことです。1 RTT あたり「CE マークを見たか、見なかったか」の1ビットしか返せません。これを改善したのが AccECN(More Accurate ECN Feedback in TCP)で、RFC 9768(Proposed Standard、2026年4月公開、RFC 3168 を更新)として発行されています。CE マークの個数を返せるようになるため、DCTCP や L4S のような、細かい輻輳シグナルを前提とするアルゴリズムが機能します。

Linux では net.ipv4.tcp_ecn で制御します。カーネル文書 Documentation/networking/ip-sysctl.rst によれば、双方が対応する最も高い方式(AccECN、ECN、ECN なし)がネゴシエーションで選ばれ、値の意味は次のとおりです。

値着信接続発信接続
0ECN なしECN なし
1ECNECN
2ECNECN なし
3AccECNAccECN
4AccECNECN
5AccECNECN なし

同文書に記載された既定値は 2、つまり「相手から要求されれば ECN を受け入れるが、こちらからは要求しない」という保守的な設定です。

L4S

L4S(Low Latency, Low Loss, and Scalable throughput)は、ECN の枠組みをさらに進めたアーキテクチャです。RFC 9330(Informational、2023年)がアーキテクチャを、RFC 9331 が ECN プロトコル面を、RFC 9332 が DualQ Coupled AQM を規定しています。

目標値が意欲的で、RFC 9330 はキューイング遅延の平均1ミリ秒未満、99パーセンタイルでおよそ2ミリ秒を掲げています。実現の柱は3つです。

  • ホスト側: スケーラブルな輻輳制御(DCTCP、TCP Prague など)を使う
  • ネットワーク側: 2つのキューを結合した AQM(DualQ Coupled AQM)、あるいはフロー単位のキューイングで、従来型トラフィックと分離する
  • プロトコル: ECT(1) のコードポイントを L4S パケットの識別子として使う

L4S の肝は「既存の Classic な輻輳制御を壊さずに、同じリンク上で共存させながら段階的に導入できる」という設計にあります。

QUIC での輻輳制御

HTTP/3 が使う QUIC では、輻輳制御は TCP と同じ考え方を UDP の上で再実装したものになっています。規定は RFC 9002「QUIC Loss Detection and Congestion Control」(2021年5月、Proposed Standard)です。QUIC 自体の設計は HTTP/3 と QUIC 入門 で扱っています。

RFC 9002 が定めるのは NewReno 相当のコントローラで、スロースタートと輻輳回避を持ちます。TCP との対応と違いを並べると次のようになります。

項目TCP(RFC 5681 ほか)QUIC(RFC 9002)
初期ウィンドウIW10(RFC 6928)10 * max_datagram_size。ただし max(14720, 2 * max_datagram_size) で制限
最小ウィンドウ1セグメント2 * max_datagram_size
損失時の削減ssthresh を FlightSize の半分へkLossReductionFactor = 0.5
パケット順序再送で同じシーケンス番号を再利用パケット番号は単調増加。再送は新しい番号
並べ替え閾値重複 ACK 3つkPacketThreshold = 3
時間ベース判定RACK(RTT の 5/4 相当)kTimeThreshold = 9/8
長期の無応答RTO でスロースタートへ持続的輻輳(PTO の3倍)で最小ウィンドウへ

初期ウィンドウのバイト上限が TCP の 14,600 ではなく 14,720 になっているのは、UDP のオーバーヘッドが 8 バイトで TCP の 20 バイトより小さいぶんを足しているためだと RFC 9002 が明記しています。

QUIC で構造的に有利なのは再送の曖昧さがないことです。TCP では再送したセグメントの ACK が元の送信に対するものか再送に対するものか区別できず、RTT 推定が歪む場合がありました。QUIC はパケット番号が単調増加し再利用されないため、この曖昧さが原理的に発生しません。

なお RFC 9002 のコントローラは「既定として示されたもの」であり、実装は CUBIC や BBR を載せられます。実際、主要な QUIC 実装の多くは CUBIC あるいは BBR を選択可能です。

Linux での確認と切り替え

ここまでの話を、実際のコマンドに落とします。以下は Linux 前提のコマンド例です(手元の macOS では sysctl net.ipv4.* は存在しません)。

現在の設定を見る

# 現在使われている輻輳制御アルゴリズム
sysctl net.ipv4.tcp_congestion_control
 
# 利用可能なアルゴリズム(ロード済みモジュール)
sysctl net.ipv4.tcp_available_congestion_control
 
# 特権なしで設定できるアルゴリズム
sysctl net.ipv4.tcp_allowed_congestion_control
 
# 既定の qdisc と実際の割り当て
sysctl net.core.default_qdisc
tc qdisc show dev eth0

切り替える

# モジュールをロード(未ロードなら available に出てこない)
sudo modprobe tcp_bbr
 
# 一時的に切り替え
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
sudo sysctl -w net.core.default_qdisc=fq
 
# 永続化
cat <<'EOF' | sudo tee /etc/sysctl.d/99-tcp.conf
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
EOF
sudo sysctl --system

net.ipv4.tcp_congestion_control は新規コネクションにのみ適用されます。既存の接続は切り替わらないので、サーバープロセスの再起動あるいは接続の張り直しが必要です。また Documentation/networking/ip-sysctl.rst によれば、受動的な(着信)接続はリスナー側の選択を継承します。

アプリケーション単位で指定する

システム全体を変えずに、ソケット単位で選ぶこともできます。

#include <netinet/tcp.h>
 
const char cc[] = "bbr";
setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, cc, sizeof(cc) - 1);

段階的に試す場合は、この方法で一部のサービスだけに適用するのが安全です。

状態を観測する

# コネクションごとの cwnd、ssthresh、RTT、再送数、使用アルゴリズム
ss -tin
 
# 出力例の見方
#   cubic wscale:7,7 rto:204 rtt:3.5/1.75 mss:1448
#   cwnd:10 ssthresh:7 bytes_sent:... retrans:0/3 ...

ss -tin は輻輳制御のトラブルシュートで最も情報量の多いコマンドです。cwnd が小さいまま張り付いていないか、retrans が増え続けていないか、rtt の変動が大きくないかを見ます。

もう少し踏み込むなら次のあたりです。

# 全体の再送・損失統計
nstat -az | grep -iE "retrans|lost|ecn"
 
# TCP スタックのイベントをトレース
sudo bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb { @[comm] = count(); }'

実務でのチューニング指針とアンチパターン

最後に、実務で判断を誤りやすいポイントを整理します。

まずボトルネックがどこかを切り分ける

輻輳制御の設定をいじる前に、遅さの原因が本当に輻輳制御かを確かめます。

症状疑うべきもの
帯域が出ない。RTT は安定しているウィンドウサイズ不足(BDP に対して受信バッファが小さい)
帯域が出ない。損失が数パーセントある物理層・無線区間の品質。損失ベースの CC の限界
帯域は出るが、負荷時に RTT が数百ms に膨らむbufferbloat。AQM の導入を検討
小さなリクエストのテールレイテンシだけ悪いキューイング、あるいはスロースタート中に完結する短いフロー
スループットが定期的にガクッと落ちる損失イベントごとの cwnd 削減。経路上のポリサーの可能性も

短いリクエストの応答時間は、実は輻輳制御のアルゴリズムよりも初期ウィンドウとハンドシェイクの往復回数に支配されます。数十KB の API レスポンスは輻輳回避フェーズに到達する前に完結するので、CUBIC を BBR に変えても変化はほぼありません。この領域を改善したいなら、CDN とキャッシュ制御 で RTT そのものを縮めるほうが効きます。

やりがちなアンチパターン

1. バッファをとにかく大きくする

net.core.rmem_max や wmem_max を極端に大きくすれば速くなる、という記事は多いですが、必要なのは BDP 相当までです。それを超えると単にメモリを食い、遅延を隠す方向に働きます。Linux の受信バッファは既定で自動チューニングされるので、まず net.ipv4.tcp_moderate_rcvbuf が有効かを確認するのが先です。

2. BBR を「速くなる設定」として全台に適用する

BBR が効くのは、無関係な損失がある経路や、深いバッファのある経路です。データセンター内の低遅延・低損失な経路では CUBIC との差はほとんど出ません。むしろ共有ボトルネックでの他フローとの相互作用というリスクだけが増えます。

3. qdisc を変えずに BBR にする

BBR は pacing を前提としています。fq を設定しないまま BBR を有効にすると、想定と違う挙動になります。BBR に切り替えるなら net.core.default_qdisc=fq をセットで設定してください。

4. 初期ウィンドウを勝手に大きくする

ip route change ... initcwnd 50 のように IW を大きくすると、確かに小さなレスポンスは速く返ります。しかしこれは他のフローから帯域を奪う方向の変更で、IW10 ですら RFC 6928 は Experimental の位置づけです。上げるなら、経路の素性が分かっている閉じたネットワークに限定すべきです。

5. 輻輳制御でレート制限を代替しようとする

輻輳制御はネットワーク容量への適応であり、アプリケーションレベルの流量制御ではありません。「上流のサービスに投げすぎない」ためのものは別のレイヤーの仕事です。こちらは レートリミットのアルゴリズム や、ロードバランシングのアルゴリズム の領域になります。

変更する前に計測する

輻輳制御の変更は、条件によって効果が正負どちらにも振れます。最低限、次を変更の前後で取ってください。

  • スループット(iperf3 などで、実際の経路上で)
  • 負荷時の RTT(ping を流しながら大きな転送をかけ、遅延がどれだけ膨らむか)
  • 再送率(nstat の TcpRetransSegs 系)
  • アプリケーションのレイテンシ分布(p50 ではなく p99 を見る)

負荷時 RTT の膨らみを見ないと、bufferbloat を増やしながら「スループットが上がった」と誤って判断しがちです。

まとめ

  • 輻輳制御は、送信側が cwnd という「パケットに現れない変数」でネットワークの容量を推測する仕組みで、1986年に実際に起きた輻輳崩壊(LBL とバークレー間で 32 Kbps から 40 bps へ)が出発点です
  • RFC 5681 がスロースタート、輻輳回避、高速再送、高速回復の4つを定義し、AIMD のノコギリ波を作ります。初期ウィンドウは RFC 6928 の IW10 が事実上の標準で、Linux の TCP_INIT_CWND も 10 です
  • 重複 ACK 3つによる損失検知は末尾損失と並べ替えに弱く、RFC 8985 の RACK-TLP が時間ベースの判定と探索パケットで補っています
  • 損失ベースの AIMD は高速・長距離経路で不利で、RFC 9438 の CUBIC は3次関数(C=0.4、beta_cubic=0.7)で回復を速め、RTT 間の不公平も減らします。Linux と macOS の既定は CUBIC です
  • BBR は損失ではなく帯域と最小 RTT のモデルで送信レートを決め、pacing を前提とします。BBRv3 は draft-ietf-ccwg-bbr-06(2026年7月、Experimental、WG Document)で規定中で、RFC にはなっていません。2026年9月22日に確認した範囲では、Linux メインラインの tcp_bbr.c は BBRv1 相当のままです
  • bufferbloat は「落ちないのに遅い」を生みます。CoDel(RFC 8289)と FQ-CoDel(RFC 8290)はキューの滞在時間を見て短く保ち、fq_codel はフロー単位のキューで小さなフローを守ります
  • ECN(RFC 3168)は落とさずに輻輳を知らせる仕組みで、AccECN(RFC 9768、2026年4月)が CE マークの個数を返せるように拡張しました。L4S(RFC 9330 / 9331 / 9332)はこれを土台に、ECT(1) と DualQ Coupled AQM で低遅延を狙います
  • QUIC の輻輳制御は RFC 9002 が規定し、NewReno 相当を既定としつつ、パケット番号の単調増加によって再送の曖昧さを排除しています
  • 実務では、まずボトルネックの切り分けを行い、変更するときは負荷時 RTT と p99 レイテンシを含めて前後比較してください。BBR を入れるなら fq とセットで、段階適用とロールバック手段を用意するのが安全です

参考リンク

CPUスケジューラの仕組み - 古典アルゴリズムからCFS、EEVDF、sched_extまで

CPUスケジューラの仕組み - 古典アルゴリズムからCFS、EEVDF、sched_extまで

約46分

CPUスケジューラの仕組みを、スループット・レイテンシ・公平性のトレードオフ、FIFOやMLFQなどの古典アルゴリズム、LinuxのCFSからEEVDFへの移行、sched_ext、スケジューリングクラス、cgroupのcpu.maxによるスロットリング、観測方法まで一次ソース付きで解説します。

ノンブロッキングI/OとI/O多重化 - select/poll/epoll から io_uring までの歩き方

ノンブロッキングI/OとI/O多重化 - select/poll/epoll から io_uring までの歩き方

約47分

C10K問題を出発点に、select/pollの限界、epollが速い理由(赤黒木とレディリスト)、レベルトリガとエッジトリガの違い、kqueueやIOCPとの対比、io_uringのSQ/CQリングとセキュリティ上の扱い、libuv/nginx/Redisの実装までを一次情報と実測で整理します。

SSH の仕組み - 鍵交換・ホスト鍵検証・公開鍵認証を一次ソースで理解する

SSH の仕組み - 鍵交換・ホスト鍵検証・公開鍵認証を一次ソースで理解する

約18分

リモートサーバー運用の土台である SSH プロトコルを、一次ソースの RFC 4251/4252/4253/4254(SSH-2 の Architecture / Authentication / Transport / Connection)を軸に整理します。TCP 接続からバージョン交換、KEXINIT による鍵交換と Diffie-Hellman、ホスト鍵の検証(known_hosts と TOFU の弱点)、公開鍵認証(authorized_keys と署名)、チャネルの多重化とポートフォワーディング(ssh -L / -R / -D)まで、その仕組みと OpenSSH での確認方法をインフラ担当者・開発者向けにまとめます。