概要
pythonを使って、socketサーバーを書いて、SO_REUSEPORTオプションがある場合の動作を簡単に確認する。
サンプルコード
こんな感じのコード。ちなみにpython3。python2は、TOKYOオリンピックくらいまでの命だから、もう書かないの。
実行すると、デフォルトで[::1]:60001でlistenするTCP echo serverが立ち上がるように書いてるつもり。(IPv6アドレスとポートはオプションで変更可能)
- so-reuseport-test.py
#!/usr/bin/env python3
import logging
import contextlib
import os
import socket
logger = logging.getLogger()
def start(
host='::1',
port=60001,
set_reuseport=True,
):
receiver_name = 'Receiver-{}'.format(os.getpid())
logger.info(
'{}: pid={}'.format(receiver_name, os.getpid()),
)
sock = socket.socket(
family=socket.AF_INET6,
type=socket.SOCK_STREAM,
)
with contextlib.closing(sock):
sock.setsockopt(
socket.SOL_SOCKET,
socket.SO_REUSEADDR,
1,
)
if set_reuseport is True:
sock.setsockopt(
socket.SOL_SOCKET,
socket.SO_REUSEPORT,
1,
)
logger.info(
'{}: so_reuseaddr={}'.format(
receiver_name,
sock.getsockopt(
socket.SOL_SOCKET,
socket.SO_REUSEADDR,
),
),
)
logger.info(
'{}: so_reuseport={}'.format(
receiver_name,
sock.getsockopt(
socket.SOL_SOCKET,
socket.SO_REUSEPORT,
),
),
)
sock.bind((host,port))
sock.listen(1)
logger.info(
'{}: Listening sockname={}'.format(receiver_name, sock.getsockname()),
)
while True:
bound_sock, addr = sock.accept()
logger.info(
'{}: accepted sockname={}'.format(
receiver_name,
bound_sock.getsockname(),
),
)
with contextlib.closing(bound_sock):
msg = bound_sock.recv(4096)
return_msg = '{}: recv message={}'.format(receiver_name, msg)
logger.info(return_msg)
bound_sock.send((return_msg + '\n').encode())
return
if __name__ == '__main__':
import argparse
logger.addHandler(
hdlr=logging.StreamHandler(),
)
logger.setLevel(
level=logging.INFO,
)
parser=argparse.ArgumentParser(
description='SO_REUSEPORT example',
)
parser.add_argument(
'--listen-addr',
required=False,
default='::1',
)
parser.add_argument(
'--listen-port',
required=False,
type=int,
default=60001,
)
parser.add_argument(
'--without-reuseport',
required=False,
action='store_true',
default=False,
)
args = parser.parse_args()
start(
host=args.listen_addr,
port=args.listen_port,
set_reuseport=not args.without_reuseport,
)
SO_REUSEPORT無しの場合
1つ目のサーバー起動
まずは、SO_REUSEPORT無しで立ち上げる。「Receiver-XXX」の「XXX」は、起動したサーバーのプロセス番号。
$ python3 so-reuseport-test.py without-reuseport
Receiver-191: pid=191
Receiver-191: so_reuseaddr=1
Receiver-191: so_reuseport=0
Receiver-191: Listening sockname=('::1', 60001, 0, 0)
2つ目のサーバー起動
上の1個目のサーバーを起動したまま、同じホストの違うシェルで、2個目のサーバーも同じlisten address/portで立ち上げる。
$ python3 so-reuseport-test.py --without-reuseport
Receiver-192: pid=192
Receiver-192: so_reuseaddr=1
Receiver-192: so_reuseport=0
Traceback (most recent call last):
File "so-reuseport-test.py", line 109, in <module>
set_reuseport=not args.without_reuseport,
File "so-reuseport-test.py", line 55, in start
sock.bind((host,port))
OSError: [Errno 98] Address already in use
はい、もちろん失敗。1個目のサーバーもさっさとCtrl+cで終了してね。
SO_REUSEPORT有りの場合
1つ目のサーバー起動
次に、SO_REUSEPORT有りで立ち上げる。1個目は普通に見える。
$ python3 so-reuseport-test.py
Receiver-193: pid=193
Receiver-193: so_reuseaddr=1
Receiver-193: so_reuseport=1
Receiver-193: Listening sockname=('::1', 60001, 0, 0)
2つ目のサーバー起動
上の1個目のサーバーを起動したまま、同じホストで、2個目のサーバーも同じlisten address/portで立ち上げる。
$ python3 so-reuseport-test.py
Receiver-194: pid=194
Receiver-194: so_reuseaddr=1
Receiver-194: so_reuseport=1
Receiver-194: Listening sockname=('::1', 60001, 0, 0)
ほれ。無事に起動した。
socatで何か送ってみる
これで、同じlisten address/portで、2つのプロセスがbind/listenしてることになる。
というわけで、socatを使って何かデータを送ってみる。
$ echo 'Hello' | socat - tcp6-connect:[::1]:60001
Receiver-193: recv message=b'Hello\n'
PID 193のプロセスがacceptしてデータを受信してくれたみたい。これを何回か繰り返してみる。
$ for i in $(seq 1 10);do
echo 'AAAA' | socat - tcp6-connect:[::1]:60001
done
Receiver-194: recv message=b'Hello\n'
Receiver-193: recv message=b'Hello\n'
Receiver-193: recv message=b'Hello\n'
Receiver-193: recv message=b'Hello\n'
Receiver-194: recv message=b'Hello\n'
Receiver-194: recv message=b'Hello\n'
Receiver-194: recv message=b'Hello\n'
Receiver-193: recv message=b'Hello\n'
Receiver-194: recv message=b'Hello\n'
Receiver-193: recv message=b'Hello\n'
ちゃんと振り分けしてくれているYO!
HAHAHAHA!
SO_REUSEPORTって?
- SO_REUSEPORT(とSO_REUSEADDR)を有効にすると、同じlisten address/portで複数のプロセス(スレッド)がlistenできる。
- ただし、全プロセスがSO_REUSEPORT(とSO_REUSEADDR)を有効にしないといけない。
- さらに全プロセス、同じユーザーでないといけない。
- 振り分けは、listen socketをaccept中の(つまり暇な)プロセス群のなかから、カーネルが適当に選んでくれるよ。
- つまり、pythonみたいに、サーバーの性能出すために複数プロセス使うんだけど、リクエスト振り分け目的のnginxやgunicornみたいなのは不要ってこと。
- 最低1プロセス生きていれば、それ以外のプロセス群をrestartしようが落とそうが問題ない(プロセス終了時はgracefulにね)。
- systemdでも対応しているから、次回はそのことについても書こう。