TCP/UDP のスループットと RTT の関係性をざっくり理解する

次のような環境を用意し、TCP/UDP のスループットと RTT の関係性を検証する。

f:id:kimulla:20200822082515p:plain

検証結果

次の検証結果から、TCP のスループットは RTT が増えると低下することがわかる。なお、これは CUBIC における検証結果なので、輻輳制御アルゴリズムが異なると厳密には結果が異なる。また、今回の検証は同一ホスト上のコンテナ間通信のため物理ケーブルの帯域制限などは存在せず、純粋に CPU の性能限界によってスループットの上限が決まっている。

f:id:kimulla:20200822082538p:plain

TCP/UDP RTT  Bitrate(スループット)
TCP 0ms 65.6 Gbits/sec
TCP 1ms 21.9 Gbits/sec
TCP 10ms 2.33 Gbits/sec
TCP 100ms 224 Mbits/sec
TCP 1000ms 5.23 Mbits/sec

また次の検証結果から、UDP のスループットは RTT に比例しないことがわかる。また UDP は再送制御の仕組みがないため、送信側で設定した送信スピードに無理があると受信側が取りこぼす可能性があることもわかる。

f:id:kimulla:20200822082555p:plain

TCP/UDP RTT Bitrate  Bitrate(sender)  Bitrate(receiver) Lost/Total
UDP 0ms 1M 1.00 Mbits/sec 997 Kbits/sec 0%
UDP 1ms 1M 1.00 Mbits/sec 997 Kbits/sec 0%
UDP 10ms 1M 1.00 Mbits/sec 996 Kbits/sec 0%
UDP 100ms 1M 1.00 Mbits/sec 987 Kbits/sec 0%
UDP 1000ms 1M 1.00 Mbits/sec 907 Kbits/sec 0%
UDP 0ms 10M 10.0 Mbits/sec 9.96 Mbits/sec 0%
UDP 1ms 10M 10.0 Mbits/sec 9.96 Mbits/sec 0%
UDP 10ms 10M 10.0 Mbits/sec 9.95 Mbits/sec 0%
UDP 100ms 10M 10.0 Mbits/sec 9.86 Mbits/sec 0%
UDP 1000ms 10M 10.0 Mbits/sec 9.06 Mbits/sec 0%
UDP 0ms 100M 100.0 Mbits/sec 99.6 Mbits/sec 0%
UDP 1ms 100M 100.0 Mbits/sec 99.6 Mbits/sec 0%
UDP 10ms 100M 100.0 Mbits/sec 99.5 Mbits/sec 0%
UDP 100ms 100M 100.0 Mbits/sec 98.6 Mbits/sec 0%
UDP 1000ms 100M 100.0 Mbits/sec 8.72 Mbits/sec 87%
UDP 0ms 1000M 1000 Mbits/sec 996 Mbits/sec 0.014%
UDP 1ms 1000M 1000 Mbits/sec 996 Mbits/sec 0.017%
UDP 10ms 1000M 1000 Mbits/sec 995 Mbits/sec 0.024%
UDP 100ms 1000M 1000 Mbits/sec 111 Mbits/sec 88%
UDP 1000ms 1000M 1000 Mbits/sec 8.49 Mbits/sec 99%

検証結果の理由

TCP はデータ欠損が発生しない。これを実現するための仕組みに再送処理があるが、そもそも再送がなるべく発生しないように、受信側が処理可能なデータ量だけを送信するように流量制御を行っている。流量制御の仕組みは、

  1. 受信側はバッファを用意する。

  2. 受信側は送信側にウィンドウサイズ(rwnd)を通知する。(バイト数単位で指定する)

  3. 送信側は輻輳制御アルゴリズムに基づいてウィンドウサイズ(cwnd)を決定し、ウィンドウサイズ( min(cwnd, rwnd) )分のデータを送信する。

  4. 受信側はデータを受け取ったら ACK を返す。

  5. 送信側は ACK を受け取ると、次のデータを送信する(ウィンドウがスライドする=スライディングウィンドウ)。このとき輻輳制御アルゴリズムがウィンドウサイズを再調整する。

f:id:kimulla:20200822082823g:plain

ただ複数セグメントをまとめて送信できるとはいえ、送信側がパケットを送るためには、 受信側からの ACK が定期的に必要になる。そのため RTT が大きいと ACK 待ちの時間が増え、パケットの送信間隔が伸びる。つまり、スループットが落ちる。

f:id:kimulla:20200822102443p:plain

なおスループットの理論値は次のようになるらしい。
参考 教科書には載っていない ネットワークエンジニアの実践技術

スループット(bps) = TCP Window Size(Byte) * 8(bit) / RTT(sec)

( ※ この理論値はかなり参考値な気がする。ウィンドウサイズは輻輳制御アルゴリズムによって刻一刻と変化する値だし、RTT もネットワーク環境の変化でいくらでも変化する。それを固定で見積もるのは現実を無視した値なので、見積もり通りの性能が出ることはないはず。あくまで大まかな目安を知る目的にとどめておき、詳しくは検証すべきだと思う。)

上記式からもわかる通り、一般的にはウィンドウサイズが大きいほどスループットが上がる。TCP ヘッダーに設定できるウィンドウサイズの上限は 216(64KB) だが、ヘッダーオプションに Window Scale (RFC1323)が存在し、これを利用すると 230(約 1GB)までウィンドウサイズを広げられる。

CentOS 8 のデフォルトだと有効になっている。

# sysctl net.ipv4.tcp_window_scaling
net.ipv4.tcp_window_scaling = 1

ただウィンドウサイズをフルに使うためにはバッファサイズのチューニングが必要らしい。次の記事が参考になりそう。

UDP は信頼性のない仕組みのため、送信側はとにかくパケットを送り続ける。RTT が延びれば最初のパケットの到着時間は延びるものの、それ以降はデータが到着し続けるため、RTT がスループットに影響しない。

まとめ

  • TCP のスループットは RTT が増えると低下する
  • UDP のスループットは RTT に比例しない
  • だからといって TCP より UDP が偉いわけではない。TCP は信頼性を確保するためにこうしてる。
  • ネットワーク全然わからん。もう少し勉強しよう。

検証方法

CentOS8 が入ったホストを 1 台用意する。

Host ]# cat /etc/redhat-release
CentOS Linux release 8.1.1911 (Core)
Host ]# uname -a
Linux localhost.localdomain 4.18.0-193.6.3.el8_2.x86_64 #1 SMP Wed Jun 10 11:09:32 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

Host ]# sysctl net.ipv4.tcp_congestion_control
net.ipv4.tcp_congestion_control = cubic

送信用のコンテナを用意する。

Sender ]# podman run -it --rm centos:8 /bin/bash
Sender ]# yum install -y iperf3

受信用のコンテナを用意する。

Sender ]# podman run -it --rm centos:8 /bin/bash
Receiver ]# yum install -y iperf3

コンテナに iperf3 をインストールし、スループットを計測する。

Receiver ]# iperf3 -s
-----------------------------------------------------------
Server listening on 5201
-----------------------------------------------------------

// TCP
Sender ]# iperf3 -c 10.88.0.100 -p 5201
Connecting to host 10.88.0.100, port 5201
[  5] local 10.88.0.101 port 36354 connected to 10.88.0.100 port 5201
[ ID] Interval           Transfer     Bitrate         Retr  Cwnd
[  5]   0.00-1.00   sec  7.71 GBytes  66.2 Gbits/sec    0   3.09 MBytes
[  5]   1.00-2.00   sec  7.74 GBytes  66.5 Gbits/sec    0   3.09 MBytes
[  5]   2.00-3.00   sec  7.73 GBytes  66.4 Gbits/sec    0   3.09 MBytes
[  5]   3.00-4.00   sec  7.75 GBytes  66.6 Gbits/sec   47   2.22 MBytes
[  5]   4.00-5.00   sec  7.75 GBytes  66.6 Gbits/sec    0   2.42 MBytes
[  5]   5.00-6.00   sec  7.77 GBytes  66.7 Gbits/sec   45   2.45 MBytes
[  5]   6.00-7.00   sec  7.71 GBytes  66.2 Gbits/sec    0   2.47 MBytes
[  5]   7.00-8.00   sec  7.69 GBytes  66.1 Gbits/sec    0   2.47 MBytes
[  5]   8.00-9.00   sec  7.67 GBytes  65.9 Gbits/sec   25   2.47 MBytes
[  5]   9.00-10.00  sec  7.70 GBytes  66.1 Gbits/sec    0   2.47 MBytes
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Retr
[  5]   0.00-10.00  sec  77.2 GBytes  66.3 Gbits/sec  117             sender
[  5]   0.00-10.03  sec  77.2 GBytes  66.1 Gbits/sec                  receiver

iperf Done.

// UDP
Sender ]# iperf3 -c 10.88.0.100 -p 5201 -b 1MB -u 
Connecting to host 10.88.0.106, port 5201
[  5] local 10.88.0.101 port 60525 connected to 10.88.0.100 port 5201
[ ID] Interval           Transfer     Bitrate         Total Datagrams
[  5]   0.00-1.00   sec   123 KBytes  1.01 Mbits/sec  87
[  5]   1.00-2.00   sec   122 KBytes   996 Kbits/sec  86
[  5]   2.00-3.00   sec   122 KBytes   996 Kbits/sec  86
[  5]   3.00-4.00   sec   123 KBytes  1.01 Mbits/sec  87
[  5]   4.00-5.00   sec   122 KBytes   996 Kbits/sec  86
[  5]   5.00-6.00   sec   122 KBytes   996 Kbits/sec  86
[  5]   6.00-7.00   sec   123 KBytes  1.01 Mbits/sec  87
[  5]   7.00-8.00   sec   122 KBytes   996 Kbits/sec  86
[  5]   8.00-9.00   sec   122 KBytes   996 Kbits/sec  86
[  5]   9.00-10.00  sec   123 KBytes  1.01 Mbits/sec  87
- - - - - - - - - - - - - - - - - - - - - - - - -
[ ID] Interval           Transfer     Bitrate         Jitter    Lost/Total Datagrams
[  5]   0.00-10.00  sec  1.19 MBytes  1.00 Mbits/sec  0.000 ms  0/864 (0%)  sender
[  5]   0.00-11.04  sec  1.19 MBytes   907 Kbits/sec  0.004 ms  0/864 (0%)  receiver

iperf Done.

次に、検証パターンごとに RTT を伸ばす。そのために、コンテナに対応する veth に qdisc で遅延を挿入する。

Host ]# podman inspect relaxed_knuth | grep cni
            "SandboxKey": "/var/run/netns/cni-fff0bb28-e38b-6139-9914-de4b390775fe",
Host ]# ip --detail address show dev vethd4bb9e1c
41: vethd4bb9e1c@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master cni-podman0 state UP group default
    link/ether f6:f6:23:f1:f3:45 brd ff:ff:ff:ff:ff:ff link-netns cni-fff0bb28-e38b-6139-9914-de4b390775fe promiscuity 1 minmtu 68 maxmtu 65535
    veth
    bridge_slave state forwarding priority 32 cost 2 hairpin off guard off root_block off fastleave off learning on flood on port_id 0x8002 port_no 0x2 designated_port 32770 designated_cost 0 designated_bridge 8000.26:c5:b4:fc:d7:d4 designated_root 8000.26:c5:b4:fc:d7:d4 hold_timer    0.00 message_age_timer    0.00 forward_delay_timer    0.00 topology_change_ack 0 config_pending 0 proxy_arp off proxy_arp_wifi off mcast_router 1 mcast_fast_leave off mcast_flood on mcast_to_unicast off neigh_suppress off group_fwd_mask 0 group_fwd_mask_str 0x0 vlan_tunnel off isolated off numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet6 fe80::f4f6:23ff:fef1:f345/64 scope link
       valid_lft forever preferred_lft forever

Host ]# tc qdisc add dev vethd4bb9e1c root netem delay 1000ms

遅延をかけた場合の ping を確認すると、 RTT が設定した遅延ぶん伸びているのがわかる。遅延をかけない場合も 0.001ms くらいの RTT だったが、ほぼ無視できるので無視した。

Sender ]# ping 10.88.0.100
PING 10.88.0.100 (10.88.0.100) 56(84) bytes of data.
64 bytes from 10.88.0.100: icmp_seq=1 ttl=64 time=100 ms
64 bytes from 10.88.0.100: icmp_seq=2 ttl=64 time=100 ms
64 bytes from 10.88.0.100: icmp_seq=3 ttl=64 time=100 ms
^C
--- 10.88.0.100 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 5ms
rtt min/avg/max/mdev = 100.172/100.173/100.175/0.258 ms

これを利用し RTT を 0ms 1ms 10ms 100ms 1000ms に設定して計測する。