net.ipv4.tcp_tw_recycle は廃止されました ― その危険性を理解する

# Disclaimer

私はネットワークの勉強もちゃんとしたことないし、Linux のソース読むのもはじめてな素人です。

何かおかしなところなどあれば、遠慮なくコメント欄でまさかりをお願いいたします。

## ソースコードの引用に関して

本文中で Linux のコード／ドキュメントを引用している箇所がありますが、すべてタグ `v4.11` のものです。また、日本語のコメント・翻訳文は筆者が入れたものです。

# TL; DR

Linux のカーネルパラメータ `net.ipv4.tcp_tw_recycle` は、バージョン4.12から廃止されました。
今後はこの設定は行わないようにしましょう（というかできません）。

一方、`net.ipv4.tcp_tw_reuse` は安全であり、引き続き利用できます。

…というだけの話なのですが、自分用にメモがてら経緯・背景などを記録しておきます。

# なんで気がついたか

このパラメータは多数の Fluentd インスタンスを扱うときの推奨設定として[公式ドキュメントでかつて紹介されていた](https://docs.fluentd.org/v1.0/articles/before-install) [^1] ものでした。

[^1]: 現在、当該ドキュメントからは削除済みです: [fluent/fluentd-docs `6ca2a90`](https://github.com/fluent/fluentd-docs/commit/6ca2a90fd47527a0f7beee3bb121bd67434b6bdd#diff-86a05223d6a24405fa7d59b1e39ed38a) _（2018/01/05 更新）_

> For high load environments consisting of many Fluentd instances, please add these parameters to your `/etc/sysctl.conf` file. Please either type `sysctl -p` or reboot your node to have the changes take effect. If your environment doesn’t have a problem with TCP_WAIT, then these changes are not needed.
> 
> ```
> net.ipv4.tcp_tw_recycle = 1
> net.ipv4.tcp_tw_reuse = 1
> net.ipv4.ip_local_port_range = 10240    65535
> ```

自分のチームでもこれに倣ってこの設定を Ansible のプレイブックに記載していました。

```yaml
- name: update sysctl to avoid TCP_WAIT problem
  sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
    sysctl_set: yes
  with_dict:
    net.ipv4.tcp_tw_recycle: 1
    net.ipv4.tcp_tw_reuse: 1
    net.ipv4.ip_local_port_range: 10240    65535
```

ところがあるとき、そのプレイブックをもとに新しいマシンイメージを作ろうとしたら、下記のようなエラーが出てプロビジョンが失敗してしまったのです。

```
sysctl: cannot stat /proc/sys/net/ipv4/tcp_tw_recycle: No such file or directory
```

# 何が起きていたのか

Linux のカーネルバージョン v4.12 から、この設定項目がそもそも廃止されていました。

コミット: 
**tcp: remove tcp_tw_recycle** `4396e46`
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4396e46187ca5070219b81773c4e65088dac50cc

自分のケースでは Ubuntu 16.04 LTS (Xenial) をずっと使っていたのですが、急に新しいイメージのビルドができなくなり、原因が全然分からず数時間溶かしました。
まさか Ubuntu の同じバージョン (16.04.3) の中でカーネルバージョンが変わる [^2] なんて思っておらず……（おそらく LTS だけだと思いますが）

[^2]: GCE でいうと `ubuntu-1604-lts` ファミリーの `ubuntu-1604-xenial-v20171208` から該当します

ちなみにお使いの Linux のバージョンの確認には、`uname` コマンドが使用できます。

```
$ uname -r
4.13.0-1002-gcp
```

# なぜ廃止されたのか

廃止の経緯を理解するためには、そもそも `net.ipv4.tcp_tw_recycle` というのが何を調整するパラメータなのかを知る必要があります。
順を追って見ていきましょう。

## TCP の TIME_WAIT

まずは典型的な TCP の接続終了方法を見てみます。

![tcp_close.png](https://qiita-image-store.s3.amazonaws.com/0/105378/0fdbd3be-c669-9faa-6692-ab058bedfbd9.png)


注目してほしいのは送信者側（アクティブクローズ側）です。受信者側の `FIN` （＝「こっちも終わりまーす」）に対して `ACK` を返したあと、**すぐに `CLOSED` 状態に移ってはならず、しばらく（具体的にはMST（最大セグメント生存時間）の2倍）待つ**ことが定められています。

これは、ネットワーク上で起きうるパケットロストや遅延に対し、同じ送信者・受信者間で行われた別の接続が混ざらないようにするための工夫です。

しかしこの `TIME_WAIT` 状態というのはただの「待ち」状態であるため、高負荷環境下ではできるだけ減らしたいという気持ちになりますね。

## `net.ipv4.tcp_tw_{reuse,recycle}`

問題はここからです。

Linux にはこの挙動を変えるための独自の設定項目が2つあります。
それが `net.ipv4.tcp_tw_reuse` と `net.ipv4.tcp_tw_recycle` です。

どちらも、「ある特定の条件を満たせば、 `TIME_WAIT` 状態のソケットをすぐに新しい接続に使いまわす」という設定です。

### `net.ipv4.tcp_tw_reuse`: より安全な設定

[マニュアル](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/networking/ip-sysctl.txt?h=v4.11#n648)には下記のように書かれています。

> Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. Default value is 0. It should not be changed without advice/request of technical experts.

> プロトコル的観点から見て安全な場合に、TIME-WAIT ソケットを新しい接続に再利用することを許可する。既定値は0。 専門家からのアドバイス・要請なしには変更すべきではない。

ポイントは "when it is safe from protocol viewpoint" という箇所でしょうか。
これがどういうことか、具体的に見ていきましょう。

`net.ipv4.tcp_tw_reuse` を有効にすると、 `TIME_WAIT` 状態のソケットでもタイムスタンプが1秒でも新しければ、新しい接続で再利用できるようになります。

In [`net/ipv4/tcp_ipv4.c`](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/ipv4/tcp_ipv4.c?h=v4.11#n105):

```c:net/ipv4/tcp_ipv4.c
int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
	/* ……省略…… */
	if (tcptw->tw_ts_recent_stamp &&
	    (!twp || (sock_net(sk)->ipv4.sysctl_tcp_tw_reuse &&
			     get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
		/* ……省略…… */
		return 1;
	}

	return 0;
}
```

ただしすぐに再利用されるというわけではなく、外向きの接続で、なおかつ接続先が同じ場合に限られます。

In [`net/ipv4/inet_hashtables.c`](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/ipv4/inet_hashtables.c?h=v4.11#n315):

```c:net/ipv4/inet_hashtables.c
static int __inet_check_established(struct inet_timewait_death_row *death_row,
				    struct sock *sk, __u16 lport,
				    struct inet_timewait_sock **twp)
{
	/* ……省略…… */
	sk_nulls_for_each(sk2, node, &head->chain) {
		if (sk2->sk_hash != hash)
			continue;

		if (likely(INET_MATCH(sk2, net, acookie,
					 saddr, daddr, ports, dif))) {
			if (sk2->sk_state == TCP_TIME_WAIT) {
				tw = inet_twsk(sk2);
				if (twsk_unique(sk, sk2, twp))
					break;
			}
			goto not_unique;
		}
	}
	/* ……省略…… */
}
```

接続が外向きに限られること、なおかつ比較対象のタイムスタンプを付与しているのが自マシンであることから、この処理は "safe from protocol viewpoint" になっているんですね。

### `net.ipv4.tcp_tw_recycle`: よりアグレッシブな設定

[マニュアル](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/networking/ip-sysctl.txt?h=v4.11#n643)には下記のように書かれています。

> Enable fast recycling TIME-WAIT sockets. Default value is 0. It should not be changed without advice/request of technical experts.

> TIME-WAIT ソケットの高速なリサイクルを有効にする。既定値は0。専門家からのアドバイス・要請なしには変更すべきではない。

これだけ読んでもよく分かりませんね。
先ほどとの違いでいえば、safe かどうかに関する記述がなくなったのと、"allow to reuse" が "enable recycling" に変わったくらいでしょうか。

こちらもソースで具体的な動きを確認してみましょう。

In [`net/ipv4/tcp_input.c`](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/ipv4/tcp_input.c?h=v4.11#n6277):

```c:net/ipv4/tcp_input.c
int tcp_conn_request(struct request_sock_ops *rsk_ops,
		     const struct tcp_request_sock_ops *af_ops,
		     struct sock *sk, struct sk_buff *skb)
{
	/* ……省略…… */
	if (!want_cookie && !isn) {
		/* ……コメント省略…… */
		if (net->ipv4.tcp_death_row.sysctl_tw_recycle) {
			bool strict;

			dst = af_ops->route_req(sk, &fl, req, &strict);

			if (dst && strict &&
			    !tcp_peer_is_proven(req, dst, true,
						tmp_opt.saw_tstamp)) {
				NET_INC_STATS(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
				goto drop_and_release;
			}
		}
		/* ……省略…… */
		isn = af_ops->init_seq(skb, &tcp_rsk(req)->ts_off);
	}

	/* ……省略…… */

drop_and_release:
	dst_release(dst);
drop_and_free:
	reqsk_free(req);
drop:
	tcp_listendrop(sk);
	return 0;
}
```

ざっくり言うと、パケットのタイムスタンプを `TIME_WAIT` に入ったときの最後のものと比較し、古ければパケットをドロップ、新しければ新しい接続にそのまま使うという動きです。

`tcp_tw_reuse` の場合と違い、**内向きのリクエストであっても即、古いと見なされたパケットをドロップしてしまう**というのが "recycle" という単語に込めたニュアンスなのでしょうか。

タイムスタンプはリモート側でパケットに付与されるもので、unixtime などの絶対時間ではなく uptime （マシンの起動時間＝相対時間）です。
ということは、**そのタイムスタンプが単調に増えていくという前提が成り立たない環境**では、この処理が "safe" とはとても言えませんね。

この「前提が成り立たない環境」というのは実はそれほど珍しいものではなく、従来で一番多いケースだと NAT や LB の背後にあるサーバと通信する（＝1つのソース IP に複数マシンが紐づく）場合が該当します。

### （蛇足） `net.ipv4.tcp_fin_timeout` は `TIME_WAIT` の待ち時間を調節するものではない

この辺の話を日本語でググると、`net.ipv4.tcp_fin_timeout` が `TIME_WAIT` の待ち時間を調節するものだと書かれている記事を見つけられるかもしれません。

ですが、[マニュアル](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/networking/ip-sysctl.txt?h=v4.11#n284)にある通りこれは誤りで、実際は `FIN_WAIT_2` のタイムアウト設定です。

> The length of time an orphaned (no longer referenced by any application) connection will remain in the FIN_WAIT_2 state before it is aborted at the local end. While a perfectly valid "receive only" state for an un-orphaned connection, an orphaned connection in FIN_WAIT_2 state could otherwise wait forever for the remote to close its end of the connection. Cf. tcp_max_orphans Default: 60 seconds

> 孤立した（どのアプリケーションからも参照されていない）接続が、ローカル側で中断されるまでに FIN_WAIT_2 状態で待ち続ける時間。FIN_WAIT_2 状態は、孤立してない接続にとっては全く問題のない「受信オンリー」状態だが、孤立した接続においてはこの設定なしではリモートが接続を終了するのを永遠に待つことになってしまう。tcp_max_orphans も参照。既定値: 60秒

`TIME_WAIT` の待ち時間は `TCP_TIMEWAIT_LEN` という定数で設定されており、ソースをリコンパイルしない限り変更できない定数です。つまり、これは**調整すべき値ではない**ということです。

In [`include/linux/tcp.h`](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/net/tcp.h?h=v4.11#n116):

```c:include/linux/tcp.h
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
				  * state, about 60 seconds	*/
```

## timestamp offsets のランダム化

「タイムスタンプが単調に増えていくという前提が成り立たない環境」で、従来一番多かったのは NAT や LB を使っている場合だと前述しました。

この状況は Linux v4.10 から変わりました。

どう変わったか。実は、「あらゆる状況で」その前提が成り立たなくなったのです。
これは、下記の変更によりコネクションごとにタイムスタンプがランダム化されるようになったためです。

コミット:
tcp: randomize tcp timestamp offsets for each connection `95a22ca`
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=95a22caee396cef0bb2ca8fafdd82966a49367bb

もはやこうなっては、`tcp_tw_recycle` はどんな状況でも設定すべきでない項目ですね。
そんなわけで、廃止されるに至ったというわけです。

# まとめ

`tcp_tw_recycle` オプションは従来でも NAT/LB 利用など一部ケースで不適だったが Linux v4.10 からあらゆる状況で不適になり、結局 v4.12 で廃止されました。

また、`TIME_WAIT` の待ち時間を決める `TCP_TIMEWAIT_LEN` は調整すべき値ではありません。

一方 `tcp_tw_reuse` は安全に `TIME_WAIT` ソケットの再利用効率を高めてくれるので、 `TIME_WAIT` に悩んでいる場合はこの項目のみを設定しましょう。

（`tcp_fin_timeout` については危険というわけではないですが、`TIME_WAIT` とは関係ないため、今回は深入りしませんでした）

# 参考リンク

- [kernel/git/torvalds/linux.git - Linux kernel source tree](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4396e46187ca5070219b81773c4e65088dac50cc)
    - 今回の変更のコミット
- [Coping with the TCP TIME-WAIT state on busy Linux servers | Vincent Bernat](https://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux)
    - TIME-WAIT 周りの詳しい解説記事（英文）
- [ぜんぶTIME_WAITのせいだ！ - Qiita](https://qiita.com/kuni-nakaji/items/c07004c7d9e5bb683bc2)
    - TIME-WAIT 周りの日本語での解説記事
- [tcp_tw_なんとかの違い - Qiita](https://qiita.com/smallpalace/items/b0b351cebc453287e10e)
- [「net.ipv4.tcp_tw_recycle」を有効にするのは（場合によっては）やめた方がいい - pullphone's blog](http://d.hatena.ne.jp/pullphone/20120511/1336722675)
- [Linuxカーネルの「TCP_TIMEWAIT_LEN」変更は無意味？ - Togetter](https://togetter.com/li/871768)
    - tcp_tw_reuse オプションに関する識者の皆さまのツイート（一部元ツイートは削除済みの模様）
- [Transmission Control Protocol - Wikipedia](https://en.wikipedia.org/wiki/Transmission_Control_Protocol)
    - Wikipedia 先生の TCP の解説。2018年1月3日閲覧。
- [TIME_WAITに関する話](https://www.slideshare.net/takanorisejima/timewait) _（2018/01/05 追加）_
    - TIME_WAIT （というか EADDRNOTAVAIL）にどう対処するかが紹介されている日本語スライド（tcp_tw_recycle の話は載っていないですが）