SO_REUSEPORT pythonサンプル

# 概要
pythonを使って、socketサーバーを書いて、SO_REUSEPORTオプションがある場合の動作を簡単に確認する。

## サンプルコード
こんな感じのコード。ちなみにpython3。python2は、TOKYOオリンピックくらいまでの命だから、もう書かないの。

実行すると、デフォルトで[::1]:60001でlistenするTCP echo serverが立ち上がるように書いてるつもり。（IPv6アドレスとポートはオプションで変更可能）

- so-reuseport-test.py

```py3
#!/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」は、起動したサーバーのプロセス番号。

```bash
$ 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で立ち上げる。

```bash
$ 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個目は普通に見える。

```bash
$ 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で立ち上げる。

```bash
$ 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を使って何かデータを送ってみる。

```bash
$ echo 'Hello' | socat - tcp6-connect:[::1]:60001
Receiver-193: recv message=b'Hello\n'
```

PID 193のプロセスがacceptしてデータを受信してくれたみたい。これを何回か繰り返してみる。

```bash
$ 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でも対応しているから、次回はそのことについても書こう。