nginxのv1.9あたりからOSS版でも使えるTCPロードバランス機能をmrubyでプログラマブルに制御できるようにngx_mrubyでもサポートしました。
これで、HTTPやHTTP/2だけでなくTCPのロードバランスサーバでもmrubyによって通信をプログラマブルに制御できるようになったわけです。
nginxのTCPロードバランス機能は、nginx内部ではstreamモジュールとして、httpのモジュールとは別で実装しているため、ngx_mrubyでも一から実装し直す必要がありました。
ということで少し面倒だなぁと思っていたのですが、ちょうど、僕の最近やりたい事としてTCPのロードバランサをもう少しプログラマブルに書きたいというのがあって、色々とTCPロードバランサを探すよりも、自分でnginxのTCPロードバランサ機能をプログラマブルに制御できるようにすれば良いかと思い、楽をするために実装してみました。
- Support nginx stream module for tcp load balancer by matsumoto-r · Pull Request #140 · matsumoto-r/ngx_mruby · GitHub
- Support stream init and exit directives with mruby, and access control methods by matsumoto-r · Pull Request #141 · matsumoto-r/ngx_mruby · GitHub
masterにはマージ済みですが、もう少し色々と調整したり検証してからリリースしようと思います。気になる方はぜひmasterのものを使っていただいて性能やバグなどフィードバックいただけると嬉しいです。大体動くはずです。
簡単な使い方
動的upstream
ngx_mrubyのsteramモジュールは nginx v1.9.6以上 で使えるようにしています。
例えば、以下のような設定のように書きます。
stream {
upstream dynamic_server0 {
server 127.0.0.1:58080;
}
server {
listen 12346;
mruby_stream_code '
c = Nginx::Stream::Connection.new "dynamic_server0"
c.upstream_server = "192.168.0.3:54321"
Nginx::Stream.log Nginx::Stream::LOG_NOTICE, "dynamic_server0 was changed to 192.168.0.3:54321"
';
proxy_pass dynamic_server0;
}
}
以下のように、予めロードバランス先のupstreamであるdynamic_server0を書いておき、それをmruby_stream_codeというディレクティブの中で新しいupstream先である192.168.0.3:54321に書き換える事ができます。これによって、ポート12346に対する接続は192.168.0.3:54321へとフォワードされます。
これはすべての接続について、upstreamを書き換えるようにしていますが、もちろん条件によって様々なupstreamに書き換える事が可能です。
ここになにかupstreamのヘルスチェックみたいなのをいれて選択することも可能ですね。
基本的にnginxのstreamは、httpでのlocation設定などがないので、セッション処理時にフックできるmrubyはmruby_stream_code(mruby_stream ファイル名でもOK)だけにしています。その他、後述する他のディレクティブもngx_mrubyのhttpモジュールと同様、_codeをつけたディレクティブは引数にコードを直接渡し、つけない場合(mruby_stream_initとかmruby_stream_worker_initとか)は引数にコードが書かれたファイルパスを渡します。
起動時の設定から動的upstream
ngx_mrubyのstreamモジュールは、httpモジュールと同様、master起動時・worker起動時・worker停止時にmrubyをフックすることができるようにしています。それらのRubyは基本的にngx_mrubyと同様mrubyのstateを共有するため、クラスを定義しておいて後から読む事も可能です。
これを利用して、例えば起動時にsrc-ipとdst-ipの対応をJSONやデータベースから読み込んでハッシュにしてメモリ上に置いておき、それをsession時の処理であるmruby_stream_codeを使って対応通りのupstreamサーバにフォワードすることも可能です。
これによって、HTTPといったアプリケーションレイヤー以下のTCPレベルでの通信において、接続元から任意のupstreamに振り分ける事も可能です。また、mrubyの許す範囲でその他様々な条件によってupstreamを選択できるのもngx_mrubyの強みでしょう。
stream {
upstream dynamic_server0 {
server 127.0.0.1:58080;
}
# ngx_mruby起動時にupstream振り分けの設定を読んでおく
mruby_stream_init_code '
Userdata.new.new_upstream = "127.0.0.1:58081"
';
# workerの初期化時のフックとか
mruby_stream_init_worker_code '
p "ngx_mruby: STREAM: mruby_stream_init_worker_code"
';
# worker停止時のフックとかもかける
mruby_stream_exit_worker_code '
p "ngx_mruby: STREAM: mruby_stream_exit_worker_code"
';
server {
listen 12346;
# session処理時に、起動時に読んだupstreamの設定から振り分け
mruby_stream_code '
c = Nginx::Stream::Connection.new "dynamic_server0"
# mruby_stream_initで定義したデータを取得して使う
c.upstream_server = Userdata.new.new_upstream
';
proxy_pass dynamic_server0;
}
アクセス制御
TCPロードバランサにおいて、例えば接続元をしぼったり、mruby内で何か条件や制御を行った結果からupstreamにfowardするかどうかを決定したい場合もあると思います。
それらを実現するために、ngx_mrubyではアクセス制御も可能になっています。
stream {
server {
listen 12347;
mruby_stream_code '
if Nginx::Stream::Connection.remote_ip == "127.0.0.1"
current_status = Nginx::Stream::Connection.stream_status
Nginx::Stream::Connection.stream_status = Nginx::Stream::ABORT
Nginx::Stream.log Nginx::Stream::LOG_NOTICE, "current status=#{(current_status == Nginx::Stream::DECLINED) ? "NGX_DECLINED" : current_status} but deny from #{Nginx::Stream::Connection.remote_ip} return NGX_ABORT"
end
';
proxy_pass dynamic_server1;
}
}
例えば上記のように、接続元が127.0.0.1だったら接続を拒否したい場合には、Nginx::Stream::Connection.stream_status=メソッドを使って、アクセス制御を行います。拒否したい場合は、Nginx::Stream::ABORTをセットすることで、接続をすぐに切る事ができます。
また、例のごとくアクセス制御の記述はmrubyやmrbgemが許す範囲内で様々なルールを書く事ができるでしょう。
まとめ
以上のように、ngx_mrubyはnginxのHTTP機能だけでなくTCPロードバランサであるstream機能にも対応しました。
mrubyやmrbgemが許す範囲でかなりプログラマブルにmrubyを使って設定をかけるようになったのではないでしょうか。というのも、TCPロードバランサであってもmrubyを設定で書きたいでござると感じていたからです。
これによって、例えばそれほどTCPロードバランサに性能が必要なくて、バックエンドも性能重視じゃないけとHTTPといったアプリケーションレイヤではなくTCPを使わないといけない環境の場合、可用性や透過の柔軟性を重視したTCPロードバランサの選択肢としてngx_mrubyを使うのは良さそうに思っています。
HTTPよりもよりオーバーヘッドの少ないレイヤでの実装なので、基本的にはmrubyはすべて起動時にコンパイルしてキャッシュするようにしています。また、ngx_mrubyのHTTPモジュールよりもオーバーヘッドが発生するかもしれません。(その辺りはまだ未検証なのでだれか検証していただけると最高です)TLSとかもちゃんと使えるのかな?とか色々ngx_mruby及びnginxレベルで試したい所はいくつかあります。
状況によっては結構使える場面が沢山あると思いますのでぜひお試しください。