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 インスタンスを扱うときの推奨設定として公式ドキュメントで紹介されていた 1 ものでした。

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 のプレイブックをに記載していました。

- 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 だけだと思いますが)

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

$ uname -r
4.13.0-1002-gcp

なぜ廃止されたのか

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

TCP の TIME_WAIT

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

tcp_close.png

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

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

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

net.ipv4.tcp_tw_{reuse,recycle}

問題はここからです。

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

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

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

マニュアルには下記のように書かれています。

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:

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:

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: よりアグレッシブな設定

マニュアルには下記のように書かれています。

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 recyling" に変わったくらいでしょうか。

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

In net/ipv4/tcp_input.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_timeoutTIME_WAIT の待ち時間を調節するものではない

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

ですが、マニュアルにある通りこれは誤りで、実際は 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:

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 とは関係ないため、今回は深入りしませんでした)

参考リンク


  1. 当該ドキュメントは修正済みです: fluent/fluentd-docs#394 

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