ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Rubyでも SO_REUSEPORT 使いたい!

一般に同じアドレスを同じポートではlistenできない。しかし、ソケットのオプションに SO_REUSEPORT というものがあり、Linuxではカーネル3.9以降で利用できる。

ソケットを作成した後に setsockopt(2)SO_REUSEPORT が有効になるように指定すると、同じアドレス・同じポートでのbind/listenが可能になり、リクエストが来た際にはリスンしているソケットそれぞれに回されていく。

ただ、この機能はRubyTCPServerクラス ではすぐには利用できない。 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の上述のコードを同じことをしているように読める。

github.com

TCPServer.openreuseport: true のようなオプションがあると便利なのだろうか。

参考

Nginx は、オプションでSO_REUSEPORTが有効になり、各ワーカでリスンするようになるらしい。

qiita.com

Pumaのbinder.rbも素のSocketクラスを利用してsetsockoptしているようで、SO_REUSEPORTを使いたいというPRがあった。

github.com