Help us understand the problem. What is going on with this article?

SO_REUSEPORT pythonサンプル

More than 3 years have passed since last update.

概要

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でも対応しているから、次回はそのことについても書こう。
takaomag
zeta
サイト内検索/レコメンドを主軸としたECソリューションを開発・提供。ディープラーニング技術のEC展開にも注力しています。
https://zeta.jpn.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away