次のような環境を用意し、TCP/UDP のスループットと RTT の関係性を検証する。
検証結果
次の検証結果から、TCP のスループットは RTT が増えると低下することがわかる。なお、これは CUBIC における検証結果なので、輻輳制御アルゴリズムが異なると厳密には結果が異なる。また、今回の検証は同一ホスト上のコンテナ間通信のため物理ケーブルの帯域制限などは存在せず、純粋に CPU の性能限界によってスループットの上限が決まっている。
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 は再送制御の仕組みがないため、送信側で設定した送信スピードに無理があると受信側が取りこぼす可能性があることもわかる。
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 はデータ欠損が発生しない。これを実現するための仕組みに再送処理があるが、そもそも再送がなるべく発生しないように、受信側が処理可能なデータ量だけを送信するように流量制御を行っている。流量制御の仕組みは、
- 受信側はバッファを用意する。
- 受信側は送信側にウィンドウサイズ(rwnd)を通知する。(バイト数単位で指定する)
- 送信側は輻輳制御アルゴリズムに基づいてウィンドウサイズ(cwnd)を決定し、ウィンドウサイズ( min(cwnd, rwnd) )分のデータを送信する。
- 受信側はデータを受け取ったら ACK を返す。
- 送信側は ACK を受け取ると、次のデータを送信する(ウィンドウがスライドする=スライディングウィンドウ)。このとき輻輳制御アルゴリズムがウィンドウサイズを再調整する。
ただ複数セグメントをまとめて送信できるとはいえ、送信側がパケットを送るためには、 受信側からの ACK が定期的に必要になる。そのため RTT が大きいと ACK 待ちの時間が増え、パケットの送信間隔が伸びる。つまり、スループットが落ちる。
なおスループットの理論値は次のようになるらしい。
参考 教科書には載っていない ネットワークエンジニアの実践技術
スループット(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
ただウィンドウサイズをフルに使うためにはバッファサイズのチューニングが必要らしい。次の記事が参考になりそう。
- LinuxサーバーのTCPネットワークのパフォーマンスを決定するカーネルパラメータ – 1編 | TOAST Meetup
- 【図解】TCP window size の仕組み〜MSS(MTU)との違い,calculated window size,unknown factor,受信バッファの設定変更~ | SEの道標
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 に設定して計測する。