はじめに
一年程前にリリースされた Nginx v1.9.0で、Streamモジュールが追加されました。
Streamモジュールを使うと、任意のポートでNginxがTCPの接続を待ち受けるよう設定できます。
stream {
server {
listen 12345;
# ...
}
}
この機能は、例えばNginxをTCPロードバランサとして構成する時に威力を発揮するようです。
強力そうな機能ですが、個人的にNginxをTCPロードバランサとして使っていなかったため、特に機能を活用する場もなくスルーしていました。
ところで、つい先日公開されたNginxのモジュール、stream-lua-nginx-module、及び、stream-echo-nginx-moduleを組み合わせれば、任意のポートで待ち受けるTCPサーバをLuaで記述できることに気付きました。
ngx_stream_lua_module can now do "hello world" TCP servers. Many more features are coming in the next few days :D pic.twitter.com/kHM7ycy1RP
— agentzh (@agentzh) 2016, 1月 20
ngx_lua_stream_moduleについて
ngx_stream_lua_module - Embed the power of Lua into Nginx stream/TCP Servers.
OpenRestyの作者、 Yichun "agentzh" Zhang氏により開発されています。
現時点のステータスは「ほとんど機能するものの実験的」とのこと。
名前 | ngx_lua_stream_module |
---|---|
作者 | Yichun "agentzh" Zhang |
必要なバージョン | Nginx 1.9.7以降 |
名前 | ngx_echo_stream_module |
---|---|
作者 | Yichun "agentzh" Zhang |
必要なバージョン | Nginx 1.9.0以降 |
EchoサーバやDaytimeサーバをLuaで記述・登録すれば、Nginx上でそれらのサーバが動く構成に出来ます。
・・・先ほどの例だとこのような感じで、サーバをLuaで書けます。
stream {
server {
listen 12345;
content_by_lua_block {
-- サーバのLua実装をここに記述!! --
}
}
}
更に、コアの部分ではnginxの高速性を引き継ぐため、自動的に(!)、かなりパフォーマンスが高くなっていました。
ビルド
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
動作確認環境
- 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)
使ってみる
公式のドキュメント、及びテストケースが充実しているため、直接参照するのがてっとり早いと思います。
接続を受け付けて文字列を返すだけなら、stream_echo_nginx_moduleのAPIで足りるかもしれません。
ただし、制御構造が書けない、nginxの内部変数にアクセス出来ないなどかなり制限が多いので、大抵はstream_lua_nginx_moduleを使うことになると思います。
簡単なプロトコルを実装して動かしてみます。
(実装は厳密なものではありません、念のため)
Daytimeプロトコル
RFC | 867 |
---|---|
Port | 13 |
実装
stream {
# Daytime Protocol
server {
listen 13;
content_by_lua_block {
ngx.say(ngx.localtime());
}
}
}
確認
$ nc 127.0.0.1 13
2016-02-08 19:52:49
Discardプロトコル
RFC | 863 |
---|---|
Port | 9 |
実装
# 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
}
}
確認
$ nc 127.0.0.1 9
hogefuga ← ENTERキー
(何もecho backされない)
Chargenプロトコル
RFC | 864 |
---|---|
Port | 19 |
実装
# 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;
}
}
-- 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
確認
% 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 |
---|---|
Port | 7 |
実装
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;
}
}
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
確認
$ 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と、そのclientを利用させて頂きました。
# 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のコードを読み込むようにすることで、開発効率が改善されます。
location /something {
lua_code_cache off;
content_by_lua_file /path/to/lua/file.lua;
}
スクリプトの確認にrestyコマンドを使う
Openrestyに付属のrestyコマンドが開発時にとても便利です。
restyコマンドは、裏で最小構成のnginxデーモンを立ち上げ、指定したLuaのコードを init_worker_by_lua 命令内で実行してくれます。
ngx_luaのAPIに馴染みがない使い始めの頃、コマンドラインでAPIを使ってみるのに大変重宝しました。
内部でタイマーハンドラを使っているので、一部はrestyコマンドから実行できない制限もありますが(ngx.on_abortハンドラ等)、通常は問題ないと思います。
echo ngx.say("hello") > test.lua
$ resty -e 'ngx.say ("hello")'
hello
$ resty test.lua
hello
おわりに
確かにNginx + Luaで高パフォーマンスのTCPサーバが作れそうですが、問題は「何に活用すれば良いか」、ですかね・・・。