読者です 読者をやめる 読者になる 読者になる

Mercari Engineering Blog

メルカリのエンジニアブログです。技術情報を日々発信していきます。

nginxによるTCPロードバランサー

SREチームの@cubicdaiyaです。今回はnginxによるTCPレイヤーでのロードバランスについて解説します。

ロードバランサーとしてのnginx

nginxはHTTPやTCP、UDP等の複数のレイヤーでロードバランサーとして稼働させることができます。(TCPロードバランサーは1.9.0以降、UDPロードバランサーは1.9.13以降で利用可能です)

また、ngx_http_ssl_modulengx_stream_ssl_module を利用することでそれぞれのレイヤーでTLSを有効化することも可能です。

TCPロードバランサー用のモジュールを有効にする

HTTPレイヤーでロードバランスするためのモジュールはデフォルトで組み込まれますが、TCP(とUDP)レイヤーでロードバランスするにはnginxのconfigureスクリプトに--with-stream(あるいは --with-stream=dynamic)を付与してビルドする必要があります。

cd nginx-1.11.3
./configure --with-stream
make
sudo make install

以下はTCPロードバランサー向けに明示的に組込む必要があるモジュールの一覧です。

$ ./configure --help | grep with-stream         
  --with-stream                      enable TCP/UDP proxy module
  --with-stream=dynamic              enable dynamic TCP/UDP proxy module
  --with-stream_ssl_module           enable ngx_stream_ssl_module
  --with-stream_geoip_module         enable ngx_stream_geoip_module
  --with-stream_geoip_module=dynamic enable dynamic ngx_stream_geoip_module

nginxの標準モジュールで明示的に組み込む必要があるモジュールについてはこのように --with- で始まるオプションが用意されています。一方デフォルトで組み込まれるモジュールについては --without- で始まるオプションが用意されており、特定のモジュールを無効する際に指定します。

# ngx_stream_geo_moduleとngx_stream_map_moduleを無効にする
./configure \
  --with-stream \
  --without-stream_geo_module \
  --without-stream_map_module

nginxをTCPロードバランサー向けに設定する

nginxのTCPロードバランサー設定は stream コンテキストに記述します。(ポート番号やIPアドレスはテキトーです)

http {
    # こっちじゃない
}

stream {
    error_log /var/log/nginx/stream.log info;
    proxy_protocol on;

    upstream grpc {
        server 192.168.0.1:12345;
        server 192.168.0.2:12345;
    }

    server {
        listen 12345;
        proxy_pass grpc;
    }
}

ちなみにUDPロードバランサーとして動かすには listen ディレクティブの引数に udp を追加します。

listen 1234 udp;

このように若干の違いはあるもののアップストリームやプロキシの設定方法はHTTPのロードバランサーの場合とほとんど同じです。一方で、HTTPレイヤーでのロードバランサーに比べるとできることが限られるので注意が必要です。例えばHTTPヘッダの操作なんかは当然できませんし、ログフォーマットの設定もHTTPの場合と違ってできません。

追記(20160926): nginx-1.11.4からstreamコンテキストでlog_formatディレクティブが利用可能になりました。

次にnginxのTCPロードバランサー特有の設定について解説していきます。

TCPロードバランサーでのロギング

nginxでアクセスログを出力するには通常 access_log ディレクティブを利用しますが、stream コンテキストでは access_log ディレクティブが使えないのでかわりに error_log ディレクティブを利用します。また、ログレベルは info 以上である必要があります。

追記(20160926): nginx-1.11.4からstreamコンテキストでaccess_logディレクティブが利用可能になりました。

error_log /var/log/nginx/stream.log info;

ログはこんな感じで出力されます。

2016/08/09 11:38:16 [info] 76796#0: *4 client 127.0.0.1:63501 connected to 0.0.0.0:9999
2016/08/09 11:38:16 [info] 76796#0: *4 proxy 127.0.0.1:63502 connected to 127.0.0.1:9001
2016/08/29 11:38:16 [info] 76796#0: *4 client disconnected, bytes from/to client:78/171, bytes from/to upstream:171/78

アップストリームのサーバに接続元のIPアドレスを伝搬する

一般にHTTPレイヤーのロードバランサーでは X-Forwarded-ForX-Real-IP といったヘッダを利用してアップストリームのサーバに接続元のIPアドレスを伝搬するといったことがよく行われます。

TCPロードバランサーで同じことをやるにはPROXY protocolを利用します。nginxでは proxy_protocol ディレクティブでPROXY protocolのON/OFFを切り替え可能です。(デフォルトはOFF)

proxy_protocol on;

この際、アップストリームのサーバもPROXY protocolに対応する必要がある点に注意しましょう。メルカリではgRPCを利用したサーバの前段にnginxを配置してTCPレイヤーでロードバランスしているのですが、当時開発していた際にgRPCのサーバがPROXY protocolに対応していないことがわかったので@kazegusuriに対応してもらったことがありました。以下のスライドにそのへんの話が少し載っています。

GRPCの実践と現状での利点欠点 / Go Conference 2016 Spring

サーバステータスの取得

nginxをTCPロードバランサーとして動かす場合でも各種サーバステータスの取得はHTTPの場合と同じく、ngx_http_stub_status_moduleを利用します。

http {
    server {
        listen 80;
        location /status {
            stub_status on;
            allow 127.0.0.1;
            deny all;
        }
    }
}

stream {
    # こっちじゃない
}

なお、HTTPの場合と違ってnginxが処理している各リクエストのステータス(Reading、Writing、Waiting)はカウントしないので注意しましょう。( Active Connectionsserver accepts handled requests はカウントされます)

まとめ

nginxによるTCPレイヤーでのロードバランスについて解説しました。若干癖はあるものの、nginxに慣れている開発者であればHTTPレイヤーでロードバランスするのと同じような感覚で設定できるので使いやすいのではないかと思います。