nginx の reuseport (SO_REUSEPORT) を試してみる。 docker-compose で

# nginx の reuseport オプションとは

nginx 1.9.1 からの新機能で、 SO_REUSEPORT というソケットのオプションを有効化するもの。
https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/

# SO_REUSEPORT とは

Linux Kernel 3.9 以降の機能で、 1 つのポートを複数のプロセスで bind できるようにするもの。

http://man7.org/linux/man-pages/man7/socket.7.html
> SO_REUSEPORT (since Linux 3.9)
> Permits multiple AF_INET or AF_INET6 sockets to be bound to an
> identical socket address.  This option must be set on each
> socket (including the first socket) prior to calling bind(2)
> on the socket.  To prevent port hijacking, all of the
> processes binding to the same address must have the same
> effective UID.  This option can be employed with both TCP and
> UDP sockets.
(以下略)


つまり、同じポートを使う別のプロセスをにょきにょき建てられる。
なので、高速化とダウンタイムのないデプロイには便利。

# Docker で試してみた。

メジャーなディストリビューションで 3.9 以上がデフォルトになってるのものは少ない。

ところで docker-machine の VirtualBox ドライバーが提供する boot2docker のカーネルは 2015 年 10 月 13 日現在、 Linux v4.1.10 (AUFS v4.1-20151012) 。
https://github.com/boot2docker/boot2docker/releases

これは行ける！と思ったので、自分の使ってる OS X 上で、 Docker on boot2docker な構成で試してみた。
せっかくなので docker-compose (元 fig) で楽をすることにした。

普通の nginx の使い方なら HUP シグナルを送れば reload (graceful restart) するので充分だが、
特に Docker の場合は「コンテナごと入れ替えたい」というケースが頻繁にありうるので、このケースとよりマッチしそうだ。
コンテナのクラスタごと Green-Blue-Deploy する場合とか。

# 用意したファイル。

以下の 3 つのみ。

- docker-compose.yml
- nginx の default.conf (reuseport を ON にしたもの。 HTTP の応答は単に "OK" とだけ返答する )
- test.sh (一括で実行するシェルスクリプト)

3 つの nginx プロセスをホストのネットワークを利用する設定で立ち上げる。
reuseport オプションが指定されているので、 3 つとも 80 番のポートで待ち受け、
早い者勝ちでコネクションを確立して普通の HTTP の応答をする。
Ctrl-C で終了。

```yaml:docker-compose.yml
nginx_1:
  image: nginx
  net: host
  volumes:
    - ./conf.d:/etc/nginx/conf.d
  ports:
    - "80:80"

nginx_2:
  image: nginx
  net: host
  volumes:
    - ./conf.d:/etc/nginx/conf.d
  ports:
    - "80:80"

nginx_3:
  image: nginx
  net: host
  volumes:
    - ./conf.d:/etc/nginx/conf.d
  ports:
    - "80:80"
```

```nginx:conf.d/default.conf
server {
    server_name localhost;
    listen 80 reuseport;
    location / {
        return 200 'OK';
    }
}
```

```bash:test.sh
#!/bin/sh

MACHINE="$1"
eval "$(docker-machine env "$MACHINE")"
TARGET_HOST=$(docker-machine ip "$MACHINE")

docker-compose up -d
while sleep 1; do
  curl -fsSL "$TARGET_HOST"
done > /dev/null &
SUB="$!"
docker-compose logs
kill "$SUB"
docker-compose stop
docker-compose rm -f
```

# 実行例

```console
$ ./test.sh default
Creating nginx2_nginx_2_1...
Creating nginx2_nginx_3_1...
Creating nginx2_nginx_1_1...
Attaching to nginx2_nginx_1_1, nginx2_nginx_3_1, nginx2_nginx_2_1
nginx_3_1 | 192.168.99.1 - - [13/Oct/2015:12:30:54 +0000] "GET / HTTP/1.1" 200 2 "-" "curl/7.43.0" "-"
nginx_2_1 | 192.168.99.1 - - [13/Oct/2015:12:30:55 +0000] "GET / HTTP/1.1" 200 2 "-" "curl/7.43.0" "-"
nginx_3_1 | 192.168.99.1 - - [13/Oct/2015:12:30:56 +0000] "GET / HTTP/1.1" 200 2 "-" "curl/7.43.0" "-"
nginx_2_1 | 192.168.99.1 - - [13/Oct/2015:12:30:57 +0000] "GET / HTTP/1.1" 200 2 "-" "curl/7.43.0" "-"
nginx_1_1 | 192.168.99.1 - - [13/Oct/2015:12:30:58 +0000] "GET / HTTP/1.1" 200 2 "-" "curl/7.43.0" "-"
nginx_2_1 | 192.168.99.1 - - [13/Oct/2015:12:30:59 +0000] "GET / HTTP/1.1" 200 2 "-" "curl/7.43.0" "-"
nginx_1_1 | 192.168.99.1 - - [13/Oct/2015:12:31:00 +0000] "GET / HTTP/1.1" 200 2 "-" "curl/7.43.0" "-"
```

おー、 nginx1_1, nginx2_1, nginx3_1 が入れ替わりながら応答してる。イイカンジだ。

実際の使用例では、

- 新しいクラスタをデプロイ
- 旧いクラスタの nginx コンテナに QUIT シグナルを送る (graceful shutdown)

の順で行えば良いと思う。

# 余談

## なお、 HAProxy は SO_REUSEPORT を自動で有効化してくれるらしい
https://github.com/haproxy/haproxy/blob/3cd0ae963e958d5d5fb838e120f1b0e9361a92f8/src/proto_tcp.c#L792-L798
http://diary.sorah.jp/2015/02/09/20150209