Dev Study
← ネットワークの仕組み
レッスン 11 / 13·約 18 分·第2期 ネットワークの仕組み

送りすぎない仕組み — フロー制御と輻輳制御の違い

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択今ここ
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理
  7. DB保存

第21週から、端点どうしの会話の段(トランスポート)を扱っています。ポートで相手を特定し、3回の握手で接続を作り、ACKと再送で失われたデータを取り戻すところまで来ました。最後に残るのが「そもそも、どれだけの速さで送ってよいのか」です。全体像の中では、ルータが並ぶ経路の上を、端から端まで流れる量を決める話にあたります。

まず結論

TCPには「送りすぎない」ための上限が2つあります。相手の受信バッファの空きを示す rwnd と、途中の経路が耐えられる量を送信側が推定した cwnd です。別々の理由で別々に動きます。

この2つは対称ではありません。rwnd は受信側がヘッダで広告してくる値、cwnd は送信側が自分の中だけで持つ内部状態でパケットには現れません。そして実際に送れるのは両方の小さい方、min(cwnd, rwnd) です。

? 読む前に予測

受信側が「空きは64KBあります」と広告し、送信側は輻輳ウィンドウを10KBと見積もっている——このとき、送信側が送ってよいデータ量はどれだけでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

上限が2つある、という状況そのもの

前回まで、失われたデータをどう取り戻すかを見てきました。残っているのは、その手前の問いです。そもそも、どれだけの速さで送ってよいのか。

送りすぎる先が2か所ある、というのがこの話の出発点です。

ひとつは相手です。受信側にはデータを貯めておく受信バッファがあり、アプリケーションがそこから読み出します。読み出しが追いつかなければ溢れます。

もうひとつは途中です。送信側と受信側の間にはいくつものルータとリンクがあり、流れきれない量を送り込めば待ち行列が伸び、やがてパケットが捨てられます。

この2つはまったく別の事情です。相手のPCが重くてバッファを読み出せないことと、経路上のルータが混んでいることの間には何の関係もありません。片方が空いていても、もう片方が詰まっていることがあります。

1本の通信に、別々の理由で上限が2つある
送信側どれだけ送ってよいかを決める
送る
途中の経路ルータ・リンクが捌ける量には限りがある
届く
受信側のバッファアプリが読み出すまで貯まっていく
  • ・受信バッファの空き → これを超えると相手が溢れる
  • ・経路が捌ける量 → これを超えると途中で捨てられる
  • ・2つは無関係。どちらか一方だけ守っても足りない

だから TCP は、上限を2つ持ちます。相手の都合に合わせるのがフロー制御、途中の道の都合に合わせるのが輻輳制御です。名前が似ているうえに、どちらも「送る量を抑える」という同じ結果をもたらすので、しばしば混同されます。しかし抑える理由が違い、抑える量の決め方はもっと違います。

具体実物

規格が定める、実際に送ってよい量

では、上限が2つあるとき、実際に送れるのはどれだけでしょうか。RFC 5681 が明確に規定しています。

RFC 5681 — 送信可能なデータ量の規定(逐語)コマンド出力
a TCP MUST NOT send data with a sequence number higher than
the sum of the highest acknowledged sequence number and
the minimum of cwnd and rwnd

鍵になるのは the minimum of cwnd and rwnd ——最小値です。

✓ ここまでの確認

受信ウィンドウ(rwnd)と輻輳ウィンドウ(cwnd)は、それぞれどこから送信側に伝わりますか。

抽象原理

ここが本題 — 2つは対称ではない

「上限が2つあって、小さい方が効く」と聞くと、対等な2つの値が並んでいる絵を思い浮かべます。その絵が、いちばんの誤解の元です。

この2つは、そもそも情報の出どころが違います。

rwnd と cwnd — 何がどう違うか表
rwnd(受信ウィンドウ) cwnd(輻輳ウィンドウ)
誰が決めるか 受信側 送信側
どう伝わるか TCPヘッダのWindowフィールド 伝わらない
で広告される
パケットに 現れる 現れない
出るか
何を表すか 受信バッファの空き 経路が耐えられる量の推定値
確かさ 相手が実際に測った値 送信側が推測した値

rwnd は、相手から届く値です。 受信側は自分のバッファの空きを知っているので、その値をTCPヘッダのWindowフィールドに書いて送ってきます。送信側は受け取ったパケットを見るだけでよく、推測は要りません。

cwnd は、誰も教えてくれない値です。 従来の TCP では、経路の途中にいるルータが「私はいま混んでいます」と知らせることはなく、送信側は損失や ACK の遅れから推定します。明示的な通知の仕組み(ECN、RFC 3168。ルータが IP ヘッダの ECN 欄に CE を立てる)もありますが、経路と両端が対応している場合に限られます。そしてどちらの場合も、ヘッダに輻輳ウィンドウの値そのものを書く欄はありません。cwnd は送信側のメモリ上にあるだけの変数で、パケットのどこにも現れないのです。

この非対称性から、決定的な違いが生まれます。rwnd は観測できるが、cwnd は推定するしかない。 では何から推定するのでしょうか。送信側が手にできる唯一の手がかりは、ACKの返り方です。送ったデータのACKが順調に返ってくるなら、経路はまだ余裕があるのだろう。ACKが返らない、あるいは重複ACKが届くなら、どこかで捨てられたのだろう——この間接的な証拠だけを頼りに、cwnd を上げ下げし続けます。

具体実物

推定はどこから始めるか — 初期ウィンドウ

接続を張った直後、送信側は経路について何も知りません。ACKがまだ一度も返ってきていないからです。ではいくつから始めるのでしょうか。

RFC 5681 §3.1 は、3ウェイハンドシェイク完了後の cwnd を初期ウィンドウ(IW)として規定しています。規定されているのは上限で、SMSS(送信側の最大セグメントサイズ)によって変わります。

RFC 5681 §3.1 — 初期ウィンドウの規定表
SMSS の大きさ IW セグメント数の上限
SMSS > 2190 2 * SMSS 2セグメント
1095 < SMSS <= 2190 3 * SMSS 3セグメント
SMSS <= 1095 4 * SMSS 4セグメント
 
一般的なイーサネット: SMSS は約1460バイト → 上限3セグメント
SYN または SYN/ACK が失われた場合 → 1セグメント

ここでよく見かける誤りを正しておきます。「スロースタートの初期ウィンドウは1セグメント」という説明です。

これは RFC 2581 以前の古い記述であって、現在の規定ではありません。上の表のとおり、RFC 5681 では SMSS に応じて上限が 2〜4セグメントになります。一般的なイーサネット環境なら上限は 3セグメントです。1セグメントになるのは、SYN または SYN/ACK が失われた場合に限られます。

なお、規定の上限と実装の既定値は別物です。Linux カーネルの既定は 10セグメント(include/net/tcp.h の TCP_INIT_CWND 10)で、これは RFC 5681 の上限ではなく、実験的な RFC 6928 が提案した値です。

抽象原理

「スロースタート」は、遅くない

初期ウィンドウから先、cwnd をどう増やすか。最初に使われるのがスロースタートです。

名前で損をしているアルゴリズムです。「スロー」と付いているので「ゆっくり送る方式」だと思われがちですが、増え方は指数的です。ACKが返ってくるたびに cwnd を増やしていくため、ラウンドトリップごとにウィンドウはおおよそ倍々に膨らみます。

スロースタートでウィンドウが膨らんでいく(SMSS約1460の場合)
ハンドシェイク完了IW = 3セグメント(RFC 5681 の上限の場合)○ 経路について何も知らない状態から出発
1往復後ACKが順調に返る○ ウィンドウはおよそ倍に
2往復後まだ余裕がある○ さらにおよそ倍に
cwnd が ssthresh に届く増やしすぎの手前○ 輻輳回避へ切り替え、増え方が穏やかになる
損失の兆候ACKが返らない・重複ACK× 推定を下方修正する
↓
「スロー」なのは出発点の小ささ。増え方そのものは速い
  • ・ゆっくり増やしているのではなく、小さく始めている
  • ・切り替えの境界は ssthresh。cwnd < ssthresh ならスロースタート、cwnd > ssthresh なら輻輳回避

なぜ小さく始めるのでしょうか。知らないからです。 いきなり大量に送って詰まらせるより、小さく試して反応を見るほうが安全です。反応が良ければ急いで上げる。控えめなのは最初の一歩だけで、探り方はむしろ大胆です。

✓ ここまでの確認

「スロースタート」という名前から受ける印象と、実際の cwnd の増え方の関係として正しいのはどれですか。

⇄ 2つを見比べる
具体例 A

受信ウィンドウ — 相手の都合。教えてもらえる

受信側は自分のバッファの空きを正確に知っています。その値をTCPヘッダのWindowフィールドに書いて送ってくるので、送信側は推測せずに知ることができます。アプリケーションがデータを読み出せば空きが増え、読み出しが滞れば縮み、その変化もそのまま広告されます。

rwnd の性質表
出どころ 受信側が自分のバッファを見て決める
伝わり方 TCPヘッダのWindowフィールドで広告
確かさ 実測値。推測ではない
変わる理由 受信アプリがデータを読み出したか、滞ったか
守らないと 相手のバッファが溢れる
具体例 B

輻輳ウィンドウ — 途中の道の都合。誰も教えてくれない

従来の TCP では、経路上のルータは混雑を通知しません(明示通知の ECN、RFC 3168 もあるが、経路と両端が対応している場合に限られる)。輻輳ウィンドウを書くヘッダの欄もありません。送信側は、ACKが順調に返るか・返らないかという間接的な証拠を頼りに、耐えられる量を推定し続けます。小さく始めて増やし、損失の兆候が出たら下げる、の繰り返しです。

cwnd の性質表
出どころ 送信側が自分で推定して持つ内部状態
伝わり方 伝わらない。パケットに現れない
確かさ 推定値。常に間違っている可能性がある
変わる理由 ACKが返ったか、損失の兆候が出たか
守らないと 経路上で捨てられる

どちらも「これ以上送るな」という上限です。では、この2つの決定的な違いは何でしょうか。そしてその違いは、それぞれの振る舞いをどう変えているでしょうか。

「その値を、送信側はどうやって知るのか」に注目して比べてください。

具体実物

第2期のトランスポート編を振り返る

トランスポートの段で見てきたこと
第21週ポート○ どのアプリ宛かを見分ける
第22週3ウェイハンドシェイク○ 接続を作る。古い重複SYNを排除する
第23週ACKと再送○ 失われたデータを取り戻す
第24週フロー制御と輻輳制御○ そもそも送りすぎない。min(cwnd, rwnd)
↓
届くことの保証は、見分ける・繋ぐ・取り戻す・抑えるの積み重ねでできている
  • ・今回の山場は、rwnd が伝わる値で cwnd が伝わらない値だという非対称性

輻輳制御には、ここで扱った以外にも多くのアルゴリズムがあります。本コースは原理だけを扱うので比較には踏み込みませんが、上限が2つあり、性質が違い、厳しい方が効くという骨格を持っていれば、どの説明を読んでも位置づけが分かります。

この章のまとめ

  • TCPには送りすぎを防ぐ上限が2つある。相手のバッファの空きに合わせるフロー制御(rwnd)と、経路が耐えられる量に合わせる輻輳制御(cwnd)。
  • 実際に送れるのは min(cwnd, rwnd)。RFC 5681 は「確認済みの最大シーケンス番号と、cwnd と rwnd の最小値との和」を超えて送ってはならない(MUST NOT)と規定している。合計でも大きい方でもない。
  • 2つは対称ではない。 rwnd は受信側がWindowフィールドで広告してくる値、cwnd は送信側が自分で維持する内部状態でパケットには現れない。だから rwnd は読むだけでよく、cwnd はACKの返り方から推定し続けるしかない。
  • 初期ウィンドウの上限は SMSS 次第で2〜4セグメント(RFC 5681 §3.1)。一般的なイーサネット(SMSS約1460)では上限3セグメント、SYN/SYN-ACK 喪失時は1セグメント。Linux の既定は10(RFC 6928)。「常に1セグメント」は RFC 2581 以前の古い記述。
  • 「スロースタート」は低速という意味ではない。 増加は指数的で、小さく始めるだけ。
  • スロースタートと輻輳回避の境界は ssthresh。**cwnd と ssthresh が等しいときは「どちらを使ってもよい」**と規定されており、決まっていない。
  • 「相手の都合 vs 途中の道の都合」は入口として有効だが言い切らない。輻輳は受信ホストのNIC/CPUでも起こりうる。本質は場所ではなく「広告される値か、推定するしかない値か」。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 4正解: 0

RFC 5681 が規定する、送信してよいデータ量の上限として正しいのはどれですか。

用語集(7語)
受信ウィンドウ (rwnd)
受信側が「いま受信バッファにこれだけ空きがある」と示す値。TCPヘッダのWindowフィールドに書かれて送信側へ広告される。受信アプリケーションがデータを読み出すと空きが増え、読み出しが滞ると縮む。相手から伝わってくる値である点が cwnd との決定的な違い。
輻輳ウィンドウ (cwnd)
送信側が「経路はこれだけなら耐えられそうだ」と推定して保持する内部状態。送信側のローカル変数であり、TCPヘッダのどのフィールドにも現れない。誰も教えてくれないので、ACKが返ってくる様子や損失の兆候から送信側が自分で上げ下げする。
フロー制御
受信側が処理しきれない量を送りつけないための仕組み。上限は rwnd。相手のバッファという、はっきり観測できる事情に基づく。
輻輳制御
ネットワークが捌ききれない量を送り込まないための仕組み。上限は cwnd。誰も正解を教えてくれないため、送信側が推定し続ける。
スロースタート
接続開始時や損失からの回復時に、小さな初期ウィンドウから cwnd を増やしていくアルゴリズム。名前に反して増加は指数的で、ラウンドトリップごとにおおよそ倍々に膨らむ。「スロー」なのは出発点の小ささを指す。
初期ウィンドウ (IW)
3ウェイハンドシェイク完了後の cwnd の大きさ。RFC 5681 §3.1 では SMSS に応じて上限が2〜4セグメント、一般的なイーサネット(SMSS約1460バイト)では上限3セグメント。SYN/SYN-ACK が失われた場合は1セグメント。Linux の既定は10セグメント(RFC 6928)。
ssthresh(スロースタート閾値)
スロースタートと輻輳回避を切り替える境界値。cwnd が ssthresh 未満ならスロースタート、超えていれば輻輳回避を使う。ちょうど等しいときは、RFC 5681 が「どちらを使ってもよい」としている。