👏

Linux で TCP_DEFER_ACCEPT が有効でも accept 後 read できるとは限らない

2023/01/03に公開

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 接続が成立するのを遅らせる」と書いてあります。つまりこれを過ぎると、データが到着していなくても accept できてしまう場合があります。

tcp: accept socket after TCP_DEFER_ACCEPT period というパッチがこの動作を導入しました。要約すると次のようなコメントが書かれています。

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

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

    /* 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 という奇妙なことになります。

サーバー側でソケットが accept されないまま時間が経過すると、クライアントは送ったデータに対する ACK が返ってこないので、データを再送します。socket を accept できない場合、サーバーは ACK を送出せず、パケットを無視し続けます。

SYN_RECV のままサーバー側はタイムアウトを迎え、 SYN+ACK を再送します。クライアント側はそれに対応するACKを返送しますが、サーバーは TCP_DEFER_ACCEPT の設定回数が経過するまで、送られてきた ACK を無視します。そのうちクライアントは再送タイムアウトを迎え、データを再送します。それでも accept backlog がいっぱいだと再びパケットが落ちます。

TCP_DEFER_ACCEPT の設定回数を超えそうになったり、超えた状態でまだサーバーがソケットを accept できない場合、サーバー側のソケットは SYN_RECV 状態のままでタイムアウトし、SYN+ACK を送出します(Patch: tcp: reduce SYN-ACK retrans for TCP_DEFER_ACCEPT)。クライアントが対応する ACKを返し、さらにそのタイミングで accept backlog に空きがあった場合にサーバーが accept すると、ACK に対して accept したので、データの存在しないソケットができる事になります。この状態でクライアント、サーバーの両方の socket が ESTABLISHED になります。

クライアントは再送タイムアウトを迎えた後データを送信し、通常のフローになります。

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

Discussion