2017.02.24

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


nginx
こんにちは、次世代システム研究室のN.O.です。
先日部内でnginxproxy_next_upstreamの挙動についての共有があり、ロードバランサーとして設定する際に気をつけるポイントがあることを知りました。現在のプロジェクトでもちょうど環境を整備しているところでしたので、調べた結果を共有します。nginxを既に運用されている方にとっては何を今更、という内容ですがご了承ください。

本ブログ時点でのnginxのバージョンは1.10.3です。

proxy_next_upstream

ロードバランサーの設定を見る前にproxy_next_upstreamについて解説します。proxy_next_upstreamはロードバランス先となるupstreamのサーバがトラブルなどで応答出来ない時などに、別のサーバにリクエストを再送します。なお再送されるのはGETなどのメソッドとなっており、POST, LOCK, PATCHといったnon-idempotentに分類されるメソッドは再送されません。(※ 以前のバージョンではnon-idempotentのメソッドも再送されたようです。1.9.13で修正されました)。proxy_next_upstreamは指定された回数(proxy_next_upstream_tries)と時間(proxy_next_upstream_timeout)の間、次のサーバにリクエストを再送し続けます。

nginxでのロードバランサー設定

それではロードバランサーの設定をhttp://nginx.org/en/docs/http/load_balancing.html#nginx_load_balancing_configurationを例に見ていきます。
http {
    upstream myapp1 {
        server srv1.example.com;
        server srv2.example.com;
        server srv3.example.com;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
        }
    }
}
ここにデフォルト値を明記するとこのようになります。※他にもデフォルト値はありますが、proxy_next_upstreamに関連する項目のみ書き出しています。
http {
    upstream myapp1 {
        server srv1.example.com max_fails=1 fail_timeout=10;
        server srv2.example.com max_fails=1 fail_timeout=10;
        server srv3.example.com max_fails=1 fail_timeout=10;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
            proxy_next_upstream error timeout;
            proxy_next_upstream_tries 0;
            proxy_next_upstream_timeout 0;
            proxy_connect_timeout 60s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
        }
    }
}
このようにproxy_next_upstreamがデフォルトで有効になっていることがわかります。このときerrorおよびtimeoutが発生した場合、かつnon-idempotentではないリクエストに限り再送されることになります。proxy_next_upstream_triesとproxy_next_upstream_timeoutのデフォルトは0となっており、これは無制限を意味します。またtimeoutはproxy_connect_timeout、proxy_send_timeout、proxy_read_timeoutを指し、いずれもデフォルトは60秒です。

upstream serverのmax_failsとfail_timeout

nginxはリクエストが失敗した時に、そのサーバをバランス先となるupstreamから外す機能があります。これはPassive Health Monitoringとしてドキュメントに記載されています。このPassive方式のヘルスチェックではmax_failsで指定された回数リクエストに失敗すると、fail_timeoutに指定された時間だけupstreamから外れます。なお失敗とみなされるのはerror, timeout, invalid_headerとなっています。
  • error: upstreamのサーバとのコネクションの確立に失敗する
  • timeout: upstreamのサーバとの通信がtimeoutに達した時に失敗とみなす
  • invalid_header: upstreamのサーバが返したレスポンスヘッダが不正だったとき失敗とみなす
errorは対向のサーバがダウンしている時などが該当します。timeoutはproxy_connect_timeout、proxy_send_timeout、proxy_read_timeoutいずれかの閾値が該当します。invalid_headerは様々な理由がありそうですが、対向のサーバが正常に動作していないといったことが考えられます。

またproxy_next_upstreamでhttp_500, http_502, http_503, http_504が定義されている時は、これらも失敗とみなされます。

proxy_next_upstream_triesとproxy_next_upstream_timeout

proxy_next_upstream_triesとproxy_next_upstream_timeoutはデフォルトで無制限となっており、nginxはバランス先のサーバに再送し続けます。

proxy_next_upstreamのtimeout

timeoutはproxy_connect_timeout、proxy_send_timeout、proxy_read_timeoutを区別しません。そのためリクエスト先のサーバが処理済、未処理にかかわらずtimeoutに達すると別のサーバにリクエストを再送します。これは場合によっては2重の処理が発生する可能性があります。

proxy_next_upstreamをoffにした場合

proxy_next_upstreamをoffにすると、リクエストがerror, timeout, invalid_headerとなった場合、クライアントには502 bad gatewayが返ります。後の動作は当該のサーバがfailとなった場合同様に、max_failsに達していた場合はfail_timeoutの期間バランス対象から外れます。またproxy_next_upstream offの場合、http_500, http_502, http_503, http_504をfailとすることはできませんでした。これらの設定はあくまでproxy_next_upstreamが有効な場合に利用できるようです。

nginxのロードバランスで気をつけるポイント

実際のリクエストをヘルスチェックとして利用している

Passive方式のヘルスチェックでは実際のリクエストをヘルスチェックとして利用します。そのため事前にヘルスチェックして問題のあるサーバをupstreamから外すといったことができません。

upstreamから外れたサーバは復旧の有無にかかわらず、fail_timeout経過後にupstreamに戻る。

ヘルスチェックにて一度upstreamから外れても、fail_timeout経過後にupstreamに戻ってしまいます。このときバランス先のサーバがトラブルを抱えたままですと再度実際のリクエストが飛び、リクエストに失敗することになります。サーバの復旧に時間が掛かる状況が発生すると、少なからずリクエストに悪影響が出ます。

ヘルスチェックの種類が限られている。

通常盤のnginxでは、ヘルスチェックはerror, timeout, invalid_headerと、proxy_next_upstreamで定義した場合の5xx系のステータスコードのみとなっており、カスタムで定義できません。

ソリューション

上記の対策として利用できそうな設定、3rd party moduleおよび有償版のNGINX Plusを簡単にご紹介します。

upstreamから恒常的にサーバを外す

恒常的にバランスから外すのにserverディレクティブにてdownが利用出来ます。これは通常版のnginxで利用可能です。
    upstream myapp1 {
        server srv1.example.com down;
        server srv2.example.com;
        server srv3.example.com;
    }
また動的にupstreamをコントロールすることのできるcubicdaiya/ngx_dynamic_upstreamというmoduleがあります。このmoduleを利用することでよりダイナミックにupstreamのサーバを追加・削除することができますので、デプロイの場面でも活躍してくれると思います。

Activeなヘルスチェック

有償ですがNGINX Plusではhealth checkに関する設定ができます。予算に組み込めそうな時は検討してみるとよいです。
3rd party moduleではyaoweibin/nginx_upstream_check_moduleがあるのですが、このmoduleではnginxのソースコードに多数のpatchを当てる必要がありますので難易度は高めです。

設定例

今回の調査を踏まえ、私が現在参加しているプロジェクト向けに作成した設定です。nginxからupstream先となるアプリケーションサーバでは、ステータスコード500も失敗とみなしたかったため、proxy_next_upstreamにhttp_500も加えています。再送を最低限に抑えるためproxy_next_upstream_triesを1にしたところ再送されなかったため、proxy_next_upstream_triesを2に設定しています。timeout関連はデフォルトが長めのため、短く設定します。またerror_logのレベルをwarnにしますと、upstreamから外れたサーバがログに記録されるようになります。
http {
    upstream myapp1 {
        server srv1.example.com;
        server srv2.example.com;
        server srv3.example.com;
    }
    server {
        listen 80;
        error_log error.log warn;

        location / {
            proxy_pass http://myapp1;
            proxy_next_upstream error http_500;
            proxy_next_upstream_tries 2;
            proxy_next_upstream_timeout 20s;
            proxy_connect_timeout 10s;
            proxy_send_timeout 10s;
            proxy_read_timeout 10s;
        }
    }
}

別記1: ログ出力

proxy_next_upstreamで出力されるエラーログを観察しました。

upstreamから外れたサーバがあるとログレベルwarnで出力されました。
2017/02/22 15:47:12 [warn] 23257#0: *3 upstream server temporarily disabled while connecting to upstream, client: 192.168.0.200, server: , request: "GET / HTTP/1.1", upstream: "http://192.168.0.2:3000/", host: "192.168.0.10"
2017/02/22 13:16:56 [warn] 5644#5644: *41 upstream server temporarily disabled while reading response header from upstream, client: 192.168.0.200, server: , request: "GET / HTTP/1.1", upstream: "http://192.168.0.2:3000/", host: "192.168.0.10"
またerror, timeout, invalid_headerではログレベルerrorで出力がありました。

error
2017/02/22 15:00:04 [error] 23180#0: *113 connect() failed (111: Connection refused) while connecting to upstream, client: 192.168.0.200, server: , request: "GET / HTTP/1.1", upstream: "http://192.168.0.2:3000/", host: "192.168.0.10"
timeout
2017/02/22 15:47:12 [error] 23257#0: *3 upstream timed out (110: Connection timed out) while connecting to upstream, client: 192.168.0.200, server: , request: "GET / HTTP/1.1", upstream: "http://192.168.0.2:3000/", host: "192.168.0.10"
invalid_header
2017/02/22 14:39:29 [error] 23159#0: *77 upstream sent no valid HTTP/1.0 header while reading response header from upstream, client: 192.168.0.200, server: , request: "GET / HTTP/1.1", upstream: "http://192.168.0.2:3000/", host: "192.168.0.10"
なおhttp_500ではログレベルerrorでの出力はありませんでした。http_500で再送が発生した場合、ログレベルwarnでupstream server temporarily disabled while reading response header from upstreamが出力されました。

別記2: proxy_next_upstream_tries 1のときhttp_500で失敗扱いにならない

設定例で触れましたがproxy_next_upstream_tries 1だと再送は起こりませんでした。それなら再送をさせずにhttp_500でupstreamから外すことができるのではないか、と考え試したのですが、期待通りに動作せず、ステータスコード500を返すサーバにリクエストが振られ続ける動作となりました。このためproxy_next_upstream_tries 1はproxy_next_upstream_tries offと同様の動作となりますので注意が必要です。
http {
    upstream myapp1 {
        server srv1.example.com;
        server srv2.example.com; ←500を返すサーバ
    }

...中略...

        location / {
            proxy_pass http://myapp1;
            proxy_next_upstream error http_500;
            proxy_next_upstream_tries 1;
アクセスログ
192.168.0.200 - - [22/Feb/2017:14:11:07 +0900] "GET / HTTP/1.1" 200 37 "-" "curl/7.43.0"
192.168.0.200 - - [22/Feb/2017:14:11:08 +0900] "GET / HTTP/1.1" 500 32 "-" "curl/7.43.0"
192.168.0.200 - - [22/Feb/2017:14:11:09 +0900] "GET / HTTP/1.1" 200 37 "-" "curl/7.43.0"
192.168.0.200 - - [22/Feb/2017:14:11:10 +0900] "GET / HTTP/1.1" 500 32 "-" "curl/7.43.0"
192.168.0.200 - - [22/Feb/2017:14:11:11 +0900] "GET / HTTP/1.1" 200 37 "-" "curl/7.43.0"

あとがき

nginxをロードバランサーとして使用する時のポイントと、いくつかのソリューションについてご説明しました。

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。