STUDY · NETWORK GUIDE

5-8 輻輳回避 (Congestion Avoidance) — Tail Drop と WRED

キューが溢れてから捨てる Tail Drop と、溢れる前に確率的に捨てる RED/WRED を csr1000v (IOS-XE 17.03.08a) の実機で対比する。同じ輻輳点に Tail Drop と random-detect を切り替えて当て、show policy-map interface の Random drop / Tail drop 列で一括破棄と早期確率破棄の違いを見る。Tail Drop の total drops 248 に対し WRED は Random drop 117 と Tail drop 90 が併存し、WRED の表の Tail drop 列は平均キュー長が最大閾値を超えたときの破棄を数えるためゼロにはならないこと、dscp-based の重み付けで AF13 が AF11 の約 4 倍の破棄率で絞られることを実測する。複数並列 TCP による TCP Global Synchronization の再現は 3 試行とも不成立で、再現しなかった理由の考察までを正直に扱う第 5 章 QoS 編の実機節。

1. 前節の振り返りと本節の内容

前節 5-7 HQoS (階層 QoS) では、1 本の物理 WAN を dot1q サブインタフェースで 2 拠点の独立したサブレート契約帯域に分け、各サブレートの中でさらにクラス優先する階層 QoS を csr1000v の実機で組みました。核はサブレート分離で、拠点 A が契約帯域を超えて大量に drop する裏で、契約内の拠点 B が 1 パケットも落とさずに守られることを実測しました。

5-6 のキューイングと 5-7 の HQoS は、いずれも輻輳したときに どのパケットを先に送り、どれを待たせるか を決める仕組みでした。この 2 つをまとめて輻輳管理 (Congestion Management) と呼びます。輻輳管理が扱うのはキューの出口側、つまりスケジューラの並べ替えです。5-5 の policing と shaping は送出順ではなくレートそのものを天井で縛る別の層で、5-6 で確認したとおり policing は入口方向にも当てられます。

本節はここから軸を変え、キューが溢れる前に、どのパケットをキューに入れずに捨てるか を扱います。この破棄判定を輻輳回避 (Congestion Avoidance) と呼びます。輻輳回避が扱うのは同じキューの入口側で、5-6 や 5-7 のスケジューラとは直交した別の軸です。既定の破棄判定は Tail Drop で、キューが満杯になった後に到着したパケットを無差別に捨てます。これに対し RED (Random Early Detection) と WRED (Weighted Random Early Detection) は、キューが満杯になる前に平均キュー長に応じた確率でランダムに捨てます。

本節では、5-6 と同じ単一の輻輳点に Tail Drop (既定) と random-detect (WRED) を切り替えて当て、show policy-map interface の Random drop / Tail drop 列で一括破棄と早期確率破棄の違いを csr1000v の実機で対比します。あわせて、複数の TCP フローが同時に破棄を受けて揃ってレートを絞る TCP Global Synchronization を、複数並列 TCP フローの実測で試みます。

2. Tail Drop の問題 — 満杯後の一括破棄

Tail Drop は、キューが queue-limit に達した後に到着したパケットを、送信元も DSCP も見ずに一括して破棄する動作です。何も設定しないクラスの既定動作がこれで、「末尾 (tail) に来たものから捨てる」ことが名前の由来です。パケットを捨てる判断に使われるのはキューの残量だけで、どのフローのパケットかは考慮されません。

5-6 で親 shaper を当てて輻輳を作ったときの show policy-map interface に、Tail Drop の姿がそのまま現れています。

snippet
      queue limit 64 packets
      (queue depth/total drops/no-buffer drops) 64/296/0

queue depth が queue limit と同じ 64 で張り付き、その裏で total drops が 296 まで積み上がっています。キューは満杯のまま維持され、入りきらなかったパケットが次々と捨てられている状態です。この動作には 2 つの害があります。

1 つ目の害は 遅延の高止まり です。キューが満杯のまま維持されるということは、キューに入れたパケットが送り出されるまでの待ち時間が常に最大になるということです。本節の輻輳点である shape average 500000 は、1500 バイトのパケットに換算すると毎秒 41.7 パケットしか送り出せません。queue limit 64 packets はこのレートで 1.536 秒分のバッファに相当します。キューが満杯に張り付くと、後から入ったパケットは 1.5 秒あまり待たされてから送り出されます。

2 つ目の害は TCP Global Synchronization です。TCP はパケットの損失を輻輳の信号として扱い、損失を検知すると輻輳ウィンドウ (cwnd) を絞って送出レートを下げます。Tail Drop はキューが満杯になった瞬間に、そこへ到着した複数フローのパケットをまとめて捨てるため、複数の TCP フローが同じ瞬間に損失を検知します。その結果、各フローが揃ってレートを絞り、輻輳が解けると揃って回復し、また揃って満杯に達する、という周期を繰り返します。この現象を TCP Global Synchronization と呼びます。

TCP Global Synchronization の実害は 2 つあります。フローが揃ってレートを絞る谷の区間では、リンクに空きができて帯域が無駄になります。谷から回復した後は再びキューが満杯まで埋まるため、バッファ遅延は高いまま推移します。帯域を使い切れず、しかも遅延も下がらない、という状態です。

輻輳回避は、この 2 つの害への処方です。キューが満杯になる前に、少しだけ・ランダムに パケットを捨てて、送信側に早めにブレーキを踏ませます。早く捨てることでキューが満杯まで行かなくなり、ランダムに捨てることで落ちるフローがばらけます。

3. 検証ラボの構成と前提の確認

本節の検証は、5-6 と同じ csr1000v の L3 2 セグメント構成をそのまま流用します。PC1 が複数の並列 TCP フローを送出し、R1 (csr1000v) の出口 Gi3 に親 shaper を置いて輻輳点を作り、そのキューの入口に当てる破棄判定を Tail Drop と WRED で切り替えます。

検証ラボ。PC1 が 4 本の並列 TCP フロー (AF11) を送出し、R1(csr1000v) の出口 Gi3 に親 shape 500k を置いて唯一の輻輳点を作る。5-6/5-7 が出口側のスケジューラ (送出順) を扱ったのに対し、本節はそのキュー入口の破棄判定を、既定の Tail Drop と random-detect (WRED) で切り替える。効果は show policy-map interface の Random drop / Tail drop 列で対比する。

5-6 からの実質的な差分は、送出するトラフィックが UDP の固定レートから 複数並列の TCP フロー に変わる点です。WRED が意味を持つのは、パケットを落とされたときに送出レートを下げる応答性のあるトラフィック、つまり TCP のようなプロトコルに限られます。Cisco のドキュメントも WRED を「トラフィックの大半が TCP/IP である場合にのみ有用」と位置づけています。UDP のように損失に反応しないトラフィックに WRED を当てても、早く捨てた分だけ無駄に失われるだけです。

まず使用した機器と IOS-XE を確認します。

snippet
R1# show version | include Cisco IOS-XE Software|CSR|Version
Cisco IOS XE Software, Version 17.03.08a
Cisco IOS Software [Amsterdam], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 17.3.8a, RELEASE SOFTWARE (fc3)
(GPL ライセンス表記の 3 行を省略)
cisco CSR1000V (VXE) processor (revision VXE) with 1104920K/3075K bytes of memory.

機種は csr1000v、ソフトウェアは 5-6 と 5-7 で使ったものと同じ IOS-XE 17.03.08a です。この csr1000v はデモ用にスループットの上限が絞られています。

snippet
R1# show platform hardware throughput level
The current throughput level is 1000 kb/s

送出側の PC1 には iperf が入っています。Cisco Modeling Labs の公開ドキュメントは Alpine ノードに iperf3 が同梱されると記していますが、本検証で使ったイメージに iperf3 は入っておらず、classic 系の iperf 2.2.0 だけが存在しました。本節の測定はすべてこの iperf 2.2.0 で行っています。

snippet
PC1 $ which iperf >/tmp/w_iperf 2>&1; echo IPERF_RC=$?; cat /tmp/w_iperf 2>/dev/null; which iperf3 >/tmp/w_iperf3 2>&1; echo IPERF3_RC=$?; cat /tmp/w_iperf3 2>/dev/null; echo ---VERSION_IPERF---; iperf --version 2>&1; echo ---VERSION_IPERF3---; iperf3 --version 2>&1
IPERF_RC=0
/usr/bin/iperf
IPERF3_RC=1
---VERSION_IPERF---
iperf version 2.2.0 (10 April 2024) pthreads
---VERSION_IPERF3---
-sh: iperf3: not found

PC1 から PC2 まで、R1 のルーティングを経由して L3 で到達できることを確認します。

snippet
PC1 $ ping -c 3 -W 2 192.168.20.20
PING 192.168.20.20 (192.168.20.20): 56 data bytes
64 bytes from 192.168.20.20: seq=1 ttl=42 time=0.920 ms
64 bytes from 192.168.20.20: seq=2 ttl=42 time=1.132 ms

--- 192.168.20.20 ping statistics ---
3 packets transmitted, 2 packets received, 33% packet loss
round-trip min/avg/max = 0.920/1.026/1.132 ms

RTT は 1 ミリ秒前後です。初回の 1 パケットは ARP 解決で落ちますが、疎通は取れています。

次に、この輻輳点で TCP が実際に溢れることを確かめます。ここは 5-6 や 5-7 より慎重な確認が必要な箇所です。UDP は指定したレートで送り続けるため「送出レート > shape レート」にすれば必ず溢れますが、TCP は輻輳を検知すると自分でレートを絞る ため、同じ理屈が通りません。並列数と queue-limit を組み合わせて総需要を上げ、輻輳が成立していることを実機のカウンタで確認してから先へ進みます。

QoS を何も当てていない状態で、AF11 (ToS 0x28) でマークした TCP を 4 本並列で 8 秒間送出したときの送出量は次のとおりです。

snippet
PC1 $ iperf -c 192.168.20.20 -p 5201 -P 4 -t 8 -i 1 -S 0x28
(接続情報と 1 秒刻みの区間行を省略)
[  3] 0.00-16.15 sec  1.09 MBytes   564 Kbits/sec
[  4] 0.00-16.15 sec   669 KBytes   339 Kbits/sec
[  1] 8.00-16.15 sec  63.6 KBytes  64.0 Kbits/sec
[  1] 0.00-16.15 sec   858 KBytes   435 Kbits/sec
[  2] 0.00-16.15 sec   848 KBytes   430 Kbits/sec
[SUM] 0.00-8.08 sec  3.41 MBytes  3.54 Mbits/sec

-t 8 で 8 秒の送出を指示したにもかかわらず、各ストリームの完了時刻は 16.15 秒まで伸びています。送りたい量に対して経路が追いついていない状態です。

この需要に対し、Gi3 の class-default に shape average 500000 だけを当ててボトルネックを作り、同じ AF11 マークの TCP 4 並列を流したときのカウンタが次のものです。並列数とマークは直前の baseline と同じですが、送出時間は輻輳を積み上げるために -t 8 から -t 15 に伸ばしています。

snippet
R1# show policy-map interface GigabitEthernet3
 GigabitEthernet3

  Service-policy output: PM-PARENT-TAILDROP

    Class-map: class-default (match-any)
      864 packets, 1263800 bytes
      5 minute offered rate 34000 bps, drop rate 4000 bps
      Match: any
      Queueing
      queue limit 64 packets
      (queue depth/total drops/no-buffer drops) 65/86/0
      (pkts output/bytes output) 778/1147980
      shape (average) cir 500000, bc 2000, be 2000
      target shape rate 500000

queue-limit を明示していないため、IOS-XE が親 shaper の帯域から導いた既定値 queue limit 64 packets が表示されています。queue depth が 65 に達し、total drops が 86 まで積み上がっている ことが、TCP でも輻輳が成立した証拠です。自分でレートを絞る TCP であっても、4 本並列という需要増であれば親 shape 500 kbps を溢れさせられることが実機で確認できました。

なお、この queue depth の 65 は queue limit の 64 を 1 つ超えています。この値は show を打った瞬間のスナップショットで、上限判定を受けている最中のパケットまで含めて数えられている可能性があります。内部の数え方は本ラボの出力からは確定できないため、深さの絶対値ではなく、上限付近で張り付いているという事実のほうを読み取ります。

輻輳回避が判定するのは、パケットを出力キューに入れるか、その手前で捨てるかです。したがって適用点は 5-6 や 5-7 と同じく出口方向 (service-policy output) になります。本節の MQC 構成でも、random-detect はすべて Gi3 の output に当てています。

トラフィックの分類には、5-4 で扱った match dscp を使います。本節は同じ AF クラスの中の破棄優先度の違いを扱うため、AF11 と AF13 を 1 つにまとめた CM-BULK を新たに定義します。CM-VOICE は、優先キューへの WRED 併用可否を確認する §8 で使います。

snippet
class-map match-any CM-BULK
 match dscp af11
 match dscp af13
class-map match-any CM-VOICE
 match dscp ef

投入後の定義は次のとおりです。DSCP 名の後ろに 10 進値が併記されます。

snippet
R1# show class-map
 Class Map match-any class-default (id 0)
   Match any

 Class Map match-any CM-BULK (id 1)
   Match   dscp af11 (10)
   Match   dscp af13 (14)

 Class Map match-any CM-VOICE (id 2)
   Match   dscp ef (46)

送出側は、この DSCP に対応する ToS バイトでマークして送出します。DSCP 値を 4 倍 (2 ビット左シフト) したものが、ECN が 0 のときの ToS バイトです。AF11 (10) は ToS 0x28、AF13 (14) は ToS 0x38、EF (46) は ToS 0xb8 になります。

輻輳点の物理量も先に押さえておきます。親 shaper の shape average 500000 は 1500 バイトのパケットで毎秒 41.7 パケット、queue-limit 64 packets はその 1.536 秒分です。一方、CML 内の PC1 と PC2 の間の RTT は数ミリ秒で、帯域遅延積 (BDP: Bandwidth-Delay Product) は 500 kbps × 2 ms = 125 バイトと、1 パケットにも満たない値です。このラボのキューは BDP に対して極端に深い 状態にあり、この非対称は §7 の考察で再び出てきます。

4. Tail Drop の実証 — 満杯後に無差別に捨てる

まず既定の Tail Drop を実証します。親 shaper の class-default に shape average 500000queue-limit 64 packets だけを置き、子ポリシーも random-detect も付けません。この状態の class-default は単一の FIFO キューで、破棄判定は既定の Tail Drop です。

snippet
policy-map PM-PARENT-TAILDROP
 class class-default
  shape average 500000
  queue-limit 64 packets
!
interface GigabitEthernet3
 service-policy output PM-PARENT-TAILDROP

この構成に、AF11 でマークした TCP を 4 本並列で 60 秒間送り込みます。この送出条件は、あとで WRED を当てるときにも一字一句同じものを使います。条件を完全に揃えておくことで、drop の内訳の差がポリシーの差であると言い切れます。

snippet
PC1 $ iperf -c 192.168.20.20 -p 5201 -P 4 -t 60 -i 1 -S 0x28
(接続情報と 1 秒刻みの区間行を省略)
[  1] 0.00-66.50 sec   874 KBytes   108 Kbits/sec
[  3] 0.00-69.03 sec   966 KBytes   115 Kbits/sec
[  2] 0.00-70.34 sec   775 KBytes  90.3 Kbits/sec
[  4] 0.00-70.53 sec  1.47 MBytes   174 Kbits/sec
[SUM] 0.00-60.25 sec  4.02 MBytes   560 Kbits/sec

このときの Gi3 のカウンタが次のものです。

snippet
R1# show policy-map interface GigabitEthernet3
 GigabitEthernet3

  Service-policy output: PM-PARENT-TAILDROP

    Class-map: class-default (match-any)
      3002 packets, 4496344 bytes
      5 minute offered rate 112000 bps, drop rate 12000 bps
      Match: any
      Queueing
      queue limit 64 packets
      (queue depth/total drops/no-buffer drops) 61/248/0
      (pkts output/bytes output) 2754/4139536
      shape (average) cir 500000, bc 2000, be 2000
      target shape rate 500000

この出力で確認する点は 3 つです。1 つ目は queue depth で、取得した時点の値が 61 と queue limit 64 packets の上限直下にあります。バッファがほぼ埋まった状態です。2 つ目は total drops が 248 まで積み上がっていることで、これが輻輳成立の証拠になります。到着した 3002 パケットのうち 2754 パケットが送出され、248 パケットが落ちました。3 つ目は WRED の表 (Random drop 列) が出力に現れないこと です。random-detect を当てていないこの構成には Random drop という概念自体が存在せず、落ちた 248 パケットはすべて満杯後の tail drop であることを意味します。

5. WRED — 満杯前にランダムに捨てる

破棄するかどうかの判定が行われるのは、パケットを出力キューへ入れる直前です。入口 (ingress) で受け取った時点ではなく、出力インタフェースのキューに入れるかどうかを決めるその瞬間に、到着した 1 個ごとに独立して 判定します。Tail Drop も判定の位置は同じで、違うのは見ている値です。Tail Drop が見るのはキューが満杯かどうかだけで、WRED が見るのはこれから述べる平均キュー長です。

RED は、キューが満杯になる前に、平均キュー長に応じた確率でパケットを破棄する仕組みです。判定に使う値は 3 つあります。最小閾値 (minimum threshold)、最大閾値 (maximum threshold)、そして破棄確率の分母 (mark probability denominator) です。

平均キュー長が最小閾値を下回っている間は、パケットを 1 つも捨てません。平均キュー長が最小閾値と最大閾値の間にある区間では、平均キュー長が最大閾値に近づくほど高い確率でランダムに破棄します。破棄確率は最大閾値の時点で「分母ぶんの 1」に達し、既定の分母 10 であれば 10 パケットに 1 つの割合です。平均キュー長が最大閾値を超えると、到着したパケットをすべて破棄します。

この 3 つの区間は、show policy-map interface の WRED の表に並ぶ列にそのまま対応します。最小閾値と最大閾値の間で確率的に捨てた分が Random drop 列、最大閾値を超えて全破棄した分が Tail drop 列です。WRED の表の Tail drop 列は「平均キュー長が最大閾値を超えたときの破棄」を数える列であり、実キューが queue-limit まで物理的に埋まったことだけを表す列ではありません

ここで判定に使われるのが 瞬時のキュー長ではなく平均キュー長 である点は重要です。平均キュー長は指数加重移動平均で、exponential-weighting-constant (既定 9 = 1/512) が追従の速さを決めます。この定数を小さくすると平均が実キュー長に敏感になり、瞬間的なバーストで破棄が始まりやすくなります。大きくすると鈍感になり、実際の輻輳の検知が遅れます。実キューが空いているのに破棄される、あるいは満杯なのに破棄されない、という一見矛盾した挙動は、判定が平均値で行われることに由来します。

確率で捨てる区間に入ったとき、どのパケットが捨てられるかに理由はありません。到着した 1 個ごとに独立して判定するため、同じ状況をもう一度作れば捨てられるのは別のパケットになります。この どれが当たるか事前に決まらない という性質そのものが、後述する TCP Global Synchronization の回避につながります。満杯の瞬間に届いていたものが確定的にまとめて落ちる Tail Drop とは、ここが決定的に違います。WRED の効き目は「早く捨てる」ことより「捨てる相手を確定させない」ことにある、と捉えると動作が理解しやすくなります。

WRED は、この RED に DSCP や IP Precedence の重み付けを足したものです。印ごとに別々の閾値を与えることで、低優先の印を先に捨てられます。まず重み付けを付けない最小構成を投入し、csr1000v が持つ WRED のサブコマンドを実機のヘルプで確認します。

snippet
R1(config-pmap-c)#random-detect ?
  discard-class                   parameters for each discard-class value
  discard-class-based             Enable discard-class-based WRED as drop policy
  dscp                            parameters for each dscp value
  dscp-based                      Enable dscp-based WRED as drop policy
  ecn                             explicit congestion notification
  exponential-weighting-constant  weight for mean queue depth calculation
  precedence                      parameters for each precedence value
  precedence-based                Enable precedence-based WRED as drop policy
  <cr>                            <cr>

末尾の <cr> が示すとおり、random-detect は引数なしでも受理されます。引数なしで投入したポリシーを表示すると、precedence 0 から 7 までの 8 行を持つ WRED の表が現れます。

snippet
R1# show policy-map PM-TEST-RD2
  Policy Map PM-TEST-RD2
    Class class-default
       wred, exponential weight 9

      class    min-threshold    max-threshold    mark-probablity
      ----------------------------------------------------------
      0             -                -                1/10
      1             -                -                1/10
      2             -                -                1/10
      3             -                -                1/10
      4             -                -                1/10
      5             -                -                1/10
      6             -                -                1/10
      7             -                -                1/10

random-detect と入力した設定が、表示上は wred, exponential weight 9 という別の文言に変わっている点に注意が必要です。投入したコマンド名がそのまま show 出力に現れるとは限りません。この時点の min-threshold と max-threshold は - で、値が決まっていません。閾値の既定値はインタフェースに適用されたときに帯域から算出されるため、実値は show policy-map interface で確認します。

WRED を当てる位置には制約があります。階層型ポリシーでは、WRED は子ポリシーにだけ当てられます。親ポリシーは shaping を担当し、その配下の子ポリシーのクラスに random-detect を置きます。5-6 と 5-7 で組み立てた「親 shaper + 子ポリシー」の階層が、そのまま WRED の置き場所になります。

snippet
policy-map PM-CHILD-WRED
 class class-default
  random-detect
policy-map PM-PARENT-WRED
 class class-default
  shape average 500000
   service-policy PM-CHILD-WRED
!
interface GigabitEthernet3
 service-policy output PM-PARENT-WRED

閾値を書かずに random-detect だけを置くと、precedence-based の既定閾値が使われます。この構成に、§4 とまったく同じ送出条件 (AF11 の TCP 4 並列・60 秒) を流した結果が次の出力です。

snippet
R1# show policy-map interface GigabitEthernet3
 GigabitEthernet3

  Service-policy output: PM-PARENT-WRED

    Class-map: class-default (match-any)
      3050 packets, 4571846 bytes
      5 minute offered rate 100000 bps, drop rate 10000 bps
      Match: any
      Queueing
      queue limit 64 packets
      (queue depth/total drops/no-buffer drops) 0/274/0
      (pkts output/bytes output) 2776/4161292
      shape (average) cir 500000, bc 2000, be 2000
      target shape rate 500000

      Service-policy : PM-CHILD-WRED

        Class-map: class-default (match-any)
          3050 packets, 4571846 bytes
          5 minute offered rate 99000 bps, drop rate 11000 bps
          Match: any

          queue limit 64 packets
          (queue depth/total drops/no-buffer drops) 6/274/0
          (pkts output/bytes output) 2776/4161292
            Exp-weight-constant: 9 (1/512)
            Mean queue depth: 29 packets
            class       Transmitted         Random drop      Tail drop          Minimum        Maximum     Mark
                    pkts/bytes            pkts/bytes       pkts/bytes          thresh         thresh     prob

            0               7/606             1/96             1/86                16            32  1/10
            1            2769/4160686       117/177138        90/136260            18            32  1/10
            2               0/0               0/0              0/0                 20            32  1/10
            3               0/0               0/0              0/0                 22            32  1/10
            4               0/0               0/0              0/0                 24            32  1/10
            5               0/0               0/0              0/0                 26            32  1/10
            6               0/0               0/0              0/0                 28            32  1/10
            7               0/0               0/0              0/0                 30            32  1/10

この出力が本節の中心です。Tail Drop 構成では (queue depth/total drops/no-buffer drops) の 1 行しか出ませんでしたが、WRED を当てたクラスには Random drop 列と Tail drop 列が並ぶ表 が現れます。同じ輻輳に対して、最大閾値を超えて一括で捨てた数と、最大閾値に達する前に確率的に捨てた数を、1 つの表の中で対比できます。

読む順序は 3 つです。1 つ目は既定閾値の実値で、precedence 0 が 16 / 32、precedence 1 が 18 / 32 と、precedence が上がるほど最小閾値が高い階段になっています。いずれも queue limit 64 packets の内側に収まる値です。閾値を bytes で表示するプラットフォームもあるため、単位は実機の出力で確かめます。2 つ目は Exp-weight-constant: 9 (1/512)Mean queue depth: 29 packets です。この 29 パケットは show を打った時点の平均キュー長で、precedence 1 の最小閾値 18 と最大閾値 32 の間に収まっています。取得した瞬間は確率破棄が働く区間にあった、という意味です。3 つ目が AF11 のトラフィックが乗る precedence 1 の行で、Transmitted 2769 パケットに対し Random drop 117・Tail drop 90 が計上されています。

この行に、教科書どおりにいかない事実が 2 つ現れています。 1 つは、Random drop カウンタが実際に動いたことです。仮想ルータのデータプレーンが WRED の確率破棄を実装しているかは設定の受理とは別の問題ですが、この csr1000v は実際に早期破棄を実行していました。もう 1 つは、WRED を入れても Tail drop がゼロにならなかった ことです。Random drop 117 と Tail drop 90 が併存しています。

この Tail drop 90 の読み方には注意が要ります。WRED の表の Tail drop 列が数えているのは、平均キュー長が最大閾値を超えていた区間の破棄です。この構成の precedence 1 の最大閾値は 32 パケットで、平均キュー長がここを超えると WRED は到着したパケットをすべて破棄し、その分が確率破棄と区別されて Tail drop 列に積まれます。Tail drop 90 は、実キューが queue-limit 64 packets まで物理的に満杯になった回数を示す数ではありません。閾値 32 の側で全破棄に切り替わった分がここに現れています。

つまり WRED は、最小閾値から最大閾値までの区間を確率破棄に置き換える仕組みであり、輻輳が最大閾値の先まで進めば、そこから先の破棄は WRED の内側でも無差別になります。「WRED を入れれば Tail Drop が消える」のではなく、「破棄の一部が、最大閾値に達する前の確率的な早期破棄に置き換わる」 が実機の正確な姿です。

Tail Drop 構成との対比では、捨てた総量に大きな差は出ていません。total drops は 248 に対し 274 です。送信側 iperf の [SUM] 行も 560 Kbits/sec に対し 537 Kbits/sec で同程度です。

この 2 つの [SUM] 行が親 shape の 500 kbps を上回って見える点には説明が要ります。iperf 2 の [SUM] 行は、各ストリームの実際の送出時間より短い約 60 秒の窓で総量を割った値だからです。実際には各ストリームの完了は 64 秒から 71 秒まで伸びており、転送量の合計を最も遅く終わったストリームの完了時刻で割り直すと、Tail Drop 構成が約 479 kbps、WRED 構成が約 478 kbps と、どちらも shape レートの内側に収まります。

キューの深さは、Tail Drop で取得した queue depth が 61、WRED では親側が 0・子側が 6 でした。破棄判定に使われる平均キュー長も、取得時点で 29 パケットと queue limit 64 packets の半分以下です。

ただし、この 61 と 6 はどちらも iperf の送出が完了したあとに show を 1 回打って取得した値です。 輻輳の最中の深さではなく、送出が止まってキューが掃けていく過程のスナップショットにあたります。Mean queue depth は指数加重移動平均ですが、示しているのは取得時点の WRED 判定用の平均であって、測定期間全体の統計平均ではありません。1 回の取得だけで「測定期間を通してキューが浅かった」と断定することはできません。実際、同じ表に Tail drop が 90 計上されている以上、show を打たなかった時間帯には平均キュー長が最大閾値 32 を超えた区間があったことになります。

この 61 と 6 の差を、ポリシーの差に帰属させることもできません。 本節では、shape average 500000queue-limit 64 packets というこことまったく同じ Tail Drop 構成で別の測定も行っており、そのときの送出完了後の queue depth は 0 でした。同じ構成でも、取得のタイミング次第で 61 にも 0 にもなるということです。キューの深さの差を根拠にしたい場合は、送出中に show policy-map interface を複数回取得して時系列で並べる必要があります。

この 1 点の取得から言えるのは、捨てた総量がほぼ同じであった ことまでです。WRED がキューを浅く保つという効能は、閾値が queue-limit の内側に置かれるという仕組みから導かれるものであって、本節の 1 回のスナップショットで実測できたものではありません。

なお、親 class-default の total drops 274 と、WRED の表に並ぶ破棄数の合計 (117 + 90 + 1 + 1 = 209) は一致しません。本節では破棄の内訳の比較を WRED の表の中だけで行い、親のカウンタは輻輳量の目安として扱います。

閾値は明示することもできます。queue-limit 64 packets の内側に、最小閾値 8・最大閾値 32・破棄確率の分母 10 を置きます。

snippet
policy-map PM-CHILD-WRED
 class class-default
  queue-limit 64 packets
  random-detect
  random-detect precedence 1 8 32 10

AF11 は DSCP 10 で、そのビット列 001010 の上位 3 ビットが IP Precedence にあたるため、precedence 1 として扱われます。precedence-based ではこの precedence 1 の閾値を明示する形になります。

snippet
R1# show policy-map interface GigabitEthernet3
(親 shaper の表示と precedence 2〜7 の 0/0 行は省略)
            Exp-weight-constant: 9 (1/512)
            Mean queue depth: 26 packets
            class       Transmitted         Random drop      Tail drop          Minimum        Maximum     Mark
                    pkts/bytes            pkts/bytes       pkts/bytes          thresh         thresh     prob

            0               8/666             1/96             1/86                16            32  1/10
            1            5531/8315142       264/399696       117/177138             8            32  1/10

precedence 1 の Minimum thresh が 18 から 8 に変わり、設定した閾値が反映されています。ただしこの表の数値は、前の測定から引き継がれた累積値です。 本節ではポリシーをインタフェースから外さずに閾値だけを追加投入したため、カウンタがゼロに戻っていません。差分を取ると Transmitted は 2762、Random drop は 147、Tail drop は 27 の増加です。最小閾値を下げた効果だけを厳密に切り分けたい場合は、no service-policy で一度外してから当て直し、カウンタをゼロから計測する必要があります。

2 つの破棄の起き方の違いを、下図で対比します。

破棄の判定は、パケットを出力キューへ入れる直前 (enqueue 時) に 1 個ずつ独立して行われる。上レーンの Tail Drop が見るのはキューが満杯かどうかだけで、満杯になった後に届いた分をまとめて捨てる。下レーンの WRED が見るのは平均キュー長で、図の下部にある 3 つのゾーン (min 未満は全通し / min と max の間は確率破棄 / max 超は全破棄) のどこに平均があるかで動作が変わる。確率破棄でどれが当たるかは事前に決まらない。

図の上下で注目する点は、通過した本数ではなくキューの深さです。Tail Drop はキューを満杯まで使い切ってから捨てるため、キューは満杯に張り付き、そこに並んだパケットの待ち時間は最大になります。WRED は満杯になる前に捨て始めるため、キューは浅いまま保たれ、待ち時間も短くなります。WRED の効能は同期の回避だけではなく、キューを浅く保つことによる遅延の削減 でもあります。

6. WRED の重み付け — AF11 と AF13 で閾値を変える

WRED の W は Weighted、つまり重み付けを指します。同じクラスに入るトラフィックであっても、DSCP の印ごとに別々の閾値を与えることで、低優先の印から先に捨てられます。5-2 と 5-4 で扱った AF (Assured Forwarding) の「後ろ 1 桁 = 破棄優先度」が、ここで初めて実際の破棄動作として効きます。

ここで precedence-based の限界が明らかになります。AF11 は DSCP 10 (001010)、AF13 は DSCP 14 (001110) で、上位 3 ビットはどちらも 001 です。AF11 と AF13 は IP Precedence では区別できません。破棄優先度の違いを閾値に反映するには、precedence-based ではなく dscp-based に切り替える必要があります。切り替えは random-detect dscp-based の宣言で行い、以降の閾値は dscp 単位で与えます。本節が実機で確認したのは、dscp-based を宣言したクラスに dscp 単位の閾値を与える構成までです。1 つのクラスや 1 つのポリシーの中で precedence 単位と dscp 単位の閾値を混在させたときに実機がどう応答するかは、本節では確認していません。

random-detect dscp-based を宣言したうえで、AF11 には高い閾値 (最小 24 / 最大 40)、AF13 には低い閾値 (最小 8 / 最大 24) を与えます。閾値が低い AF13 のほうが早い段階で破棄の対象になります。

snippet
policy-map PM-CHILD-WRED-DSCP
 class CM-BULK
  bandwidth 300
  random-detect dscp-based
  random-detect dscp af11 24 40 10
  random-detect dscp af13 8 24 10
 class class-default
  fair-queue
policy-map PM-PARENT-WRED-DSCP
 class class-default
  shape average 500000
   service-policy PM-CHILD-WRED-DSCP

投入後の running-config を見ると、DSCP 名が 10 進値に置き換わって表示されます。設定時は af11 / af13 と入力しますが、表示は dscp 10 / dscp 14 になります。

snippet
R1# show run policy-map PM-CHILD-WRED-DSCP
policy-map PM-CHILD-WRED-DSCP
 class CM-BULK
  bandwidth 300
  random-detect dscp-based
  random-detect dscp 10 24 40 10
  random-detect dscp 14 8 24 10
 class class-default
  fair-queue

この構成に、AF11 でマークした TCP 2 本と AF13 でマークした TCP 2 本を、同じ 60 秒の窓で並行して送り込みます。同一の輻輳ウィンドウで競わせることが、重み付けの対比を成立させる条件です。送信側 iperf の合計は次のとおりです。

snippet
PC1 $ iperf -c 192.168.20.20 -p 5211 -P 2 -t 60 -i 1 -S 0x28   (AF11)
[SUM] 0.00-60.22 sec  3.76 MBytes   524 Kbits/sec

PC1 $ iperf -c 192.168.20.20 -p 5212 -P 2 -t 60 -i 1 -S 0x38   (AF13)
[SUM] 0.00-60.06 sec   406 KBytes  55.4 Kbits/sec

AF13 のスループットは AF11 の 1 割ほどしかありません。なお、この 2 つの [SUM] 行は別々の iperf 実行から出たもので、単純に足すと 579 Kbits/sec と親 shape の 500 kbps を超えて見えます。§5 で述べた集計窓の問題と同じく、[SUM] の約 60 秒の窓が各ストリームの実測時間 (64 秒から 118 秒) より短いためです。per-stream の行を足すと約 502 Kbits/sec となり、shape レートとほぼ一致します。ルータ側のカウンタを見ます。

snippet
R1# show policy-map interface GigabitEthernet3
(親 shaper と class-default の表示は省略)
      Service-policy : PM-CHILD-WRED-DSCP

        Class-map: CM-BULK (match-any)
          3303 packets, 4964890 bytes
          5 minute offered rate 104000 bps, drop rate 8000 bps
          Match:  dscp af11 (10)
          Match:  dscp af13 (14)
          Queueing
          queue limit 64 packets
          (queue depth/total drops/no-buffer drops) 0/270/0
          (pkts output/bytes output) 3033/4561878
          bandwidth 300 kbps
            Exp-weight-constant: 9 (1/512)
            Mean queue depth: 0 packets
            dscp       Transmitted         Random drop      Tail drop          Minimum        Maximum     Mark
                    pkts/bytes            pkts/bytes       pkts/bytes          thresh         thresh     prob

            af11        2734/4126400        58/87812         60/90840             24            40  1/10
            af13         299/435478          6/9084          54/81756              8            24  1/10

dscp 行が af11 と af13 に分かれ、Minimum thresh と Maximum thresh が設定した 24 / 40 と 8 / 24 で表示されています。重み付けの定義そのものは、設定どおり実機に入っています。なお、この show は送出が完了したあとに取得したものです。Mean queue depth が 0 と出ているのは指数加重移動平均が輻輳解消後の値になっているためで、破棄が起きていた時点の平均キュー長ではありません。

ここで Random drop の raw 件数だけを見ると、結論を取り違えます。 早く捨てられるはずの af13 の Random drop は 6 で、af11 の 58 より少ないためです。数を正しく読むには、分母を揃える必要があります。af13 は最小閾値 8 という低い値のため極めて早い段階から破棄の対象になり、その損失を検知した TCP が送出量そのものを大きく絞りました。結果として af13 の Transmitted は 299 パケットで、af11 の 2734 パケットの約 9 分の 1 しかありません。送信側 iperf の 524 Kbits/sec 対 55.4 Kbits/sec という差も、同じことを送信側から見た数字です。

破棄の内訳にも差が出ています。af13 が落とした 60 パケットのうち 54 パケットは Tail drop 列に入っています。§5 で確認したとおり、この列が数えるのは平均キュー長が最大閾値を超えていた区間の破棄です。af13 の最大閾値は 24 パケットと低いため、平均キュー長がここを超えている間、af13 は確率破棄ではなく全破棄の側に入っていたことになります。一方、最大閾値 40 の af11 は、落とした 118 パケットの内訳が Random drop 58・Tail drop 60 とほぼ半々です。低い最小閾値で早く捨て始めるだけでなく、低い最大閾値のせいで全破棄に切り替わる点も早い というのが、af13 の絞られ方です。

そこで、到着を試みた総数 (Transmitted + Random drop + Tail drop) を分母にした破棄率で比較します。af11 は (58 + 60) ÷ 2852 で約 4.1 %、af13 は (6 + 54) ÷ 359 で約 16.7 % です。af13 は af11 の約 4 倍の破棄率で絞られています。5-2 と 5-4 で扱った「AF の後ろ 1 桁 = 破棄優先度」は、破棄の絶対数ではなく、早い段階から高い比率で捨てられ、その結果として TCP が自ら送出量を絞る という形で実際の動作に現れます。

なお、この 2 つの分母の合計 3211 は、CM-BULK の 3303 packets と 92 パケットずれます。§5 の親カウンタと WRED の表の関係と同じく、本節では破棄率の比較を WRED の表の中だけで行い、class-map の offered は輻輳量の目安として扱います。

7. TCP Global Synchronization の実測試行

§2 で述べたとおり、Tail Drop の 2 つ目の害は複数の TCP フローが揃ってレートを絞る TCP Global Synchronization です。この現象は教科書では図で説明されることが多く、実機で波形として捉えるには条件が要ります。本節では、実際の TCP フローで再現を試みたうえで、結果をそのまま記録します。

TCP Global Synchronization の概念図。横軸が時間、縦軸が各フローのスループットで、青・橙・緑・紫の 4 本が同じ輻輳点 (R1 Gi3 の出力キュー) を共有する 4 つの並列 TCP フローを表す。★縦軸は受信側 PC2 で測った値であり、輻輳点を通過して実際に届いた量 (goodput) を指す。谷は輻輳点の動作そのものではなく、破棄を検知した各 TCP 送信者が輻輳ウィンドウを絞った結果である。上段の Tail Drop では 4 本の谷が同じ位置に重なり (破線枠)、合計に深い谷ができる。下段の WRED では破棄が確率的に起こるため谷の位置がフローごとにずれ、どの時点でも落ちているのは 1 本だけなので合計は平準化される。本図は教科書的な動作を示す概念図であり、この節の実機 3 試行ではこの同期波形は再現しなかった (判定結果は本節の後半)。

判定に使う数値は、すべて 受信側 PC2 の iperf サーバが記録した 1 秒刻みのスループット です。送信側 PC1 のクライアントが表示する値は、輻輳点の手前でソケットに書き込めた量に引きずられます。輻輳点を通過して実際に届いた量、つまり goodput を見るには、測定点を受信側に置く必要があります。サーバは常駐させておき、送出が終わったあとにログを読み出します。

snippet
PC2 $ nohup iperf -s -p 5201 -i 1 > /tmp/srv2_5201.log 2>&1 &
PC2 $ cat /tmp/srv2_5201.log
------------------------------------------------------------
Server listening on TCP port 5201
TCP window size:  128 KByte (default)
------------------------------------------------------------
[  1] local 192.168.20.20 port 5201 connected with 192.168.10.10 port 50208 (tos tx=0x28,dscp=10,ecn=0)
[  2] local 192.168.20.20 port 5201 connected with 192.168.10.10 port 50212 (tos tx=0x28,dscp=10,ecn=0)
[  3] local 192.168.20.20 port 5201 connected with 192.168.10.10 port 50228 (tos tx=0x28,dscp=10,ecn=0)
[  4] local 192.168.20.20 port 5201 connected with 192.168.10.10 port 50240 (tos tx=0x28,dscp=10,ecn=0)
[ ID] Interval       Transfer     Bandwidth
(1 秒刻みの区間行を省略)
[  3] 0.00-65.19 sec   884 KBytes   111 Kbits/sec
[  4] 0.00-65.23 sec   891 KBytes   112 Kbits/sec
[  1] 0.00-69.56 sec  1.47 MBytes   177 Kbits/sec
[  2] 0.00-70.38 sec   830 KBytes  96.6 Kbits/sec

これは試行 1 の受信側ログです。4 本のストリームがいずれも dscp=10 (AF11) で接続し、それぞれの累計行が最後に並びます。区間の刻みはストリームごとに独立していて、累計の終了時刻も 65.19 秒から 70.38 秒まで 5 秒あまりばらけます。 同じ「20.00-21.00」という区間ラベルが、ストリームどうしで厳密に同じ実時刻を指す保証はありません。

再現の可否を目視の印象で決めると判定が恣意的になるため、判定基準を測定の前に固定 します。本節が採用する基準は次の 3 つです。第 1 に、各フローの 1 秒刻みスループット系列から、先頭 10 秒 (slow start によるランプアップ区間) を除外します。第 2 に、全フローのペアについて Pearson の相関係数を計算し、その中央値を取ります。第 3 に、合計スループットの系列から、各試行の親 shaper のレートの 80 % を下回る谷の回数を数えます。この閾値は固定値ではなく試行ごとの親 shape から算出するため、shape 500 kbps の試行 1・試行 2 では 400 kbps、shape 300 kbps の試行 3 では 240 kbps になります。谷は、閾値を下回る 1 秒区間が連続している間を 1 回と数えます。この 2 つの指標がともに条件を満たしたときだけ「同期あり」と判定します。判定のしきい値は 相関係数の中央値が +0.5 以上、かつ谷が 2 回以上 です。

相関係数を指標に選ぶ理由は、帯域が固定されたボトルネックにおける自然な状態が 負の相関 だからです。フローが互いに独立して競合しているなら、一方が帯域を多く取れば他方は減るため、スループットは逆方向に動きます。全フローが揃って上下する正の相関は、同時破棄のような共通の外部要因がなければ生じません。合計スループットの谷は、その同期がもたらす実害 (リンクが空く) に対応する指標として置きます。

同期を起こしやすくするノブは 3 つあります。同時に破棄されるフローの数、破棄イベントの鋭さ (バッファの浅さ)、そしてサイクルの長さ (帯域と RTT) です。3 回の試行はこの 3 つを順に振ります。試行回数は 3 回で固定し、結果に応じて増減させません

試行親 shapequeue-limitTCP 並列数測定時間狙い
1500 kbps64 packets (1.536 秒分)460 秒素の構成のベースライン
2500 kbps16 packets (0.384 秒分)890 秒バッファを浅く・フロー数を倍にして 1 回の溢れで多数フローを同時に叩く
3300 kbps128 packets (5.12 秒分)490 秒帯域を下げてサイクルを長周期化し、1 秒サンプリングで捉えやすくする

試行 3 では、送信側の輻輳制御を CUBIC から Reno に変え、netem で RTT を上乗せすることも計画していました。どちらも PC1 側では実行できませんでした。

snippet
PC1 $ echo ---SYSCTL---; sysctl -w net.ipv4.tcp_congestion_control=reno 2>&1; sysctl net.ipv4.tcp_congestion_control 2>&1; echo ---NETEM---; tc qdisc add dev eth0 root netem delay 100ms 2>&1; tc qdisc show dev eth0 2>&1
---SYSCTL---
sysctl: error setting key 'net.ipv4.tcp_congestion_control': Permission denied
net.ipv4.tcp_congestion_control = cubic
---NETEM---
-sh: tc: not found
-sh: tc: not found

輻輳制御の変更は非特権のコンソールでは拒否され、tc コマンドは Alpine のイメージにそもそも存在しませんでした。試行 3 は既定の CUBIC のまま、遅延の上乗せなしで実施しています。

試行 1 の per-stream 時系列から、20 秒台の 6 秒間を抜き出します。iperf の stream 1 から stream 4 が、上図の Flow ① から Flow ④ にあたる 4 本の並列 TCP フローです。数値は 1 秒ごとの bits_per_sec を示します。

snippet
# 受信側 (PC2) ログから起こした per-stream 1秒刻み時系列 (t_start-t_end, bits_per_sec)
# 試行 1 の 20.00〜26.00 秒を抜粋 (値は逐語)
### stream 1
(0.00〜20.00 秒を省略)
20.00-21.00	266000
21.00-22.00	0
22.00-23.00	11600
23.00-24.00	440000
24.00-25.00	382000
25.00-26.00	151000
(26.00〜71.00 秒を省略)
### stream 2
(0.00〜20.00 秒を省略)
20.00-21.00	0
21.00-22.00	0
22.00-23.00	0
23.00-24.00	521000
24.00-25.00	11600
25.00-26.00	116000
(26.00〜71.00 秒を省略)
### stream 3
(0.00〜20.00 秒を省略)
20.00-21.00	11600
21.00-22.00	0
22.00-23.00	151000
23.00-24.00	46300
24.00-25.00	23200
25.00-26.00	69500
(26.00〜71.00 秒を省略)
### stream 4
(0.00〜20.00 秒を省略)
20.00-21.00	278000
21.00-22.00	34800
22.00-23.00	174000
23.00-24.00	104000
24.00-25.00	57900
25.00-26.00	127000
(26.00〜71.00 秒を省略)

4 つのフローは、いずれも激しく上下しています。21 秒台は 4 本とも 0 から 34800 の間に沈んでおり、揃って落ち込む区間が確かに存在します。ただし、その前後では高いフローと低いフローの組み合わせが入れ替わります。20 秒台は stream 1 と stream 4 が 26 万台で stream 2 と stream 3 が 0 付近、22 秒台は stream 3 と stream 4 が上がって stream 1 と stream 2 が沈み、23 秒台は stream 1 と stream 2 が 44 万から 52 万に跳ねる一方で stream 3 と stream 4 が下がっています。同じ谷が周期的に 4 本へ揃う波形ではありません。

3 試行の判定結果は次のとおりです。r_med は先頭 10 秒を除外した全フローペアの Pearson 相関係数の中央値、dip_count は合計受信スループットが dip_threshold を下回る区間の数です。

snippet
試行1: shape=500000 dip_threshold=400000 r_med=0.223 (n_pairs=6, n_streams=4) dip_count=14 (n_samples=61) → 判定=不成立
        合計系列の採用元 = iperf [SUM] 行 (完全区間 71 / 欠測により除外 0)
試行2: shape=500000 dip_threshold=400000 r_med=-0.189 (n_pairs=21, n_streams=8) dip_count=12 (n_samples=123) → 判定=不成立
        合計系列の採用元 = iperf [SUM] 行 (完全区間 133 / 欠測により除外 0)
試行3: shape=300000 dip_threshold=240000 r_med=-0.144 (n_pairs=6, n_streams=4) dip_count=22 (n_samples=106) → 判定=不成立
        合計系列の採用元 = per-stream からの再構成 (全ストリーム報告区間のみ) (完全区間 116 / 欠測により除外 8)

dip_threshold が試行 3 だけ 240000 になっているのは、この閾値を各試行の親 shape の 80 % として算出しているためです。合計系列は、試行 1 と試行 2 では iperf が区間ごとに出力する [SUM] 行をそのまま使っています。区間ごとの [SUM] 行が揃わなかった試行 3 だけは、全ストリームが値を報告している区間を足し合わせて再構成し、揃わなかった 8 区間を除外しています。

3 試行とも判定基準を満たさず、実機では明確な同期波形の再現に至りませんでした。 r_med は +0.223 / -0.189 / -0.144 で、いずれも判定基準の +0.5 に遠く届きません。符号も試行ごとに揃わず、試行 1 がごく弱い正、試行 2 と試行 3 が弱い負です。4 本ないし 8 本のフローが揃って上下する強い正の相関は、どの試行でも現れていません

試行 2 の n_pairs が 8 本の全組み合わせ 28 通りではなく 21 通りなのは、8 本のうち 1 本が判定区間のほぼ全域で 0 bps のままだったためです。値が変化しないストリームとのペアは相関係数が定義できないため、その 7 通りが除外されています。このストリームは、除外区間である先頭 10 秒で少量を受信したあと、145 秒台まで 0 bps が続いています。浅いバッファと 8 並列の組み合わせでは、揃って絞られるのとは逆に、1 本が長時間ほとんど通らない状態も生じています。

谷の回数は、そのままリンクが空いたことを意味しません。 谷が輻輳点のアイドルを示しているなら、R1 が送り出した総量もその分だけ減っているはずです。試行 1 のカウンタは次のとおりです。

snippet
R1# show policy-map interface GigabitEthernet3
 GigabitEthernet3

  Service-policy output: PM-PARENT-TAILDROP

    Class-map: class-default (match-any)
      3162 packets, 4718334 bytes
      5 minute offered rate 115000 bps, drop rate 9000 bps
      Match: any
      Queueing
      queue limit 64 packets
      (queue depth/total drops/no-buffer drops) 0/216/0
      (pkts output/bytes output) 2946/4404304
      shape (average) cir 500000, bc 2000, be 2000
      target shape rate 500000

到着した 3162 パケットのうち 216 パケットが落ちており、破棄率は 6.83 % です。輻輳そのものは成立しています。送出できた 4404304 バイトは、cir 500000 で送り切るのに 70.5 秒かかる量です (4404304 × 8 ÷ 500000 = 70.47)。受信側の合計系列が覆う区間は 71 秒なので、shaper はこの区間のほぼ全体で cir 相当のレートを出し続けていた ことになります。受信側で per-stream の累計から求めた持続平均も 478157 bps と、shape 500 kbps の 0.956 倍です。

3 試行を同じ式で並べると次のようになります。

試行bytes outputcircir で送り切る所要時間受信側系列の区間長
14404304500 kbps70.5 秒71 秒
26504936500 kbps104.1 秒133 秒
34678376300 kbps124.8 秒124.65 秒

試行 2 だけ所要時間が区間長より短いのは、8 本のストリームの終了時刻が 93 秒から 147 秒までばらけ、区間の後半では動いているフローが減っていたためです。カウンタの計上区間は iperf の測定窓と厳密には一致しないため、この対比は目安にとどまります。それでも、輻輳点が繰り返し長くアイドルになっていたと読める値ではありません。本節の測定で言えるのは「合計受信スループットが閾値を下回る 1 秒区間が、試行ごとに 14 回・12 回・22 回あった」という観測事実までで、そこからリンクが空いたと断定することはできません。1 秒ビンの合計値は、ストリームごとに区間の刻みが独立しているぶん、隣り合うビンへ配分が寄る影響も受けます。

測定の粒度についても確認します。判定は 1 秒粒度で固定していますが、同じ受信側データをより粗いビンで集計すると相関は動きます。参考値は次のとおりで、括弧内はビンの個数です。

試行1 秒ビン3 秒ビン10 秒ビン
1+0.223 (61)+0.340 (21)-0.133 (7)
2-0.189 (123)-0.240 (41)-0.319 (13)
3-0.144 (106)+0.192 (36)+0.406 (11)

動き方は試行ごとに逆向きです。試行 1 は 3 秒ビンで +0.340 まで上がってから 10 秒ビンで負に転じ、試行 2 は粗くするほど負に振れ、試行 3 は粗くするほど正に振れて 10 秒ビンでは +0.406 と判定基準の +0.5 に近づきます。ただし 10 秒ビンではビンの個数が 7 個から 13 個しかなく、相関係数は数点の増減で大きく動く不安定な領域に入ります。この表は測定粒度に対する感度の開示であって、判定基準ではありません。 主判定は測定前に固定した 1 秒粒度のままとし、結果を見てから都合のよい粒度を選び直すことはしません。

再現しなかった理由として、次の 4 点が考えられます。いずれも、この節の測定だけで確定できるものではありません。

  1. 現代の Linux TCP の既定は CUBIC です。 CUBIC の回復曲線は Reno の直線的なノコギリ波と違って凹型で、パケットペーシングも入るため、フローどうしの位相がばらけやすくなります。教科書の Global Synchronization は Reno 型の急峻な同期波形を前提としています。試行 3 で Reno への切り替えを試みましたが、上記のとおり実行できず、この仮説を実機で切り分けることはできませんでした。
  2. BDP に対してバッファが極端に深すぎます。 §3 で確認したとおり、このラボの帯域遅延積は 125 バイトで 1 パケットにも満たないのに対し、queue-limit 64 packets は 1.536 秒分あります。教科書の同期が想定する「大きな BDP に対して浅いバッファ」とは環境の桁が逆で、本ラボはむしろバッファブロート型の環境です。
  3. 測定の粒度と位相ずれに対する感度が残っています。 上表のとおり、ビン幅を変えると相関の符号が変わる試行があります。iperf の区間の刻みはストリームごとに独立しており、累計の終了時刻も 5 秒あまりばらけるため、同じ区間ラベルの値を並べて相関を取る方法は、フロー間の位相ずれに弱い側面もあります。デモ天井 1000 kb/s の環境で 4 本から 8 本に分けると 1 フローあたりの実効レートは数十 kbps 級で、1 秒刻みでは細かい破棄と回復のサイクルを捉えきれないおそれもあります。
  4. バッファを浅くしても同期しませんでした。 queue-limit を 16 packets (0.384 秒分) まで狭め、フロー数を 8 本に倍増した試行 2 は、r_med が -0.189 と弱い負の相関で、谷の回数も 123 サンプル中 12 回と 3 試行で最少でした。破棄率は 13.27 % (664 / 5005) と 3 試行で最も高く、破棄そのものは十分に起きています。「バッファを浅くして同時破棄の確率を上げる」という筋書きだけでは、この環境で正の相関を作るには至っていません。

教科書に載っている現象が、現代の輻輳制御と仮想環境でそのまま再現するとは限りません。判定基準と谷の閾値を測定の前に固定しておいたおかげで、「谷は出ているから同期しているように見える」という都合のよい読み方をせずに済みました。再現しなかったという結果自体が、この節の実測です。

8. 落とし穴・補足

輻輳回避は、効く相手と置き場所を取り違えると、無駄に捨てるだけの設定になります。本節の実機に関わる注意点をまとめます。

WRED が効くのは TCP のような応答性トラフィックだけです。 UDP はパケットを落とされてもレートを下げないため、WRED を当てても早く捨てた分がそのまま失われるだけになります。音声や映像のような UDP 主体のクラスには WRED を当てず、そのクラスは LLQ の priority や CBWFQ の bandwidth で守ります。本節の主役を UDP から TCP に変えたのも、この性質が理由です。

既定は Tail Drop です。 random-detect を書いていないクラスは、キューが満杯になった後に無差別に破棄します。「設定していないのに早期破棄されている」ということはありません。逆に言えば、WRED を効かせたいクラスには明示的に random-detect を置く必要があります。

階層型ポリシーでは WRED は子ポリシーにだけ置きます。 親ポリシーは shape を担当し、WRED は親には当てられません (§5)。5-6 と 5-7 で組んだ「親 shaper + 子ポリシー」の構造が、そのまま WRED の置き場所の制約と噛み合います。

priority (LLQ) クラスには WRED を当てられません。 優先キューは遅延を守るためのキューで、早期破棄の対象にする設計ではありません。csr1000v は、この組み合わせを警告や暗黙の無視ではなく、明確な拒否メッセージで弾きます。

snippet
R1(config-pmap)# class CM-VOICE
R1(config-pmap-c)#  priority 200
R1(config-pmap-c)#  random-detect
Must deconfigure priority in this class before issuing this command
snippet
R1# show policy-map PM-TEST-PRIO-RD
  Policy Map PM-TEST-PRIO-RD
    Class CM-VOICE
      priority 200 (kbps)

投入後のポリシーには priority 200 (kbps) だけが残り、random-detect は入っていません。「WRED は priority トラフィックには非対応」という他プラットフォームのドキュメントの記述が、この csr1000v 17.03 でも設定レベルで確認できます。

最小閾値と最大閾値の間隔を十分に取ります。 2 つの閾値が近すぎると、平均キュー長が短時間でその区間を通過してしまい、多数のパケットが一度に破棄されます。この状態では WRED を当てていても同時大量破棄が起き、同期を招きます。「WRED を入れれば必ず同期しない」わけではなく、閾値の設計が効き目を左右します。

閾値の単位は混在させられません。 閾値は packets のほか bytes や ms でも指定できますが、同一クラス・同一ポリシーの中で単位を混在させることはできません。本節は 5-6 の queue limit 64 packets と同じ土俵に乗せるため、packets で統一しています。

バッファが BDP に対して深すぎると、Tail Drop の害は損失より遅延に出ます。 §3 で確認したとおり、このラボの queue-limit 64 packets は 1.536 秒分のバッファで、BDP の 125 バイト (1 パケット未満) に対して極端に深い値です。深いバッファは損失を減らしますが、その代わりに待ち時間を積み上げます。WRED でキューを浅く保つ効能は、この遅延の削減としても働きます。

TCP は自分でレートを絞るため、送出量だけでは輻輳を作れません。 UDP のように「送出レート > shape レート」にすれば必ず溢れる、という前提が TCP には通りません (§3)。並列数とバッファサイズを組み合わせて総需要を上げ、実機のカウンタで輻輳成立を確認してから対比に入ります。

同期の実験に fair-queue を入れると現象が壊れます。 WFQ はフローを別々のキューに分離するため、複数フローが同時に破棄を受ける状況そのものが起きにくくなります。§7 の Tail Drop 構成には fair-queue を入れません。

WRED の表の Tail drop 列は「実キューが満杯になった数」ではありません。 この列が数えるのは、平均キュー長が最大閾値を超えたときの破棄です。WRED は平均キュー長が最大閾値を超えると到着したパケットをすべて破棄するため、輻輳が最大閾値の先まで進めば Tail drop 列は必ず増えます。§5 の実測で Random drop 117 と Tail drop 90 が併存したのはこの動作によるもので、queue-limit 64 packets に実キューが到達したことを示す数ではありません。「WRED を入れたのに tail drop が出ている」のは異常ではなく、平均キュー長を閾値の内側に収めきれていないことを示す値として読みます。

Mean queue depthqueue depth は 1 回の show では傾向を語れません。 Mean queue depth は取得時点の WRED 判定に使われる指数加重移動平均であり、測定期間全体の統計平均ではありません。§5 で得た 29 パケットも、その瞬間の平均キュー長を示すだけです。queue depth も同様で、§5 では同一の Tail Drop 構成が別の測定で 61 と 0 の両方を記録しました。キューの深さを根拠にしたい場合は、送出中に show policy-map interface を複数回取得して時系列で並べます。

Random drop の raw 件数だけで効き目を比較すると誤読します。 §6 の実測では、早く捨てられるはずの AF13 の Random drop (6) が AF11 (58) より少なくなりました。強く絞られたクラスは送出量そのものが減るため、破棄の絶対数はむしろ小さくなり得ます。比較は必ず、到着を試みた総数を分母にした破棄率で行います。

ポリシーを当て直さない限り、インタフェースのカウンタはリセットされません。 §5 で閾値を変更した際、no service-policy で外さずに値だけを追加投入したため、変更後の表示は変更前からの累積値になりました。閾値変更の前後を厳密に比較したい場合は、一度外して当て直し、ゼロから計測します。

show running-config は DSCP 名を数値で表示します。 random-detect dscp af11 24 40 10 と入力しても、running-config には random-detect dscp 10 24 40 10 と出ます。設定ファイルの差分を取るときに、入力した文字列で grep しても引っかかりません。

測定用ツールの存在を前提にしません。 本検証の Alpine ノードには iperf3tc も入っておらず、sysctl による輻輳制御の変更も非特権では拒否されました (§3・§7)。公開ドキュメントの記述と実機のイメージは食い違うことがあるため、測定計画を立てる前に実機で存在を確認します。

仮想機のカウンタは、動くことを確認してから信用します。 5-6 と 5-7 では csr1000v の policing / shaping / CBWFQ / LLQ のカウンタが実機同等に動きましたが、WRED のランダム破棄が仮想機のデータプレーンで実装されているかは、それらの成功とは別の問題です。設定が受理されることと、カウンタが動くことは同じではありません。本節の csr1000v 17.03 では Random drop が実際に増えることを確認できましたが、これは事前に確かめるべき前提であって、当然の結果ではありません。

最後に、本節で組んだ輻輳回避の定義を running-config から抜き出すと次のようになります。

snippet
R1# show running-config | section class-map CM-|policy-map PM-
policy-map PM-CHILD-WRED
 class class-default
  queue-limit 64 packets
  random-detect
  random-detect precedence 1 8 32 10
policy-map PM-PARENT-WRED
 class class-default
  shape average 500000
   service-policy PM-CHILD-WRED
policy-map PM-PARENT-TAILDROP
 class class-default
  shape average 500000
  queue-limit 64 packets
policy-map PM-CHILD-WRED-DSCP
 class CM-BULK
  bandwidth 300
  random-detect dscp-based
  random-detect dscp 10 24 40 10
  random-detect dscp 14 8 24 10
 class class-default
  fair-queue
policy-map PM-PARENT-WRED-DSCP
 class class-default
  shape average 500000
   service-policy PM-CHILD-WRED-DSCP

9. 次節

本節では、キューが溢れる前にパケットを捨てる輻輳回避を、csr1000v の実機で扱いました。既定の Tail Drop はキューが満杯になった後に到着したパケットを無差別に一括破棄し、キューを満杯のまま張り付かせます。これに対し random-detect による RED / WRED は、平均キュー長が最小閾値を超えた時点から確率的に早期破棄し、キューを浅く保ちます。両者の違いは show policy-map interface の Random drop 列と Tail drop 列に現れます。WRED の重み付けは、AF11 と AF13 のように IP Precedence では区別できない印の差を、dscp-based の閾値として表現します。

実測では、Tail Drop 構成の total drops 248 に対し、送出条件を完全に揃えた WRED 構成が Random drop 117 と Tail drop 90 を計上しました。捨てた総量はほぼ同じです。取得時点の queue depth は 61 と 6 でしたが、どちらも送出完了後の 1 回取得であり、同じ Tail Drop 構成の別測定では 0 も観測されているため、この差をポリシーの差として読むことはできません。WRED を入れても Tail drop 列がゼロにならない点、そしてこの列が「平均キュー長が最大閾値を超えたときの破棄」であって実キューの満杯を意味しない点も、実機の姿として記録しました。dscp-based の重み付けでは、AF13 が AF11 の約 4 倍の破棄率で絞られ、送出量そのものが約 9 分の 1 に落ちました。一方 TCP Global Synchronization は、受信側 PC2 で測った 3 回の試行すべてで判定基準を満たしませんでした。相関係数の中央値は +0.223・-0.189・-0.144 と判定基準の +0.5 に届かず、符号も試行ごとに揃いません。教科書どおりの同期波形はこの環境では再現していません。

ここまで 5-5 から 5-8 で扱ってきた policing・shaping・queuing・HQoS・輻輳回避は、いずれも 1 つの機構を単体で取り出して、その挙動を実機で確かめるものでした。実際のネットワークでは、これらを LAN 側と WAN 側で役割分担させ、1 つの設計としてまとめる必要があります。LAN 側では信頼境界で印を付け直し、WAN 側では契約帯域に絞ったうえでクラス別に守る、という配置です。次節 5-9 では、Auto-QoS や DSCP-Trust、サブレート shaping、業務トラフィックを分類する 8 クラスモデルを軸に、LAN QoS と WAN QoS の設計を見ていきましょう。