最近 ParallelServer というライブラリを作ったのですが、その最中に奇妙な状態になってる TCP ポートを見つけたので、メモっておきます。
Ruby では TCP サーバーは次のような感じで作ることができます。お手軽ですね。
require 'socket' Socket.tcp_server_loop(12345) do |socket, client_addr| socket.puts "Your IP address: #{client_addr.ip_address}" name = socket.gets socket.puts "Hello, #{name}" socket.close end
これは 12345 ポートでクライアントからの接続を待ち、接続されたらクライアントのIPアドレスとクライアントからの入力をクライアントに送信して切断するだけの簡単なプログラムです。
~% nc -v localhost 12345 Connection to localhost 12345 port [tcp/*] succeeded! Your IP address: 127.0.0.1 hogehoge Hello, hogehoge ~%
サーバープログラム起動直後の 12345 ポートの状態は次のようになります。
~% netstat -an | grep 12345 tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN tcp6 0 0 :::12345 :::* LISTEN
IPv4 と IPv6 の両方で LISTEN 状態です。
クライアントから接続すると次のように ESTABLISHED になります。
~% netstat -an | grep 12345 tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:12345 127.0.0.1:56863 ESTABLISHED tcp 0 0 127.0.0.1:56863 127.0.0.1:12345 ESTABLISHED tcp6 0 0 :::12345 :::* LISTEN
サーバーの 12345 とクライアントの 56863 が接続されている状態です。 同じサーバー内で接続しているため、1つの接続について、サーバーからみた接続とクライアントから見た接続の2行出力されています。
このように普通は接続が確立したらサーバーとクライアントの両方が ESTABLISHED になります。
ここで次のクライアントプログラムを動かしてみます。
require 'socket' sockets = [] 200.times do |i| p i sockets.push TCPSocket.new("localhost", 12345) end sleep
サーバーに 200接続したまま何もしないプログラムです。
サーバーは最初の接続からのクライアントの入力を待つので、2個め以降の接続を処理できません。
普通のサーバープログラムは、そんなことにならないように、クライアントからの接続を受け付けた時に fork したりスレッドを作ったりするのですが、ここでは意図的にこのようにしてます。
この状態で netstat を見ると、サーバー側にいくつか SYN_RECV 状態のポートがあります。
tcp 0 0 127.0.0.1:12345 127.0.0.1:36807 ESTABLISHED tcp 0 0 127.0.0.1:12345 127.0.0.1:36930 SYN_RECV ← これ tcp 0 0 127.0.0.1:12345 127.0.0.1:36953 SYN_RECV ← これ tcp 0 0 127.0.0.1:36807 127.0.0.1:12345 ESTABLISHED tcp 0 0 127.0.0.1:36930 127.0.0.1:12345 ESTABLISHED ← これ tcp 0 0 127.0.0.1:36953 127.0.0.1:12345 ESTABLISHED ← これ
クライアントポート 36807 はちゃんと対向するサーバーが ESTABLISHED になっているのですが、36930, 36953 ポートは SYN_RECV になっています。
しばらくすると、この SYN_RECV 状態のポートはなくなります。といっても ESTABLISHED になったわけではなく、消えてなくなってしまいます。
tcp 0 0 127.0.0.1:12345 127.0.0.1:36807 ESTABLISHED tcp 0 0 127.0.0.1:36807 127.0.0.1:12345 ESTABLISHED tcp 0 0 127.0.0.1:36930 127.0.0.1:12345 ESTABLISHED ← これ tcp 0 0 127.0.0.1:36953 127.0.0.1:12345 ESTABLISHED ← これ
クライアントは ESTABLISHED だと思っているのに接続相手のサーバーはいません。
サーバープログラムを停止しても状態は変わりません。 クライアントプログラムを停止しない限り残ったままです。
tcp 0 0 127.0.0.1:36930 127.0.0.1:12345 ESTABLISHED ← これ tcp 0 0 127.0.0.1:36953 127.0.0.1:12345 ESTABLISHED ← これ
このように 200 くらい接続すると SYN_RECV が確認できるのですけど、どうやらこれは listen のバックログの値に関連してるようです。
Socket.tcp_server_loop
は内部で listen(Socket::SOMAXCONN)
しています。手元の Linux では SOMAXCONN
は 128 になっていました。
もっと単純に次のようなサーバープログラムで試してみました。
require 'socket' TCPServer.new(12345).listen(5) sleep
このようにバックログを 5 にすると、クライアント接続が 10 くらいでも再現できます。
なお、listen(Socket::SOMAXCONN)
しているのは Ruby 2.0 以降です。Ruby 1.9.3 では listen(5)
としているので、すぐに発生します。
で、結局こんなことになってしまう理由はわかりませんでした。
クライアントが ESTABLISHED だと思ってるのに SYN_RECV になってるのもわからないし、相手がいないのに ESTABLISHED になってるソケットが存在してるのもわかりません。 ネットワークの向こうの別のサーバであればパケット落ちとかでこのような状況になるのもわかるのですけど。
バックログを超えたら SYN_RECV にならなくてもいいと思うんですけど…。 TCP/IP はそういうもんなんですかね。
[追記]
Facebook の方で色々教えてもらいました。
OS X では発生しないようです。うすうす感じていたのですがやはり Linux の実装の問題のようです。
教えてもらったブログ http://veithen.blogspot.jp/2014/01/how-tcp-backlog-works-in-linux.html に答えがありました。
BSD では接続用のキューが1つで、キューがいっぱいの場合はクライアントからの SYN を破棄するだけなので、クライアントが ESTABLISHED になることはありません。私の知ってる TCP の動きです。
Linux では SYN キューと accept キューの2つがあるようで、SYN キューに入った時に SYN ACK を返し SYN_RECV 状態になり、accept キューに入った時に ESTABLISHED になるようです。
サーバープログラムが accept しなければ、accept キューがいっぱいになり SYN キューも掃けません。SYN キューにたまったものは一定時間たつと削除されるようです。
SYN キューの大きさは、sysctl net.ipv4.tcp_max_syn_backlog
で、accept キューの大きさはプログラムから指定する listen のバックログです。
sysctl -w net.ipv4.tcp_max_syn_backlog=0
にして試してみたら BSD の動きに近くなりました。ただし 0 に設定しても1個は SYN_RECV になってしまうようです。
また sysctl -w net.ipv4.tcp_abort_on_overflow=1
にすると accept キューがあふれた場合、クライアントからの SYN に RST を返すようです。この場合はクライアントプログラムは connect(2) に対して ECONNRESET エラーが返ります。
RST じゃなくて SYN を無視するようになれば BSD と同じになるんじゃないかと思ったのですが、そういうパラメータは無さそうでした。
まあ別に BSD と同じ動きにしたかったわけではなく、ただ疑問に思っただけだったので、それは解決したのでもういいです。