インダストリー事業本部のイチノです。リモート製品 (Optimal Remote, Optimal Second Sight, ポケットドクターなど遠隔地とコミュニケーションするための製品) で使われるコア技術をまとめた Communication SDK を担当しています。
本記事では、ひとつのポートで異なる通信プロトコルを、 nginx で振り分けたい場合に、役立つ方法を紹介します。
80 ポートや 443 ポートのみ許可しているセキュアなネットワーク環境へ、複数のプロトコルを持ったサービスを提供するといった場合に使える方法です。
nginx によるリバースプロキシの振り分け方法として、以下の6つが存在します。
- IP アドレスでの振り分け
- ポート番号での振り分け
- SSL/TLS の SNI による振り分け
- HTTP のパスによる振り分け
- HTTP の Host ヘッダーによる振り分け
- スクリプトによる振り分け
本記事では、 方法 6 のスクリプトによる振り分け方法を紹介します。
他の方法に対して方法 6 のメリットは、柔軟にルールを記述できることです。
デメリットは、他の方法と比べて遅いということです。 nginx が 2017 年に発表した資料によると、 nginScript を呼び出すだけで 10 %の低下。正規表現を使い振り分けを行った場合、30%ほどの低下。アクセス数が多いサービスへの導入には、他の方法も考慮したほうが良いです。
nginx で使用できるスクリプトとして Lua 等がありますが、本記事では、 nginScript での方法を紹介します。nginScript は、 nginx を設定するための JavaScript 実装です。他のスクリプトに対して nginScript を利用するメリットは次の通りです。
- 導入が簡単
- nginx の Docker イメージ で利用可能
- nginx の Linux 向けパッケージ を利用した場合 nginx-module-njs パッケージをインストールするだけで利用可能
- nginx 公式がサポートしているので、安心感がある
nginx のスクリプト以外に、 Go 言語で実現する方法もあります。本記事では、 nginx に限定します。
通信プロトコルごとにアクセスを振り分ける
例では、 TCP 80 番ポートへのアクセスに対して、クライアントから送られてきたデータを見て、HTTPか HTTP 以外の通信プロトコルかを判断させています。簡易的に GET メソッドのみ実装しています。
nginScript を利用するための nginx の設定。
load_module "modules/ngx_stream_js_module.so"; events { worker_connections 1024; } ... stream { js_include stream.js; js_set $server_url server_url; server { listen 80; js_preread preread; proxy_pass $server_url; } }
js_include ディレクティブで、ロードさせたいスクリプトを指定します。
load_module ディレクティブは、 events ディレクティブよりも前に書きます。後に書いた場合は、エラーが発生します *1
nginx の設定中の js_set で宣言した変数の値は、 nginScript 内に書かれた関数の戻り値で、決定できます。 変数の値が決定されるタイミングは、 nginx のディレクティブに依存します。
続いて、 stream.js の中身。
var server_url_ = ''; var servers_ = { http: '127.0.0.1:8080', echo: '127.0.0.1:8081' }; function preread(s) { s.on('upload', function(data, flags) { if (data.length === 0) { s.log('Continue to read.'); } else if (data.startsWith('GET')) { s.log('HTTP protocol with GET method.') server_url_ = servers_.http; s.done(); } else { s.log('ECHO protol.'); server_url_ = servers_.echo; s.done(); } }); } function server_url(s) { return server_url_; }
preread 関数で、振り分け先のサーバを決定しています。この後に server_url 関数が呼ばれ、戻り値として振り分け先のサーバを proxy_pass ディレクティブに渡します。
preread 関数で、クライアントから nginx への送信データを コールバック関数で受け取るようにしています。コールバック関数内で、データを見て、HTTP の GET メソッドか、それ以外のプロトコルとして分岐させています。なお、 if (data.length === 0)
については、コールバック関数が最初に呼ばれると data が空文字になるので、ガード条件として設けています。
s.done() を呼ぶことで preread のフェーズを完了させ、次のフェーズに移行します。 nginx のフェーズについては、以下のドキュメントが参考になります。
ドキュメント
参考にしたドキュメントをまとめて列挙します。
- https://www.nginx.com/blog/tcp-load-balancing-udp-load-balancing-nginx-tips-tricks/
- http://nginx.org/en/docs/http/ngx_http_js_module.html
- http://nginx.org/en/docs/stream/ngx_stream_js_module.html
- http://nginx.org/en/docs/njs_about.html
おわりに
ひとつのポートで異なる通信プロトコルを振り分ける方法を紹介しました。 nginScript を使うことで、nginx の静的な設定ファイルでは実現できなかった、通信データから判断して振り分けることを確認できました。
オプティムでは、nginx に限らず様々なことに興味があるエンジニアを募集しています。