SIer だけど技術やりたいブログ

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

linux network

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

検証結果

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

TCP/UDPRTT Bitrate(スループット)
TCP0ms65.6 Gbits/sec
TCP1ms21.9 Gbits/sec
TCP10ms2.33 Gbits/sec
TCP100ms224 Mbits/sec
TCP1000ms5.23 Mbits/sec

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

TCP/UDPRTTBitrate Bitrate(sender) Bitrate(receiver)Lost/Total
UDP0ms1M1.00 Mbits/sec997 Kbits/sec0%
UDP1ms1M1.00 Mbits/sec997 Kbits/sec0%
UDP10ms1M1.00 Mbits/sec996 Kbits/sec0%
UDP100ms1M1.00 Mbits/sec987 Kbits/sec0%
UDP1000ms1M1.00 Mbits/sec907 Kbits/sec0%
UDP0ms10M10.0 Mbits/sec9.96 Mbits/sec0%
UDP1ms10M10.0 Mbits/sec9.96 Mbits/sec0%
UDP10ms10M10.0 Mbits/sec9.95 Mbits/sec0%
UDP100ms10M10.0 Mbits/sec9.86 Mbits/sec0%
UDP1000ms10M10.0 Mbits/sec9.06 Mbits/sec0%
UDP0ms100M100.0 Mbits/sec99.6 Mbits/sec0%
UDP1ms100M100.0 Mbits/sec99.6 Mbits/sec0%
UDP10ms100M100.0 Mbits/sec99.5 Mbits/sec0%
UDP100ms100M100.0 Mbits/sec98.6 Mbits/sec0%
UDP1000ms100M100.0 Mbits/sec8.72 Mbits/sec87%
UDP0ms1000M1000 Mbits/sec996 Mbits/sec0.014%
UDP1ms1000M1000 Mbits/sec996 Mbits/sec0.017%
UDP10ms1000M1000 Mbits/sec995 Mbits/sec0.024%
UDP100ms1000M1000 Mbits/sec111 Mbits/sec88%
UDP1000ms1000M1000 Mbits/sec8.49 Mbits/sec99%

検証結果の理由

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

  1. 受信側はバッファを用意する。
  2. 受信側は送信側にウィンドウサイズ(rwnd)を通知する。(バイト数単位で指定する)
  3. 送信側は輻輳制御アルゴリズムに基づいてウィンドウサイズ(cwnd)を決定し、ウィンドウサイズ( min(cwnd, rwnd) )分のデータを送信する。
  4. 受信側はデータを受け取ったら ACK を返す。
  5. 送信側は 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

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

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 に設定して計測する。