人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

ngx_mrubyがnginxのTCPロードバランシング機能に対応しました

nginxのv1.9あたりからOSS版でも使えるTCPロードバランシング機能をmrubyでプログラマブルに制御できるようにngx_mrubyでもサポートしました。

github.com

これで、HTTPやHTTP/2だけでなくTCPのロードバランシングでもmrubyによって通信をプログラマブルに制御できるようになったわけです。

nginxのTCPロードバランシング機能は、nginx内部ではstreamモジュールとして、httpモジュールとは別で実装しているため、ngx_mrubyでも一から実装し直す必要がありました。

ということで少し面倒だなぁと思っていたのですが、ちょうど、僕の最近やりたい事としてTCPのロードバランサをもう少しプログラマブルに書きたいというのがあって、色々とTCPロードバランサを探したり、既存のソフトウェアで設定を試行錯誤するよりも、自分でnginxのTCPロードバランシング機能をプログラマブルに制御できるようにしたほうが手っ取り早いし理解も深まるかと思い、楽をするために実装してみました。

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;
  }
}

mruby_stream_codeはTCPセッション処理時にフックされます。

上記のように、予めロードバランス先の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_codemruby_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;
  }

上記の例では、単一の設定をnginxの起動時に読みそれをセッション処理時に使っているだけですが、これをもっとちゃんとして設定を読み込んだ上で、そこからupstreamを判定するような動的な処理にすれば非常に柔軟性の高い記述が可能になります。

アクセス制御

TCPロードバランサにおいて、例えば接続元をしぼったり、何か条件判定を行ってから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を使って設定をかけるようになったのではないでしょうか。これまではHTTPやHTTP/2といったレイヤーでしか記述できませんでしが、今後はTCPのレイヤーでも色々とプログラマブルな制御が可能になります。というのも、TCPロードバランサであってもmrubyを設定で書きたいでござるとずっと感じていたからです。

これによって、例えばそれほどTCPロードバランサに性能が必要なくて、バックエンドも性能重視じゃないけとHTTPといったアプリケーションレイヤではなくTCPを使わないといけない環境の場合、可用性や透過の柔軟性を重視したTCPロードバランサの選択肢としてngx_mrubyを使うのは良さそうに思っています。

HTTPよりもよりオーバーヘッドの少ないレイヤでの実装なので、基本的にはmrubyはすべて起動時にコンパイルしてキャッシュするようにしています。また、ngx_mrubyのHTTPモジュールよりもオーバーヘッドが発生するかもしれません。(その辺りはまだ未検証なのでだれか検証していただけると最高です)TLSとかもちゃんと使えるのかな?とか色々ngx_mruby及びnginxレベルで試したい所はいくつかあります。

状況によっては結構使える場面が沢山あると思いますのでぜひお試しください。