目次
概要
backlog=number sets the backlog parameter in the listen() call that limits the maximum length for the queue of pending connections. By default, backlog is set to -1 on FreeBSD, DragonFly BSD, and macOS, and to 511 on other platforms.
nginxのlistenに設定できるbacklogが具体的に何をしているのかを理解するためのメモ記事。
tcpソケットプログラミングとは
tcpを使ったプログラムは以下のような処理を経て通信を行う。(アーキテクチャによって細部は異なるがやることはほぼ一緒)。ここでポイントなのがTCPの処理はOSでやってくれてるという点です、ユーザプロセス的にはソケットに対してIOしてればあとは通信を行うことが可能となってます。(raw_socketとか使って自分でやることも可能)。
サーバ側
socket bind listen accept write
の順番。
sock0 = socket(AF_INET, SOCK_STREAM, 0); # socket作って addr.sin_family = AF_INET; addr.sin_port = htons(12345); addr.sin_addr.s_addr = INADDR_ANY; bind(sock0, (struct sockaddr *)&addr, sizeof(addr)); #ポート番号などをソケットに割り当てる listen(sock0, 5); # ソケットを接続待ちの状態にする while (1) { len = sizeof(client); sock = accept(sock0, (struct sockaddr *)&client, &len); write(sock, "HELLO", 5); }
3way-handshakeと状態遷移
ざっくり振り返る。今回はSYN_RECVEDとESTABLISHEDの間くらいで何が起きてるのかを把握していく
backlogとは
サーバアプリケーションがlistenしているソケットがacceptしていない、確立済TCPセッションを何個までOS側で保持するかを定義したもの。backlogはlisten(2)する時に指定できるものでアプリケーション次第ではパラメータでいじれなくなっている場合もある。nginxやapacheは特に問題なくいじることができる。
syn-queueとaccept-queueというものが存在して前者はハッシュテーブル、後者はFIFOキューで実装されています。backlogを4096と設定したらsyn-queueもaccept-queueも4096までそれぞれ設定されます。
2つのキューのイメージは以下のようになっています。
syn-queue
SYN_RECEIVED状態のソケットの数。net.ipv4.tcp_max_syn_backlog
という数字とaccept-queue
の数から割り出される。
最初のsynがクライアントから来てsyn/ackを返しackが来るまでがここの値になる、syn/ackのリトライはnet.ipv4.tcp_synack_retries
までする。
どうやったら溢れるか
SYN flood攻撃を受けたら溢れてしまう可能性がある。SYN flood攻撃とはざっくりいうとsynを大量に発行するクライアントがいたとしてsyn/ackを返してもそれに対するackを返さないような攻撃。IPスプーフィングとかsyn/ackを落とすファイアウォール設定をするとかで簡単に行える。(hping3とか使えば負荷テストもできる)
あとはsynが来たけどsyn/ackを返せないくらいサーバが高負荷になっている状況だとsyn_recvの値は増え続けていく。外部からの接続速度が非常に大きい場合に起きるかもしれないがそういう状況に遭遇したことはないのでよくは分からない...
溢れたらどうなるか
2パターン存在する。今のLinuxだとnet.ipv4.tcp_syncookies
がデフォルトで有効になっていると思うのですがとりあえず無効な場合のパターンも書く。TCP SYN flood攻撃を防ぐために開発された手法のひとつで、サーバ側は SYN を受けとった直後に記憶領域を消費するのではなくsyn/ackに対するackが来て初めて TCP 接続用の領域を割り当て接続を確立させていくというもの。
net.ipv4.tcp_syncookies
が無効
net.ipv4.tcp_syncookies = 0の場合、SYN_RECVは以下のいずれかのうち、小さい方の数まで登録できる。
これらを超えると新たにSYN_RECVは登録せず廃棄する。このため、新たにTCP接続を確立できない。synはdropされ続けクライアントの実装次第ではあるがいわゆる繋がりにくいという状況に陥る。
synのdropはnetstatの統計情報を見るコマンドとかで確認できる。
$ netstat -s TcpExt: ... 1380 times the listen queue of a socket overflowed 1380 SYNs to LISTEN sockets dropped ...
net.ipv4.tcp_syncookies
が有効
SYN_RECVはsyn backlogのサイズまで登録される。SYN_RECVがsyn backlogのサイズを越えると、SYN Flood攻撃と判断し、SYN cookiesの動作が始まる。動作がされていればSYN flood攻撃を受けていようが新規接続で接続問題は起きなくなる。
accept-queue
Acceptキューには、完全に確立された接続が含まれます、ESTABLISHなソケットとして存在してaccept(2)をアプリケーションが呼び出すことでAcceptキューから削除される動きとなります。
それぞれの設定値はそれぞれ個別に管理することも可能ですが多くは同値が設定されると思います。(net.ipv4.tcp_max_syn_backlog
などを意図的に小さく設定することはあまりなさそう??)
どうやったら溢れるか
OSが3way-handshakeを完了させたソケットをユーザプロセスがaccept(2)しないと溢れます、つまりaccept(2)を呼び出す暇もないくらいプロセスが忙しい(IOバウンドでもCPUバウンドでも)場合は溜まり続けます。最初にあげたサーバサンプルに追記をして重い処理を実行している時にaccept(2)を呼び出せずにOSがESTABLISHな接続を量産したらaccept-queueは溢れてしまいます。トラフィックが多い環境だと溢れる可能性があります。
while (1) { nanka_omoi_shori(); # ここで刺さっている時にOSがhandshakeしまくったら sock = accept(sock0, (struct sockaddr *)&client, &len); write(sock, "HELLO", 5); }
あとはメインプロセスがacceptを行い子プロセスへクライアントからの接続ソケットが取得できたら、そのファイルディスクリプタを渡すというディスクリプタパッシングという手法を用いたtcpサーバの場合はメインプロセスが忙しいだけの場合は容易に溢れが起きてしまう。
(メモ)Nginxのアーキテクチャ
Nginxはイベント駆動のアーキテクチャ、単一のリスニングソケットがワーカーに着信接続について通知し、各ワーカーは接続を取得しようとします。nginxは epoll_waitを利用して、イベントの発生を待ちイベントが発生したら処理を行うというモデルでaccept(2)がredyになるっていうイベントはこのepoll_waitで拾われ処理が行われる仕組みになっています。
溢れたらどうなるか
tcp_abort_on_overflow=1
TCP/IPスタックはRSTパケットで返答し、接続はSYNキューから取り除かれる。
tcp_abort_on_overflow=0
スタックは接続をACK済みとみなし、すなわち、このACKパケットは無視され、接続はSYNキュー(SYN_RECV)のままにしておく。その後すぐに、タイマーが開始され、SYN/ACKパケットを再度送る(再送タイミングはエクスポネンシャルバックオフ)。クライアントは再度ACKパケットをおくることができる。
Client<SYN-SENT>] --- [SYN] --> Server<LISTEN> Client<SYN-SENT>] <-- [SYN/ACK] --- Server<SYN-RECV> Client<ESTABLISHED> --- [ACK] --> Server<SYN-RECV> # このACKが無視される。ACKが来ないのでSYN/ACKをサーバ側は送る Client<ESTABLISHED> Server<ESTABLISHED>
ここでポイントなのはClientはESTABLISHになっているという点。httpクライアントならリクエスト送信可能状態に移るのでconnect timeoutみたいなのは発生せずにただの処理遅延のように見える。(サーバはSYN_RECVな状態でデータがくるのでdropするはず...ackが帰ってこないパケットの再送なのでtcp_retries2あたりのパラメータが有効になりそうだが検証してみないとよくわかっていない)