NGINX 1.9が汎用TCPサーバとして使えるようになっていた件

# はじめに
一年程前にリリースされた Nginx v1.9.0で、[Streamモジュール](http://nginx.org/en/docs/stream/ngx_stream_core_module.html)が追加されました。
Streamモジュールを使うと、任意のポートでNginxがTCPの接続を待ち受けるよう設定できます。

```nginx:nginx.conf
stream {
    server {
        listen 12345;
        # ...
    }
}
```

この機能は、例えばNginxをTCPロードバランサとして構成する時に威力を発揮するようです。

- [nginx Blog - TCP Load Balancing with NGINX 1.9.0 and NGINX Plus R6](https://www.nginx.com/blog/tcp-load-balancing-with-nginx-1-9-0-and-nginx-plus-r6/)

強力そうな機能ですが、個人的にNginxをTCPロードバランサとして使っていなかったため、特に機能を活用する場もなくスルーしていました。

ところで、つい先日公開されたNginxのモジュール、[stream-lua-nginx-module](https://github.com/openresty/stream-lua-nginx-module)、及び、[stream-echo-nginx-module](https://github.com/openresty/stream-lua-nginx-module)を組み合わせれば、任意のポートで待ち受けるTCPサーバをLuaで記述できることに気付きました。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">ngx_stream_lua_module can now do &quot;hello world&quot; TCP servers. Many more features are coming in the next few days :D <a href="https://t.co/kHM7ycy1RP">pic.twitter.com/kHM7ycy1RP</a></p>&mdash; agentzh (@agentzh) <a href="https://twitter.com/agentzh/status/689640656378028032">2016, 1月 20</a></blockquote>

# ngx_lua_stream_moduleについて
[ngx_stream_lua_module - Embed the power of Lua into Nginx stream/TCP Servers.](https://github.com/openresty/stream-lua-nginx-module)

OpenRestyの作者、 Yichun "agentzh" Zhang氏により開発されています。
現時点のステータスは「ほとんど機能するものの実験的」とのこと。

|名前|[ngx_lua_stream_module](https://github.com/openresty/stream-lua-nginx-module)|
|---|---|
|作者|Yichun "agentzh" Zhang|
|必要なバージョン|Nginx 1.9.7以降|

|名前|[ngx_echo_stream_module](https://github.com/openresty/stream-echo-nginx-module)|
|---|---|
|作者|Yichun "agentzh" Zhang|
|必要なバージョン|Nginx 1.9.0以降|

EchoサーバやDaytimeサーバをLuaで記述・登録すれば、Nginx上でそれらのサーバが動く構成に出来ます。
・・・先ほどの例だとこのような感じで、サーバをLuaで書けます。

```nginx:nginx.conf
stream {
    server {
        listen 12345;
        content_by_lua_block {
          -- サーバのLua実装をここに記述!! --
        }
    }
}
```
更に、コアの部分ではnginxの高速性を引き継ぐため、自動的に(!)、かなりパフォーマンスが高くなっていました。

# ビルド
```bash
cd nginx-1.9.10
./configure \
   --with-stream \
   --with-stream_ssl_module \
   --add-module="../stream-lua-nginx-module-master" \
   --add-module="../stream-echo-nginx-module-master" \
...後略...
```

# Dockerイメージ (動作確認中・・・)
[alpine-nginx-stream-lua-module](https://hub.docker.com/r/dseg/alpine-nginx-stream-lua-module/)

# 動作確認環境
- ArchLinux (Kernel 4.1.16-1-lts on Vultr.com, 768MB RAM / 1x CPU / SSD)
- Openresty 1.9.7.3 (with bundled LuaJIT 2.1 beta)
- stream-lua-nginx-module (git verstion, from master branch)
- stream-echo-nginx-module (git verstion, from master branch)

# 使ってみる
公式のドキュメント、及びテストケースが充実しているため、直接参照するのがてっとり早いと思います。

- [公式ドキュメント](https://github.com/openresty/stream-lua-nginx-module)
- [stream_lua_nginx_module のテストケース](https://github.com/openresty/stream-lua-nginx-module/tree/master/t)

接続を受け付けて文字列を返すだけなら、[stream_echo_nginx_module]()のAPIで足りるかもしれません。
ただし、制御構造が書けない、nginxの内部変数にアクセス出来ないなどかなり制限が多いので、大抵はstream_lua_nginx_moduleを使うことになると思います。

簡単なプロトコルを実装して動かしてみます。
(実装は厳密なものではありません、念のため)
## Daytimeプロトコル
|RFC|[867](https://tools.ietf.org/html/rfc867)|
|---|---|
|Port|13|
### 実装
```nginx:nginx.conf
stream {
  # Daytime Protocol
  server {
    listen 13;
    content_by_lua_block {
      ngx.say(ngx.localtime());
    }
  }
}
```
### 確認
```bash
$ nc 127.0.0.1 13
2016-02-08 19:52:49
```
## Discardプロトコル
|RFC|[863](https://tools.ietf.org/html/rfc863)|
|---|---|
|Port|9|
### 実装
```nginx:nginx.conf
# You need to build Nginx with the stream-echo-nginx add-on.
# https://github.com/openresty/stream-echo-nginx-module

stream {
  server {
    listen 9;
    echo_discard_request;
    echo_sleep 3600;      # in sec
  }
}
```
### 確認
```bash
$ nc 127.0.0.1 9
hogefuga ← ENTERキー

(何もecho backされない)
```
## Chargenプロトコル

|RFC|[864](https://tools.ietf.org/html/rfc864)|
|---|---|
|Port|19|
### 実装
```nginx:nginx.conf
# Chargen server
stream {
  server {
    lua_code_cache on;
    lua_check_client_abort on;
    listen 19 so_keepalive=2m:30s:4 reuseport;
    tcp_nodelay off;
    content_by_lua_file /home/dseg/devel/openresty_service/chargen.lua;
  }
}
```

```lua:chargen.lua
-- chargen.lua
-- RFC 864 (https://tools.ietf.org/html/rfc864)

-- Init character table
local chars = {}
for j = 0x21, 0x7E do table.insert(chars, string.char(j)) end

-- Main loop
local size, i, p = #chars, 0, ngx.print
while true do
    local ok, err = p(chars[i % size + 1])
    -- If client force abort, error will occure
    if not ok then
        ngx.log(ngx.ERR, "chargen.lua: Socket Write Error. ", err)
        ngx.exit(200)
        break
    end
    i = i + 1
    -- wrap line
    if (i % 72) == 0 then p("\r\n") end
end
```
### 確認
```bash
% nc 127.0.0.1 19
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh
ijklmnopqrstuvwxyz{|}~!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQR
STUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~!"#$%&'()*+,-./0123456789:;<
=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~!"#$%&
'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmn
opqrstuvwxyz{|}~!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWX
YZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~!"#$%&'()*+,-./0123456789:;<=>?@AB
CDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~!"#$%&'()*+,

← CTRL+C
```

## Echoプロトコル
|RFC|[862](https://tools.ietf.org/html/rfc862)|
|---|---|
|Port|7|

### 実装

```nginx:nginx.conf
stream {
  server {
    lua_code_cache on;
    lua_check_client_abort off;
    listen 7 so_keepalive=3m:30s:4 reuseport backlog=128;
    tcp_nodelay off;
    content_by_lua_file /home/dseg/devel/openresty_service/echo.lua;
  }
}
```

```lua:echo.lua
local p = ngx.say
local sock = assert(ngx.req.socket(true))
if not sock then
    ngx.log(ngx.ERR, "Failed to get the socket. Exiting...")
    return
end

-- Main loop
while (true) do
    local data, err = sock:receive() -- Read a data from downstream
    if not err then
        p(data) -- output data
    else
        ngx.log(ngx.ERR, err)
        ngx.exit(200)
        break
    end
end
```
### 確認
```bash
$ telnet 127.0.0.1 7
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
hello ←ENTERキー
hello
^]
telnet> q
Connection closed.
```

# ベンチマーク

Echoサーバのベンチマークを取ってみます。
計測には@methaneさん作の[echoserver](https://github.com/methane/echoserver)と、そのclientを利用させて頂きました。

```bash
# Nginx + Lua (先述のecho.lua)
$ echoserver/client -c50 -o2 -h10000 -p7 127.0.0.1
Throughput: 64447.98 [#/sec]

# C++ epoll echoサーバ起動
$ echoserver/server_epoll

$ echoserver/client -c50 -o2 -h10000 -p5000 127.0.0.1
Throughput: 78058.97 [#/sec]
```

64448 ÷ 78059 * 100 = **82.5** (%)

Nginx+Lua は C++ Epoll版の 80%程の速度でした。これはなかなか速いのでは?

# 開発時のTips
## lua_code_cache off と luaファイル外出し
通常nginx.conf内にLuaのコードを記述する形になりますが、コードを変更する度にNginxのリロードが必要となります。これはさすがに面倒です。
そこで、Luaのキャッシュ機能をオフにし、更に外部ファイルからLuaのコードを読み込むようにすることで、開発効率が改善されます。

```nginx:nginx.conf
location /something {
  lua_code_cache off;
  content_by_lua_file /path/to/lua/file.lua;
}
```
- [lua_code_cache](https://github.com/openresty/lua-nginx-module#lua_code_cache)
 
- [content_by_lua_file](https://github.com/openresty/lua-nginx-module#content_by_lua_file)

## スクリプトの確認にrestyコマンドを使う
[Openresty](http://www.openresty.org/)に付属のrestyコマンドが開発時にとても便利です。

- [openresty - resty-cli](https://github.com/openresty/resty-cli)

restyコマンドは、裏で最小構成のnginxデーモンを立ち上げ、指定したLuaのコードを init_worker_by_lua 命令内で実行してくれます。
ngx_luaのAPIに馴染みがない使い始めの頃、コマンドラインでAPIを使ってみるのに大変重宝しました。
内部でタイマーハンドラを使っているので、一部はrestyコマンドから実行できない制限もありますが(ngx.on_abortハンドラ等)、通常は問題ないと思います。

```bash
echo ngx.say("hello") > test.lua

$ resty -e 'ngx.say ("hello")'
hello

$ resty test.lua
hello
```

# おわりに
確かにNginx + Luaで高パフォーマンスのTCPサーバが作れそうですが、問題は「何に活用すれば良いか」、ですかね・・・。