はじめに

インターネットには多数のデバイスが存在しており、その多数のデバイスが通信し合うために通信プロトコルが定義されている。 様々な通信プロトコルを勉強していくと、ネットワーク内の通信が正常に行えるためにはそのネットワークの参加者全員が通信プロトコルを守っていることが必要になることに気がつく。

たとえば、TCP の通信が失敗したときの再送の間隔について説明する。 今回はわかりやすく普通のサーバー・クライアントモデルの Web サービスを考え、サーバー対してクライアントから大量のアクセスが集まりそのリソースが枯渇した状況を考える。 サーバーはリソースの枯渇により全てのクライアントと TCP のコネクションを張れないことから、一部クライアントからのアクセスを拒否する。 拒否されたクライアントは再度サーバーに対してアクセスしようと試みる。 これをアクセス出来るまでクライアントは繰り返すわけだが、その間隔は実は一定ではない。 最初は 1 秒程度で素早く再送するが、サーバーが何度もアクセスを拒否すると、クライアントの方で自動で再送の間隔を指数関数的に伸ばしてアクセスするようになっている。 このような仕様になってる理由は、全員が間隔を開けずに即座に繰り返し通信を試みた場合はサーバーのリソースは一生枯渇したままで回復しない可能性があるためである。 一方でクライアント側で指数関数的に間隔を伸ばしていくと、サーバーがアクセスを拒否し続ければいつかはリソースが回復し少しクライアントの通信を捌けるようになる。 そのため、TCP ではアクセスに失敗したときクライアント側で再送する間隔を指数関数的に引き伸ばしていくというプロトコルになっているのである。

ここで注目したいのが、TCP の再送間隔の調整はクライアントで行われるという点である。 再送の間隔を極端に短くして再度送信するようなデバイスを作ったとしたら、そのデバイスはネットワークの中で唯一得することが出来るのである。 囚人のジレンマの言葉で言えば、現在のネットワークは全員が黙秘しているパレート最適な状態であるから、自分だけ自白することで自分だけの利得を最大化出来るのである。 (世界中の数百億台というディバイスでパレート最適な状態を保っているというのは考え深いものがある。) このようなインターネットのプロトコルのパレート最適性を裏切る方法は TCP の再送時間以外にも探せばいくつもあるだろう。 この記事では TCP の再送間隔について Linux カーネルを弄って実験をする。

実験条件

OS: Ubuntu 22.04.1 LTS Linux kernel version: 5.15.73

Server と Client の IP アドレスは

  • Server: 192.168.0.10
  • Client: 192.168.0.20

とする。

TCP がアクセスを再試行する回数は/etc/sysctl.conf内に、

net.ipv4.tcp_syn_retries = 5

を書くことで調整が可能である。(この場合は 5 回リトライする)

つぎに、TCP の再送時間の計測方法を説明する。 まず Server で 23 番の TCP ポートを塞ぐ。 クライアント側で Server に telnet でアクセスを行う。

$ time telnet 192.168.0.10
Trying 192.168.0.10...
telnet: Unable to connect to remote host: Connection timed out

real    1m4.605s
user    0m0.003s
sys     0m0.000s

今回は、5 回のリトライで 1 分 4.6 秒かかったことがわかる。

そのままのカーネルで再送間隔を計測

以上のやり方で再送回数と時間の計測を行った。

再送回数時間
13.041s
27.266s
315.404s
431.399s
51m4.605s

指数関数的に増大していることがわかる。 さて次は Linux kernel の書き換える方法を説明し再送時間を弄ってみます。

カーネルのビルド方法

まず、以下からカーネルのソースを取ってくる。

https://www.kernel.org/

私が取ってきたのはlinux-5.15.73.tar.xz

解凍

$ tar xfv linux-5.15.73.tar.xz

Ubuntu の kernel の config を取ってくる。 make localmodconfigはエンター連打。

$ cp /boot/config-5.15.0-50-generic .config
$ make localmodconfig

適当にccacheをリセットする。

$ ccache -Cz

Linux kernel をビルドしてインストール

$ ccache make -j8
$ make modules_install
$ make install

再起動すればソースからビルドされて Linux kernel を使えるようになる。

Linux カーネルの書き換え

次に、Linux カーネルを書き換えて再送間隔を短くする。

Linux カーネルをどこを書き換えるかが問題になるが、結論から言うと以下の行を 1 行書き換えるだけである。

https://github.com/torvalds/linux/blob/v5.15/include/net/tcp.h#L139

変更前

#define TCP_RTO_MAX	((unsigned)(120*HZ))

変更後

#define TCP_RTO_MAX	((unsigned)(1*HZ))

TCP_RTO_MAXは再送間隔の最大値を指定しており、指数関数的に大きくなると言ってもどこかで切らなきゃいけないわけで、その切る値を定義していると解釈している。 わたしも TCP のコードの全てを理解したわけではなく、ただこの変数が怪しかったので書き換えてみたら再送間隔が短くなった程度の理解である。 またこれを見つけるまでにはこのブログ記事が大変参考になった。

弄ったカーネルで再送間隔を計測

以上のやり方で再送回数と時間の計測を行った。

再送回数時間
12.036s
23.079s
34.091s
45.118s
56.148s

再送間隔が等差数列になった。 つまり TCP の再送間隔の裏切りに成功したのである。

まとめ

  • ネットワークプロトコルをゲーム理論的視点から考察してみると面白いと誰かから聞いたので一番簡単そうな TCP の再送間隔で実験してみた。
  • wikipedia の TCP の記事は相当詳しくて Linux kernel のソースを見て書いたんだなぁとか、Linux kernel の細かいコメントが意外と色々書いてあることとかわかった。
  • Linux kernel のビルドは久々にやった。ccache 使うと早くビルド出来るのは良いが、デバッグするたびに PC 再起動するのがめんどくさい。再起動せずに開発するやり方あるのかな。
  • その他のプロトコルについても、パレート最適に落ち着いてて裏切ると自分だけ得できるパターンがあるので、kernel の書き換え場所を見つけ次第記事にしたい。