2014年5月8日木曜日

LinuxでTCP_DEFER_ACCEPTが有効でもaccept後readできない理由

listen()のbacklogが不足した際のTCP_DEFER_ACCEPTの動作について - blog.nomadscafe.jpという記事の中で、listen backlog があふれた後に accept(2) すると、その後の read(2) が EAGAIN を返したり、接続が不安定になるという事象が説明されていました。気になったので調べてみたことをまとめます。

結論から言うとこれはLinuxの仕様です。manのtcp(7)を見ると、

TCP_DEFER_ACCEPT (since Linux 2.4)

Allow a listener to be awakened only when data arrives on the socket. Takes an integer value (seconds), this can bound the maximum number of attempts TCP will make to complete the connection. This option should not be used in code intended to be portable.

「TCP_DEFER_ACCEPTで指定した秒数を過ぎるまではacceptを遅らせるよ!」と書いてあります。つまりこれを過ぎると、データが到着していなくてもacceptできてしまう場合があります。

なんでこんなことになっているのか、パッチを見てみましょう。tcp: accept socket after TCP_DEFER_ACCEPT periodというパッチにヒントがあります。要約すると

タイムアウトでSYN-ACKを再送するたびにACKを返してくるクライアントに対しては、TCP_DEFER_ACCEPTで指定した秒数を過ぎたらESTABLISHEDにしたほうが良い。そうしたらサーバープログラムがもっと待つか、エラーとして接続を閉じるか選ぶことができる。副作用としてはaccept(2)したら必ずデータがあると思ってるアプリケーションに対しては副作用があるけど、もともとTCP_DEFER_ACCEPTが有効でも無効でも動作するように設計するほうが良いよね。

とあります。

そもそもTCP_DEFER_ACCEPTはどのような動作になっているのでしょうか? tcp_minisocks.c の tcp_check_req() を見ると、単にACKを落とすだけの処理になっています。

    /* While TCP_DEFER_ACCEPT is active, drop bare ACK. */
    if (req->num_timeout < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&
        TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
        inet_rsk(req)->acked = 1;
        NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDEFERACCEPTDROP);
        return NULL;
    }   

この状態でクライアントが次にデータを送ってくると、そのままSYN_RECVからESTABLISHEDにしてしまうようになっています。言い換えると、SYN_RECVの状態のソケットでもデータを受信する場合があり、ESTABLISHEDな状態になるということです。ここでaccept backlogがいっぱいだとlisten overflowになり、だまってパケットが落とされます。この時サーバーはSYN_RECVのままですが、クライアントはESTABLISHEDという奇妙なことになります。サーバー側はタイムアウトを迎えSYN+ACKを再送します。クライアント側はそれに対応するACKを返送しますが、やっぱり無視されます。そのうちクライアントは再送タイムアウトを迎えデータを再送します。それでもaccept backlogがいっぱいだと再びパケットが落ちます。そのうち指定秒数過ぎた後にSYN+ACKがサーバー側から再送(この時点ではソケットの状態はSYN_RECV)されてきてクライアントがACKを返し、さらにそのタイミングでaccept backlogに空きがあった場合にデータの存在しないソケットができる事になります。クライアントは再送タイムアウトを迎えた後データを送信します。

追記: ブコメの指摘で、頻繁にSYN+ACK を再送するのは古い動作だということがわかりました。Patch: tcp: reduce SYN-ACK retrans for TCP_DEFER_ACCEPT この場合、SYN+ACKが最初に再送されるのはTCP_DEFER_ACCEPTでの指定秒数が経過してからとなります。

というわけで、 TCP_DEFER_ACCEPT はあくまで「遅らせる」だけなので、accept(2)すれば必ずread(2)できるということを保証しないということでした。