一般に同じアドレスを同じポートではlistenできない。しかし、ソケットのオプションに SO_REUSEPORT
というものがあり、Linuxではカーネル3.9以降で利用できる。
ソケットを作成した後に setsockopt(2)
で SO_REUSEPORT
が有効になるように指定すると、同じアドレス・同じポートでのbind/listenが可能になり、リクエストが来た際にはリスンしているソケットそれぞれに回されていく。
ただ、この機能はRubyの TCPServerクラス
ではすぐには利用できない。 TCPServer#new/open
の終了時点でアドレスがリスンされ、setsockoptするタイミングがないため。ではどうするかというと、 Socket クラスでの各メソッド Socket#setsockopt/bind/listen
を直接使えば良い。
require 'socket' class TCPReuseportServer < Socket def self.open(host, service) s = new(Socket::AF_INET, Socket::SOCK_STREAM, 0) s.setsockopt(:SOCKET, :REUSEPORT, true) s.bind(Socket.sockaddr_in(service, host)) s.listen(128) # SOMAXCONN return s end def accept s, addr = super return s end end host = "localhost" port = 10021 gs = TCPReuseportServer.open(host, port) printf("server is on %s\n", [host, port].join(":")) while true Thread.start(gs.accept) do |s| print(s, " is accepted\n") while s.gets s.write($_) end print(s, " is gone\n") s.close end end
後半のコードは るりまのサンプルコード と同じだとわかるだろう。
vagrant@ubuntu-bionic:~$ ruby -v # ...OSパッケージのRubyで雑に ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux-gnu] vagrant@ubuntu-bionic:~$ ruby reuseport.rb & [2] 2171 server is on localhost:10021: 2171 vagrant@ubuntu-bionic:~$ ruby reuseport.rb & [3] 2173 server is on localhost:10021: 2173 vagrant@ubuntu-bionic:~$ ruby reuseport.rb & [4] 2175 server is on localhost:10021: 2175 vagrant@ubuntu-bionic:~$ sudo lsof -i:10021 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME ruby 2171 vagrant 7u IPv4 123779 0t0 TCP localhost:10021 (LISTEN) ruby 2173 vagrant 7u IPv4 124953 0t0 TCP localhost:10021 (LISTEN) ruby 2175 vagrant 7u IPv4 124961 0t0 TCP localhost:10021 (LISTEN) ## 別のターミナルからtelnetすると、毎回別のプロセスがリクエストを受け取る。 #<Socket:0x000055fdba315818> is accepted: 2173 #<Socket:0x000055fdba315818> is gone: 2173 #<Socket:0x000055ac9eac94c8> is accepted: 2171 #<Socket:0x000055ac9eac94c8> is gone: 2171 #<Socket:0x000055ea9fcc95e0> is accepted: 2175 #<Socket:0x000055ea9fcc95e0> is gone: 2175
この機能がどこで役にたつかというと、具体的には serverengine のようなフレームワークを使ってマルチプロセスなワーカーを使うサーバを書いた場合に、それぞれのワーカで同じアドレス/ポートをリッスンできるし、リクエストの振り分けもカーネルでやってくれて便利、みたいなことが可能になるはず。
また、同じポートをリスンさせたまま新旧のプロセスを切り替えるような場合にも使えると思われる。
ちなみに TCPServer.open
の実装は、おそらくC言語でRubyの上述のコードを同じことをしているように読める。
TCPServer.open
に reuseport: true
のようなオプションがあると便利なのだろうか。
参考
Nginx は、オプションでSO_REUSEPORTが有効になり、各ワーカでリスンするようになるらしい。
Pumaのbinder.rbも素のSocketクラスを利用してsetsockoptしているようで、SO_REUSEPORTを使いたいというPRがあった。