TCP 3-way handshakeのレイテンシ軽減のためのTCP_DEFER_ACCEPTソケットオプション

2013/10/22 追記した.

Starletのコード読んでてlistening socketにTCP_DEFER_ACCEPTとかいうオプション渡してたので、これ何だって思って調べた.
TCPに特に詳しいわけではないので理解に誤りがあるかもしれない.

package Starlet::Server;
...
    # set defer accept
    if ($^O eq 'linux') {
        setsockopt($self->{listen_sock}, IPPROTO_TCP, 9, 1) # 9がTCP_DEFER_ACCEPTを表す
            and $self->{_using_defer_accept} = 1;
    }
...

TCP_DEFER_ACCEPTはLinux 2.4から導入されている.
Linux 2.6.32から挙動が若干変わっているらしい. (linux の TCP_DEFER_ACCEPT (サーバサイド) の挙動について - kazuhoのメモ置き場)
ApacheとかNginxでも使われてるっぽい.

TCP_DEFER_ACCEPTを理解するのに下記の記事が参考になった.
Take advantage of TCP/IP options to optimize data transmission - TechRepublic

TCP_DEFER_ACCEPTが導入された動機

HTTPリクエストに必要なデータサイズは小さいため,HTTPリクエストは1個のパケット(DATAパケット)に収まる.
本当に必要なパケット(DATAパケット)は1個なのにTCPの3-way handshakeをやらないといけなくて,HTTPリクエストを受信するまでに必要なパケット数が4個になる.

これらの4個のパケットのせいで余分な遅延やオーバヘッドが発生してしまう.
例えば,3-way handshakeのfinal ACKがパケットロスしたときに,サーバはESTABLISHED stateに移行しない.
ESTABLISHEDになってないと,final ACKの次に送信されたDATAパケットがサーバ到着後にdropされたりする.
この場合は,サーバがSYS/ACKを再送してクライアントのACKを待つことになる.

TCP_DEFER_ACCEPTの挙動

listening socketかconnected socket(つまりサーバサイドかクライアントサイド)のどちらにTCP_DEFER_ACCEPTオプションをつけるかで挙動が変わる.

listening socketの場合は,final ACKを待たずに,DATAパケットを受信するまで,accept(2)をブロックする. (普通はfinal ACKを受信した時点でacceptは処理をユーザプロセスに戻す)
これにより,final ACKがパケットロスしても,DATAパケットさえ受信すればaccept(2)は成功する.
したがって,DATAパケットのロスが無ければSYN/ACKの再送を回避できる.

connected socketの場合は,サーバからSYN/ACKを受信した後すぐにACKを返さずに,ユーザプロセスがwriteを発行した時点でDATAパケットとACKを一緒にして返す.
これにより,やりとりするパケットを1個減らせる.

上記の挙動からわかるように,TCP_DEFER_ACCEPTの使用はクライアントがACKの送信後にすぐにデータを送信する,つまりクライアントから喋り始めるプロトコルであることが前提となっている.

雑感

SYN/ACKの再送問題を回避したい(他にもあるかもしれない)っていうTCP_DEFER_ACCEPTの目的がすぐに理解できなかった.
acceptをブロックするとか動作の内容は書いてあるけど,何のためにそんなことをするのかを直接的に説明した文章が見つからなくてつらい感じだった.

参考

追記

Starletの作者であるkazuhoさんやkazeburoさんからコメントいただきました.