地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Linux】Nginxのtcpのbacklogの設定が何をしているのかに入門する

目次

概要

nginx.org

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と状態遷移

milestone-of-se.nesuke.com

f:id:ryuichi1208:20211123121236p:plain

ざっくり振り返る。今回は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までそれぞれ設定されます。

docs.huihoo.com

2つのキューのイメージは以下のようになっています。

f:id:ryuichi1208:20211123130424p:plain

blog.cloudflare.com

syn-queue

SYN_RECEIVED状態のソケットの数。net.ipv4.tcp_max_syn_backlogという数字とaccept-queueの数から割り出される。

wiki.bit-hive.com

最初のsynがクライアントから来てsyn/ackを返しackが来るまでがここの値になる、syn/ackのリトライはnet.ipv4.tcp_synack_retriesまでする。

どうやったら溢れるか

SYN flood攻撃を受けたら溢れてしまう可能性がある。SYN flood攻撃とはざっくりいうとsynを大量に発行するクライアントがいたとしてsyn/ackを返してもそれに対するackを返さないような攻撃。IPスプーフィングとかsyn/ackを落とすファイアウォール設定をするとかで簡単に行える。(hping3とか使えば負荷テストもできる)

ja.wikipedia.org

あとはsynが来たけどsyn/ackを返せないくらいサーバが高負荷になっている状況だとsyn_recvの値は増え続けていく。外部からの接続速度が非常に大きい場合に起きるかもしれないがそういう状況に遭遇したことはないのでよくは分からない...

溢れたらどうなるか

2パターン存在する。今のLinuxだとnet.ipv4.tcp_syncookiesがデフォルトで有効になっていると思うのですがとりあえず無効な場合のパターンも書く。TCP SYN flood攻撃を防ぐために開発された手法のひとつで、サーバ側は SYN を受けとった直後に記憶領域を消費するのではなくsyn/ackに対するackが来て初めて TCP 接続用の領域を割り当て接続を確立させていくというもの。

ja.wikipedia.org

net.ipv4.tcp_syncookiesが無効

net.ipv4.tcp_syncookies = 0の場合、SYN_RECVは以下のいずれかのうち、小さい方の数まで登録できる。

  • backlogのサイズまで
  • net.ipv4.tcp_max_syn_backlogの3/4まで

これらを超えると新たにSYN_RECVは登録せず廃棄する。このため、新たにTCP接続を確立できない。synはdropされ続けクライアントの実装次第ではあるがいわゆる繋がりにくいという状況に陥る。

synのdropnetstatの統計情報を見るコマンドとかで確認できる。

$ 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攻撃を受けていようが新規接続で接続問題は起きなくなる。

access.redhat.com

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サーバの場合はメインプロセスが忙しいだけの場合は容易に溢れが起きてしまう。

0xcc.net

(メモ)Nginxのアーキテクチャ

Nginxはイベント駆動のアーキテクチャ、単一のリスニングソケットがワーカーに着信接続について通知し、各ワーカーは接続を取得しようとします。nginxは epoll_waitを利用して、イベントの発生を待ちイベントが発生したら処理を行うというモデルでaccept(2)がredyになるっていうイベントはこのepoll_waitで拾われ処理が行われる仕組みになっています。

f:id:ryuichi1208:20211123130905p:plain

www.nginx.com

溢れたらどうなるか

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あたりのパラメータが有効になりそうだが検証してみないとよくわかっていない)

ngyuki.hatenablog.com

まとめ