事始め

例のシステムで、tcコマンドでネットワークレイテンシーを増加させたんですけどね。 そしたら、たったの100msの遅延だったんですけど、64MiBのデータを送信するときに、 アプリから見た時の遅延の時間がなんと1sくらいまで伸びてしまったんですよね。これ、めちゃめちゃ面白い話で、深堀りする価値があるんです。 もしかしたら、ロングファットパイプ問題についても、わかるかもしれないね。

まず、TCPの基礎、どうやって動いているのかを理解する必要がある。 が、せっかくなので、もう一回上からやろうか。 アプリケーションレイヤーから、どうやってデータが通信相手まで伝わるのか、ってのをもう一回確認して、 本題のTCPレイヤーのMTU,MSS,windowサイズについて話した後、tcpダンプの使い方を調べ、最後に実際にTCPダンプをして、 例のシステムにおいて、サーバ・クライアント間で100msの遅延が生じているときのデータのやり取りについてみてみましょう。

OSIモデル、プロトコルスタックについて確認

まず、プロトコルスタックとは「コンピュータネットワーク用のプロトコルの階層」のことです。 OSIモデル(7層) で見ていくのが大事そうですね。上から、アプリケーション層、プレゼンテーション層、セッション層、トランスポート層、ネットワーク層、データリンク層、物理層。

アプリケーション層では、具体的なサービスを提供。HTTPとか、websocketとか、FTPとか、SSHとか、その他いろいろ。

プレゼンテーション層は、データのフォーマットを定義する?そうですね。エンコードの方法とか。これはおそらくまだOSレイヤーの話ではない。

ここからがOSの世界の話。 セッション層は、connect(sockdrp,IP)で、コネクションを確立したときに、コネクションの相手を覚えておく層、だと思っていい。 つまり、ソケットのことです。ソケットが通信相手の情報を覚えておきます。 ソケットは、OSによってプログラムのされ方が違うらしいです。それでも異なるOS同士ど通信ができるのは、トランスポート層で規定されているプロトコルにすべてのOSが準拠しているからです。

トランスポート層 (L4) が、TCP / UDP です。TCPでは、アプリケーションからもらったデータを小分けにして、通し番号を付けて、 小分けにしたデータをちょっとづつ送ります。小分けにされたデータが、いわゆる「パケット」です。 再送とかもする。信頼性が高いのです。その代わりスループットは悪くなります。 一方、UDPはデータを投げっぱなしにします。信頼性は低いですが、スループットは高くなります。VoIPなどに使われますね。 L4ではまだ、IPは指定しません。

その下、ネットワーク層が、IPです。ルーティングを決めてくれます。

その下、データリンク層が、イーサネットですね。L2TPとかをするVPN、ありますよね。あれはね、このレイヤーで動いているんですよね。

その下、もう物理層です。L1です。

L4、トランスポート層のTCPについてもう少し詳しく見ていく

まずは、TCPヘッダーのフォーマットを見てみましょう。

送信元ポート番号:16bit
送信先ポート番号:16bit
シーケンス番号:32bit
ACK番号:32bit
データオフセット:4bit
未使用:6bit
コントロールビット:6bit
ウインドウ:16bit
チェックサム:16bit
緊急ポインタ:16bit
オプション:可変長

いろいろありますが、ここでまず注目したいのは、通信相手のIPアドレスがないってことですね。 なぜなら、それはL3が担当するからです。 その他、自分的にまだ理解が完ぺきではないけど大事そうなのは、シーケンス番号、ACK番号、コントロールビット、 ウィンドウの、4つですね。それぞれについて説明していきます。

シーケンス番号

「このパケットの先頭のデータが送信データの何バイト目にあたるのか送信側から受信側に伝えるためのもの」 TCPではデータを小分けにして送りますね。パケットです。で、何バイト目か?って話ですね。32bitしかないってことは、 2^32/1024/1024/1024 = 4GiBしか一回で送れないってこと?まじ??まあいいや。そんなことはないと思うけどね。

ACK番号

「データが何バイト目まで受信側に届いたのか、受信側から送信側に伝えるためのもの。」だそうです。

コントロールビット

URG:緊急ポインタ
ACK:データが正しく受信側に届いたことを意味する
PSH:FLUSH動作によって送信されたデータである
RST:接続を強制的に終了:異常終了
SYN:送信側と受信側で連番を確認しあう。これで接続どうさを表す
FIN:切断

ウィンドウ

「受信側から、送信側にウィンドウサイズ (受信確認を待たずにまとめて送信可能なデータ量) を通知するために使う。」 これが16bitであるのが、もんだいなんですよ!!

以上を念頭に次に進みます。

TCPが接続を開始して、データを送信し、切断するまで

はい。一言でいうと、 「TCPはスループットを犠牲に、信頼性を高めた通信プロトコル」です。 信頼性を高めるために、されている工夫が2つあるんですね。それが、 3ウェイハンドシェイクと、パケット送信時の受け取り確認です。 (UDPはスリーウェイハンドシェイクも、受け取り確認もしない)

ということで、3ウェイハンドシェイクから見ていきましょうか。 TCPでは、通信をするときに、通信相手とのコネクションが確立されていることを確認してから実際にデータを送信します。 この、コネクションを張る作業が、3way handshakeです。 通信を始める側 (クライアント) からサーバにまず、SYNのコントロールビットが1になっているヘッダーを送ります。この時、シーケンス番号の初期値と、ウィンドウサイズを渡しておきます。 クライアントから、接続要求を受けたサーバは、SYNのコントロールビットが1で、ウィンドウサイズとシーケンス番号の初期値をのせたヘッダーをクライアントに送り返します。 これを受け取ったクライアントは、ACK番号を送り返します。(ACKビットが立っている)これでスリーウェイハンドシェイクが終わりです。 この時点で、サーバとクライアントは、どちらからでもデータの送信ができるようになっています。

次に、実際にデータのやり取りをするところです。 データの受信側と送信側があります。 データの送信側は、シーケンス番号 + データを受信側に送ります。 受信側は、そのデータを受け取ったことをサーバに知らせるために、ACK番号を送信側に返します。 送信側からACK番号を受け取った受信側は、また次のパケットを送信します。 受信側で受け取るシーケンス番号がおかしい場合は、受信側は再要求できます。

以上を一回一回繰り返すことで、データのパケットを確実に届けることができますね。 ただ、これだとめっちゃ効率悪いですよね? アプリのデータを格納できる場所のことをMSSといいますが、MSSは最大で、1460バイトって決まっているんですよ。 で、例えば通信に10msのレイテンシーがかかるとして、上のように1460バイトごとにAck番号を送っていたら、例えば 64MiBのデータを送信するのに 6410241024/1460 * 10ms = 4596 secくらいかかってしまうんですね。やばいですよね?

これを防ぐために、受信側は、ACK番号を知らせるときに、一緒にウィンドウサイズも送信側に知らせます。 ウィンドウサイズは、「受信確認を待たずにまとめて送信可能なデータ量」でしたね。これは、受信側のバッファーの大きさで決まります。 これくらいのデータだったら一気に送っていいよ!って感じです。 ウィンドウサイズを受け取った送信側は、そのウィンドウサイズ以内で、複数のパケットを一気に送信することができます。 例えば、受信側から、2^15 = 32768 bitのウィンドウサイズが返ってきたとします。 この時、MTU = 1500 bitだとすると、 32768 / 1500 = 21 このパケットを一気に送れるようになります。

って感じです。以上が、windowサイズの説明です。 が、ですよ。ウィンドウサイズが16bitまでってちょっと小さくないですか?って話なんです。 2^16 = 65536なんですよ。64KBなんですよ。ってことで次の議論に進むわけです。

ウインドウサイズについての議論

TCPヘッダーのウィンドウフィールドは 「受信側から、送信側にウィンドウサイズ (受信確認を待たずにまとめて送信可能なデータ量) を通知するために使う」 わけですが。 これが16bitであるんですね。だいぶ小さいですよね。マジで。 2^16 / 1024/1024 0.0625MiB。 = 64KB。ほんと??小さすぎね? ってことで、[rfc1323] (https://datatracker.ietf.org/doc/html/rfc1323) でTCPについて、改訂されました。

The default TCP Window Size maximum is 64 kilobytes. However, it was realised long time ago that it is insufficient for today's networking requirements.

Therefore TCP Window Scaling was invented and documented in RFC 1323. It specifies a scaling factor starting from 1 and ending up to 16384. This means that maximum effective TCP Window Size can be 16384 * 65535 bytes = 1,073,725,440 bytes, that is, one gigabyte.

めっちゃ面白いので、のせておきます。

The TCP protocol [Postel81] was designed to operate reliably over almost any transmission medium regardless of transmission rate, delay, corruption, duplication, or reordering of segments. Production TCP implementations currently adapt to transfer rates in the range of 100 bps to 10**7 bps and round-trip delays in the range 1 ms to 100 seconds. Recent work on TCP performance has shown that TCP can work well over a variety of Internet paths, ranging from 800 Mbit/sec I/O channels to 300 bit/sec dial-up modems [Jacobson88a].

The introduction of fiber optics is resulting in ever-higher transmission speeds, and the fastest paths are moving out of the domain for which TCP was originally engineered. This memo defines a set of modest extensions to TCP to extend the domain of its application to match this increasing network capability. It is based upon and obsoletes RFC-1072 [Jacobson88b] and RFC-1185 [Jacobson90b].

There is no one-line answer to the question: “How fast can TCP go?”. There are two separate kinds of issues, performance and reliability, and each depends upon different parameters. We discuss each in turn.

そして、ロングファットパイプ問題:

TCP performance depends not upon the transfer rate itself, but
      rather upon the product of the transfer rate and the round-trip
      delay.  This "bandwidth*delay product" measures the amount of data
      that would "fill the pipe"; it is the buffer space required at
      sender and receiver to obtain maximum throughput on the TCP
      connection over the path, i.e., the amount of unacknowledged data
      that TCP must handle in order to keep the pipeline full.  TCP
      performance problems arise when the bandwidth*delay product is
      large.  We refer to an Internet path operating in this region as a
      "long, fat pipe", and a network containing this path as an "LFN"

例えば、通信路で100msの遅延が生じて、帯域幅が100MiB/sの時、 遅延と帯域幅のプロダクトは、0.1 * 100MiB/s = 10MiBなんだよね。最大でこれだけの量のデータが通信路に流れることができるわけですね。 なんでかって話なんだけど、TCP通信では、ACKを受け取ってからでないと送信側はデータ送信できないという話だったと思います。 そして、一度に送信できるデータ量はウインドウサイズで決まっていて、ん?待てよ?

遅延*帯域幅 <= ウィンドウサイズ

の時はどうなるのか??ちょっと忘れてしまった。また勉強しなおさないとな。

window, MTU, MSSを確認する方法

MTU = アプリのデータ + (TCPヘッダ + IPヘッダ) また、MSS = アプリのデータ です。TCPヘッダとIPヘッダの大きさは40バイトなので、MSSの最大値は40バイトですね。 Linux上でMTUを確認する方法ですが、

ip link show <network interface>

出力はこんな感じになりました。

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether 

続いて、windowサイズの確認ですが、これは、通信が実際にされている中で変わっていくもんだと思うので、なにかコマンドで見るのは難しいのではないかと思いました。 代わりに、ネットワークのソケットの情報を色々出してくれるコマンドを教えておきます。

ss -t -i

出力

cubic wscale:8,7 rto:250 rtt:49.746/23.257 ato:40 mss:1360 pmtu:1500 rcvmss:1360 advmss:1460 cwnd:10 ssthresh:12 bytes_sent:6020227 bytes_retrans:6868 bytes_acked:6013359 bytes_received:3783321 segs_out:89020 segs_in:71913 data_segs_out:51240 data_segs_in:49621 send 2.2Mbps lastsnd:1750 lastrcv:3850 lastack:1690 pacing_rate 2.6Mbps delivery_rate 6.3Mbps delivered:51235 busy:1185120ms retrans:0/6 reordering:6 reord_seen:392 rcv_rtt:23.668 rcv_space:71448 rcv_ssthresh:995636 minrtt:15.39

いろいろ書いてあっていいね。Mbpsも出ているのがすごいなって思いました。まあ、これはシーケンス番号 + データを投げてから帰ってくるまでの時間から簡単に出せるんだけどね。これを内部に保持していたとは驚きだ。 ネットワークのバンド幅によって何か、最適化とかしているのだろうか??気になるところではありますが。

で、実際にwindowサイズとMTUを確認するために、TCPダンプを使って、ほんとにそうなっているか見たいわけだ。

tcpダンプがどのレイヤーで実行されるかだけど、 「ルータ自作でわかるパケットの流れ」の本で見たけど、これは、データリンク層のレイヤーで行われることなんですよね。 つまり、イーサネットパケットを見てるんですね。

おまけ:tcコマンドでネットワーク遅延を変更させる方法

本筋からそれるので、参照だけで。

tcコマンドを使ってカーネル空間で、人工的にレイテンシーを発生させることができます。 必要なのは、network interface名。eno1 次のコマンドで制御できます。

sudo tc qdisc add dev <network interface> root handle 1:0 netem delay <追加したい遅延 in ms>

例えば、100msの遅延を発生させたいときは次のようにします。

sudo tc qdisc add dev eno1 root handle 1:0 netem delay 100ms

設定の解除方法はこちらです。

sudo tc qdisc del dev eno1 root

また、tsharkを使ってパケットダンプをする方法はこちらです。

sudo tshark -i eno1 -Y 'tcp.port==8080'