ネーグルアルゴリズムとsetsockopt
研究でTCPを使ってリアルタイムな処理を行おうとして詰まったので残しておきます。
servとrecv側で必ず等間隔のラグがある?
ある日研究で小さいパケット(20 ~ 50byteくらい大きさ)を, 短時間に何回も送信するような実験をすることになりました。
実験の前提としてリアルタイムかつデータに抜けがあってはならないという条件があったため, CAT6のLANケーブルをPC間に直刺しして,
通信部分は以下に示したようなコードを書いて実験に臨みました。(エラー処理とかは取り除いています。)
クライアント
#define SIZE 26 // TCP通信を指定 char data[SIZE]; for (int i = 0; i < 26; ++i) { data[i] = 'a' + i; } send(sock, data, SIZE, 0);
サーバ
#define SIZE 26 // TCP通信を指定 char data[SIZE]; recv(sock, data, SIZE, 0);
しかし上記のコードを何回実行しても必ずsendしてからrecvするまで、約0.4secほどの遅延が生じてしまいました。
これはおかしい、と思いローカルで実行して遅延はほとんど変わらず...
いろいろ調べた結果、次の記事に辿り着きました。
https://www.ibm.com/developerworks/jp/linux/library/l-hisock/index.html
TCP ソケットを使用して通信する場合、やり取りされるデータはブロック単位に分割され、通信用の TCP ペイロードに格納されます。TCP ペイロードのサイズはいくつかの要因 (パスに応じた最大パケット・サイズなど) によって決定されますが、通信が開始されるまでこれらの要因を特定することはできません。最高の性能を実現するには、それぞれのパケットにできるだけ多くの使用可能データを格納することが必要です。ペイロード内に十分なデータがない場合 (ペイロードのサイズが最大セグメント・サイズ (MSS) になります)、TCP は Nagle アルゴリズムにより、複数の小さなバッファーを自動的に連結して 1つのセグメントを作成します。これにより、アプリケーションの処理効率を上げ、小さなパケットの送信数を最小限に抑えてネットワーク全体の混雑を緩和します。
これが原因っぽい。
ネーグルアルゴリズム
Nagle(ネーグル)アルゴリズムとは、複数の小さいパケットを連結して1つの大きなセグメントにしてから送信するというものです。
こちらの記事(http://hayachi617.blogspot.com/2014/06/tips_7.html)にもある通り、データをTCP/IPを使用して送信すると必ずTCPパケットとIPパケットが送信されるデータにはくっついています。しかし、短期間に小さいデータを大量に送るとTCPパケットとIPパケットによる伝送路へのオーバーヘッドはバカになりません。最悪輻輳が発生し、ネットワークが大渋滞になってしまいます。
この問題を解決するために小さなパケットはTCPの最大セグメント長になるまで連結して送信するというネーグルアルゴリズムが提案されました。
WireSharkなどで確認したわけではありませんが自分の環境ではMTU(通信機器が一度に送信できる最大のデータ量)が1500Byteだったため、最大セグメント長は1460Byteほどであったと思われます(参考 https://milestone-of-se.nesuke.com/nw-basic/grasp-nw/mtu-mss-fragment-segment/)。ネーグルアルゴリズムでは、一定時間内に最大セグメント長に達しない場合はそこまで連結したパケットを送信することになっています。そのため実験では送信するデータが最大セグメント長に達せず、設定されていた一定時間(0.4sec)で送信されていたと考えられます。
といってもこの機能は小さいパケットをそのまま送信したいときには邪魔になるだけです。従ってsetsockopt関数でネーグルアルゴリズムを無効かする必要があります。
TCP_NODELAY
setsockoptはソケットに関連付けられるオプションを設定できる関数です。ここにソケット記述子とTCP_NODELAYを指定することでネーグルアルゴリズムを無効にできます。(参考: https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.3.0/com.ibm.zos.v2r3.hala001/setopt.html)
TCP_NODELAYを設定したコードが以下になります。
#define SIZE 26 char data[SIZE]; for (int i = 0; i < 26; ++i) { data[i] = 'a' + i; } // TCP_NODELAYを設定 int ret = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&flag, sizeof(flag)); if (ret == -1) { perror("client setsockopt\n"); exit(1); } send(sock, data, SIZE, 0);
これにより小さいパケットをそのままサーバに送信できるようになりました。
まとめ
- 輻輳制御のため、小さいパケットを連結してから送信するネーグルアルゴリズムが使われている。
- 小さいパケットをそのまま送信したいときはTCP_NODELAYを指定して、ネーグルアルゴリズムを無効にする必要がある。
何か間違いがありましたらコメントいただけると嬉しいです。