この記事はNGINX,Inc.のブログ記事「TCP/UDP Load Balancing with NGINX: Overview, Tips, and Tricks」を和訳した内容となります。
URL:https://www.nginx.com/blog/tcp-load-balancing-udp-load-balancing-nginx-tips-tricks/
この投稿は、nginx.conf 2016で発表されたNGINX,Inc.のKonstantin Pavlovによるプレゼンテーションを再構成しています。完全版のプレゼンテーションはYouTubeでご覧いただけます。
1:00 | |
1:53 | |
3:31 | |
6:18 | |
8:53 | |
9:43 | |
11:46 | |
12:32 | |
13:05 | |
13:20 | |
14:40 | |
16:25 | |
19:17 | |
20:45 | |
29:26 | |
31:48 | |
33:04 | |
33:34 |
イントロダクション
Konstantin Pavlov:私の名前はKonstantin Pavlovです。私はNGINX,Inc.のシステムエンジニアで、プロフェッショナルサービス部門で働いています。このセッションでは、NGINXで使用しているTCPおよびUDPロードバランサーの機能について説明します。
streamモジュールは2年前、NGINX 1.9で導入されました。それ以来、NGINXのHTTPロードバランシングスタックに加わり、成熟して実績のあるソリューションとなっています。
これから、サポートされているロードバランシング方式、SSLおよびTLSサポートの概要をご説明し、アクティブヘルスチェックなど、NGINX Plusが提供する追加機能についてお話ししたいと思います。
また、いくつかの設定をお見せしたいと思いますが、最小限のものとそうでもないものです。単純なWebアプリケーションファイアウォールを構築する方法など、streamモジュールとnginScriptを使うためのコツをご紹介したいと思います。
1:00 TCPロードバランシング
さっそく設定に行きましょう。
TCPロードバランシングは、非常にシンプルです。ご覧のとおり、upstreamブロックを定義しています。まず、NGINXのメインコンフィギュレーションファイルでstreamブロックを定義し、ドメイン名に2つのMySQLバックエンドを持つupstreamブロックを定義しています。
その後にserverブロック内に、listenソケットを定義してTCPプロトコルをlistenし、私の定義したバックエンドへプロキシしています。とても簡単でシンプルですね。ご覧のとおり、NGINXのHTTP設定と非常によく似ています。
後のスライドでは、より洗練された構成をいくつか紹介します。
1:53 UDPロードバランシング
UDPロードバランシングも、NGINXに追加しました。主な用途として2つあり、それは
高可用性とUDPサービスのスケーリングに役立ちます。
UDPデータグラムがNGINXに入ると、NGINXはパッシブヘルスチェックでバックエンドサービスの健全性を監視しますが、NGINX Plusの場合は、アクティブヘルスチェックを使います。そして、データグラムの接続が生きているサーバーに転送します。
この構成で、いくつかのDNSロードバランシングを行ってみます。2つのバックエンドを持つupstreamブロックを定義しました。このlistenディレクティブはTCPの設定と似ていますが、ここではこのudpパラメータを使って、NGINXにこのポートのUDPをlistenするよう指示しています。
注意すべき点は、NGINX UDPロードバランシングが、バックエンドから1つ以上の応答を受けるよう構築されていることです。DNSの場合は、リクエストが1つ、レスポンスが1つです。
また、UDPロードバランサーからのログを調べられるように、エラーログを定義しました。
3:31 TCP / UDPロードバランサーのチューニング
もちろん、TCPとUDPロードバランサーを微調整することもできます。
前回のスライドでは、重み付きラウンドロビン(Weighted Round Robin)ロードバランシングアルゴリズムを使う、デフォルトのupstream設定のみをお見せしました。でも、他にも選択肢があります。リモートアドレスのハッシュに基づくロードバランシングは、たとえば、IPアドレスに基づいてセッション親和性(永続性)を有効にします。または、最小接続数(最小接続アルゴリズム)を使うこともできます。その場合、NGINXはUDPデータグラムまたはTCP接続を、アクティブ接続が最も少ないサーバーに転送します。
NGINX Plusでは、最小時間ロードバランシング方式も使用できます。接続時間、またはバックエンドから最初のbyte を受信する時間や、最後のbyte (つまり応答全体ということです)を受信するまでの時間をもとに、最も速いサーバーを選択することができます。スライドの右側には、そのメソッドを実装する設定方法を載せています。
HTTPロードバランサーと同様に、サーバーごとのパラメータを定義できます。たとえばweight、サーバーがダウンしているとみなす条件としての失敗した接続の最大数や、それらの失敗した接続が発生する時間範囲などです。また、サーバーを明示的にダウンまたはバックアップサーバーとしてマークすることもできます。
NGINX Plusでは、バックエンドへの最大接続数を設定することもできます。この例では、接続数が20を超えている場合、NGINX Plusは新しい接続を作成しません。このslow_startパラメータは、サーバーの重みを0から公称値まで徐々に移動させるようにNGINXに指示します。これは、バックエンドで何らかのウォーミングアップが必要な場合などに便利です。起動するとすぐ、多数の新しいリクエストが流されることはありません。
このserviceパラメータでDNS SRVレコードを照会することによって、アップストリームグループを設定することもできます。この場合、resolveパラメータも含める必要があります。この構成では、バックエンドサーバーのIPアドレスが変更された場合や、サービスのDNSに新しいエントリがある場合も、NGINXを再起動する必要がありません。
6:18 TCP / UDPアクティブヘルスチェック
前のスライドで触れたように、max_failsパラメータを使ってパッシブヘルスチェックを有効にしましたが、NGINX Plusでは、アクティブで非同期のヘルスチェックも利用できます。
複数のIMAPサーバーの前にロードバランサーがあるとします(スライドには1つしかありませんが、それ以上のものは適合しないためです)。今、IMAPサーバーがあるとして、IMAPサーバーのステータスは実際には組み込みのHTTPサーバーに公開されているとしましょう。
health_checkディレクティブのportパラメータを使用して、ヘルスチェックの送信時にNGINXが通常のIMAPポートではなく、別のポート(ここでは8080)に接続するよう指示します。このmatchブロックでは、NGINXが送信するリクエストと、受け取る特定のレスポンスを定義しています。ここでは、このホストのステータスコードを要求するだけですが、ヘルスチェックをパスするには、コードは200 OKである必要があります 。
またhealth_check_timeoutは低い値に設定していますが、これはヘルスチェックがタイムアウトしてサーバーをダウンとマークするまでに、長時間をかけたくないためです。
もちろん、TCPとUDPの世界では、通常、平文のプロトコルを使用することはありません。たとえば、DNSのヘルスチェックを実装する場合は、16進数でエンコードされたデータを送る必要があります。
この特定の構成では、nginx.orgの DNS Aレコードを要求するペイロードをサーバーに送信します。ヘルスチェックを成功させるには、サーバーはexpectディレクティブで指定された16進数のIPアドレスで応答する必要があります。
8:53アクセス制御と制限
streamモジュールは、いくつかの点でHTTPモジュールと非常によく似ています。このモジュールを使うと、仮想サーバーにアクセスするユーザーを制御し、リソースの使用を制限することができます。
設定は、HTTP serverブロックとほぼ同じです。denyとallowディレクティブを使って、 特定のIPアドレスを持つ、または特定のネットワーク上のクライアントに、サービスへのアクセスを許可することができます。limit_connおよびlimit_conn_zoneを使用して、サーバーへの同時接続数を制限できます。また、必要があれば、バックエンドサーバーとの間でダウンロードとアップロードの速度を制限することもできます。
9:43クライアントのIPアドレスをバックエンドに渡す
TCPとUDPのロードバランサーを使うときの最大の課題の1つは、クライアントのIPアドレスを渡すことです。ビジネス要件ではそれが必要かもしれませんが、おそらくプロキシはその情報を持っていません。もちろん、HTTPには非常に簡単な方法があります。基本的にはX-Forwarded-Forヘッダーなどを挿入するだけです。でも、TCPロードバランサーでは何ができるでしょうか?
可能な解決策の1つは、HTTPベースのPROXYプロトコルを使用することです。バックエンド側でproxy_protocolディレクティブを有効にすれば可能です。NGINXは基本的に、クライアントのIPアドレスとメッセージを受信するプロトコルを含むPROXYプロトコルで着信接続をラップし、バックエンドに渡しています。
これはもちろん、プロキシが受け渡す先のバックエンドも、PROXYプロトコルを理解していなければならないということでもあります。これが主な欠点です。バックエンドがPROXYプロトコルを解することを確認する必要があります。
クライアントのIPアドレスを渡す別の方法は、proxy_bindディレクティブとtransparentパラメータを使うことです。これはNGINXに、クライアントのIPアドレスを使用してバックエンドのソケットにバインドするように指示します。
残念なことに、この方法はNGINX側の設定だけでなく、Linux上でルーティングテーブルを設定し、IPテーブルを編集する必要もあります。一番大変なことは、ワーカープロセッサにスーパーユーザーまたはルートIDを使用させる必要があることです。セキュリティの観点から、これは最も避けたいことです。
11:46 TLS Termination
セキュリティに関して言えば、NGINXがStreamモジュールでTLS暗号化を処理する方法はいくつかあります。
最初の動作モードの1つはTLS Terminationです。listenディレクティブにsslパラメータを含めて構成し、HTTPロードバランサーと同じように、SSL証明書とキーを指定します。
proxy_sslディレクティブを使うと、NGINXはTLSを取り除いて(復号化して)、バックエンドに暗号化されていない接続を転送します。これは、たとえば非TLSアプリケーションにTLSサポートを追加するために使用できます。
12:32 TLS再暗号化
もう1つのモードは、接続を再暗号化することです。
基本的に、NGINXは特定のソケットをlistenし、着信リクエストを復号化し、バックエンドに送信する前に再暗号化します。
やり方は以下のとおりです。proxy_ssl onディレクティブを使ってバックエンドに対するTLS暗号化を有効にし、proxy_ssl_verify onでバックエンドを確認する必要があることを指定し、proxy_ssl_trusted_certificateで証明書の場所を指定します。
13:05 TLSラッピング
もちろん、NGINXでTLSを使う別の方法は、TLS以外のポートでプレーンテキストのリクエストをlistenしている際、バックエンドへの接続を暗号化することです。
13:20ロギング
そして皆さん、ロードバランサーで何が起こっているのかを監視し、分析する必要があることと思います。
現在のリリース [注:この話の時点ではNGINX 1.11.3とNGINX Plus Release 10]では、予備的なログが用意されています。ログは上記で示された形で利用できますが、エラーログだけです。クライアントのIPアドレスとポート、およびサーバーが待機しているIPアドレスとポートが表示されます。
2つのケースそれぞれで、サーバーがバックエンドの1つに接続され、セッションが終了したことがわかります。クライアントとの間、およびアップストリームとの間で一定量のバイトを転送したことがわかります。UDPの場合とほとんど同じです。
ここで課題の1つは、NGINXのエラーログのログ形式を設定できなかったことです。私たちはNGINX Streamモジュールで何らかの変数をサポートする前にロギングを追加しました。そのため、非常に簡潔ですが、実際に拡張することができません。
14:40より良いロギング
幸いにも、私たちは最近Streamモジュールのアクセスログを有効にする機能を追加しました。現在のバージョンのNGINXとNGINX Plusでは、任意の方法でログを再構成できます。[注 – この機能は、この話の後の週にNGINX 1.11.4でリリースされたStream Logモジュールで実装されました。10月下旬にリリースされたNGINX Plus Release 11には含まれています] この方法で、監視またはログソフトウェアで最適に動作するよう設定ができます。これはデフォルトでは有効になっていませんが、NGINX stream設定ブロックでaccess_logディレクティブを指定するだけで済みます。
デフォルトでは、ログエントリはスライドの最後の行のように見えます。HTTPロギングと非常によく似ています。お使いのHTTPログ解析ツールで、それを解析できるものさえあるかもしれません。クライアントのIPアドレス、ローカル時刻、およびプロトコル(TCPまたはUDPのいずれか)が提供されます。接続ステータスについては、HTTPからステータスコードを再利用することにしました。HTTPでNGINXを使っていた人なら、皆さんそれらに精通しているからです。(ここで200は成功したTCP応答を示します。)
次に、クライアント[158]に送信され、クライアント[90]から受信したバイト数を記録します。最後に、セッションに要した全体の時間と、接続に使われたバックエンドのIPアドレスとポートであるアップストリームアドレスを取得します。
もちろん、自分で必要なログフォーマットを定義し、NGINXで使用可能な変数を再利用することができます。
16:25変数
変数といえば、最近、streamモジュールで変数を作成することが可能になりました。これにより多くの方法で設定をプログラムできるようになり、streamモジュールの可能性が大幅に広がります。
Mapモジュールを使うと、他の変数に基づいて変数を構築できます。これは、HTTPブロックとほぼ同じです。Geoモジュールを使えば、クライアントのIPアドレスまたはネットワークに基づいて変数を作成することができます。
また、MaxMind GeoIP地理データを使って、変数を設定できます。クライアントを分割してA / Bテストを行えますが、基本的には、リクエストを送る際は異なるバックエンドを定義しています。そしてもちろん、変数を設定し、後からnginScriptとjs_setディレクティブで使用できます。これについては後で説明します。
こちらが、変数を使った単純なエコーサーバーの例です。
NGINXに、localhost port 2007上のTCPトラフィックと、同じポート上でIPv6上のUDPトラフィックをlistenするように指示します。NGINXには、クライアントのIPアドレスを$remote_addr変数で返させます。
続いて私のラップトップでnetcatを使って、NGINXサーバーに接続します。ご覧のように、クライアントのアドレスを返しています。
TCPロードバランサーのstreamモジュールで変数を使う別の方法は、GeoIPです。
GeoIPモジュールは、特定の変数に投入されます。GeoIPモジュールまたはproxy_passディレクティブで接続を制限することが出来ます。
私は今、リモートアドレスに基づいてクライアントを分割しています。したがって、接続の半分が「機能テスト」バックエンドに送られます。そうすれば、機能がうまく動作しているかどうかを確認できます。残りはプロダクションバックエンドに送られます。
変数のその他の使用例としてこれに限られているわけでは無いですが、前にご紹介したproxy_bind も挙げられます。またproxy_ssl_nameディレクティブで使えます。
NGINXに、バックエンドへのTLS SNI接続にサーバー名を設定するよう指示します。
あとはもちろん、以前のスライドでご紹介したアクセスログもですね。
19:17 nginScriptを使用したTCP / UDPロードバランシングの拡張
nginScriptを使用すると、この設定スニペットは他のスライドとほとんど同じです。レスポンスには、クライアントのリモートアドレスが表示されます。
このnginx.confでは、ダイナミックストリームのnginScriptモジュールを読み込みます。streamブロックでは、特別なjs_includeディレクティブを使います。これはNGINXをstream.jsにアクセスさせますが、ここには私たちが使用するすべてのJavaScriptコードが含まれています。
このserverブロックでは、js_setディレクティブを使用して$foo変数の値を設定します。これは、JavaScript関数の戻り値です。
最後に、TCP接続のその値をクライアントに返します。
stream.jsに、foo()と呼ばれる関数を定義します。「s」はデフォルトでは、その関数をパススルーするセッションオブジェクトです。何か起こっているかどうかを見るためにちょっとロギングをしてみましょう。ストリームオブジェクト内の組み込み変数[s.remoteAddress] として利用可能なリモートアドレスを返しています。
20:45 nginScriptによるTCP / UDPペイロードのフィルタリング
前のスライドで紹介した内容は、かなりシンプルでした。それとは別に、NGINXは、近々ペイロードのフィルタリングができるようになる予定です。ロードバランシングに処理されるデータを実際に調べて判断し、それに応じてトラフィックを変更することができるようになります。ペイロードを変更することができます。
nginScriptを使って、単純なWebアプリケーションファイアウォールを実装する方法を、簡単なデモでお見せします。ここで設定とJavaScriptを確認いただけます。
では、デモを見てみましょう。
注: 下のビデオは、デモの最初の部分を21:50にスキップしています。
[埋め込みビデオのデモ]
29:26 TCP / UDP nginScript:パフォーマンス
JavaScriptで何らかの処理を行っている場合、パフォーマンスが低下します。
ここではNGINXでワーカープロセスを1つ使いましたが、HTTPバックエンドがロードバランサーで処理される典型的なシナリオでは、1秒あたりのリクエストでパフォーマンス低下を観測しました。最初の2行はベースラインシナリオです。
ご覧のとおり、JavaScriptを有効にするだけで、パフォーマンスが約10%低下しました。NOOPはJavaScriptのマシンに関数ハンドラを渡しているものの、実際には何もしていないことを意味します。単純に関数から戻ります。JavaScriptコードを呼び出していますが、何もしません。これだけで、すでに約10%のパフォーマンス低下が生じています。
正規表現を使用すると状況が悪化し、結果、パフォーマンスが30%低下します。Webアプリケーションのファイアウォールは整備されたフィルタリングを行っていますが、それらは遅いです。ある程度予測されることではありますが、それにしても本当に遅いですね。
これらの数値は2010年のXeonサーバーの数値ですので、おそらく皆さんの状況とはだいぶ異なるでしょうが、全体の割合は似るはずです。
31:48 TCP / UDPロードバランサーの将来
スライド内 「streamモジュールはかなり新しいので、フィードバックや、皆さんの用途で価値があると思われる機能のリクエストをお待ちしています」
将来的に、NGINX streamモジュールとUDP / TCPロードバランシングに何を期待されますか?
現時点では、私たちは実際可能性を模索している最中です。あなたが実現したいと思うアイデアや機能があれば、ぜひ一緒に議論させていただきたいと思います。
直近でお約束できるものは以下の通りです。クライアントからのTLS SNIを解析し、ご利用いただける変数、たとえば、proxy_pass内にTLS接続の内容を確認できるようにするものなど、をいくつか用意します。SNIでは、要求されたサーバーのサーバー名を受け取ります。要望があれば、それを特定のバックエンドに転送することができます。
その次は、リスニング側でのPROXYプロトコルのサポートです。PROXYプロトコルのリモートアドレスのような変数も設定します。それらは近い将来に追加される予定です。
現在または今後予定されているストリームロードバランサーの機能では、カバーされていない特定のユースケースがある場合には、ぜひ私たちにお知らせください。
33:04関連する記事
TCPとUDPロードバランシングについては、当社のウェブサイトにいくつかのリソースがあります。管理者ガイド、ブログ記事がいくつか、またこれからも追加される予定です。
nginScriptのドキュメントについては、ソースコードとREADMEファイルを確認してください。 [注- README ファイルは、標準的なリファレンスドキュメントと一連のブログ記事に置き代わりました。]
33:34ありがとうございました
すべての設定スニペットとこれらのスライドをGithubで入手できます。
タグ: TCP/UDP, ロードバランシング