ReverseProxyのretry機能を調査

Apache, nginx, HAProxy の ReverseProxy において、proxy 先の障害をどう回避するかを調べてみます。

Apache

Apache 2.4.x の mod_proxy_balancer, mod_proxy_http を調査対象とします (より正確には CentOS 7 の httpd-2.4.6-45.el7.centos.4.x86_64 です)。

mod_proxy_hcheck については触れません。

次のような mod_proxy_balancer を使わない Proxy 設定では retry は行われません

ProxyPass / http://backend:8080/
ProxyPass / balancer://backend/

<Proxy balancer://backend>
  BalancerMember http://backend1:8080
  BalancerMember http://backend2:8080
  BalancerMember http://backend3:8080
</Proxy>

BalancerMember が複数設定されている場合は、接続できない限りは順に次のサーバーで retry されます、デフォルトでは Member の数だけ試行されます。BalancerMember が1つの場合は ProxyPass がデフォルトのままでは retry されません。接続できなかった Member はエラー状態とされ、BalancerMember オプションの retry (秒) で指定されて時間はリクエストが割り振られなくなります。retry のデフォルトは 60 (秒) です。

  BalanceMember http://backend1:8080 retry=10

全ての Member がエラー状態ではリクエストを捌けなくなるので retry の時間を待たずして全て復活します。ProxyPass のオプションで forcerecovery=Off と指定すれば全滅の場合も retry を待ちます (forcerecovery の default は On です)。

接続のタイムアウトは BalancerMember に connectiontimeout で指定します(デフォルトの単位は秒ですが ms をつけることでミリ秒指定が可能)。接続後のタイムアウトは timeout (秒) です。接続後の timeout ではリトライされません。504 Proxy Error がクライアントに返されます。デフォルトではこのタイムアウトでは Error 状態にならないので次からのリクエストもその Member へ振り分けられます。この場合も Error にするには ProxyPass 設定で
failontimeoutOn にします。サーバーが 500 や 503 を返した場合もデフォルトでは Error 状態になりませんが、failonstatus=500,503 などとカンマ区切りでステータスを並べることで Error 状態にでき、retry 秒間リクエストが割り振られません。

ProxyPass の maxattempts でリトライの回数を指定できます。これを指定すれば BalancerMember が1つでも接続のリトライが可能です。デフォルトでは BalancerMember を全部試すように調整されます。Member の数を超える数を指定すると往復するようなかんじでリトライされます。

ProxyPass / balancer://backend/ maxattempts=10
<Proxy balancer://backend>
  BalancerMember http://backend1:8080 retry=10 disablereuse=On connectiontimeout=2 timeout=2
  BalancerMember http://backend2:8080 retry=10 disablereuse=On connectiontimeout=2 timeout=2
  BalancerMember http://backend3:8080 retry=10 disablereuse=On connectiontimeout=2 timeout=2
</Proxy>

こんな感じで、どれにも接続できないとすると

1 -> 2 -> 3 -> 3 -> 2 -> 1 -> 1 -> 2 -> 3 -> 3 -> …

という順でリトライされました。

ping 設定で各リクエストを送る前に proxy 先が生きているかどうか、あるいはレスポンスが遅くないかを確認するとあります (http://httpd.apache.org/docs/2.4/mod/mod_proxy.html#proxypass) が試してみてもどうもそんな動作をしてないなと思って mod_proxy_http.c を確認したら、POST などリクエストに BODY が含まれる場合だけになっていました。

    do_100_continue = (worker->s->ping_timeout_set
                       && ap_request_has_body(r)
                       && (PROXYREQ_REVERSE == r->proxyreq)
                       && !(apr_table_get(r->subprocess_env, "force-proxy-request-1.0")));

POST で試してみると Expect: 100-Continue というヘッダーが追加されていました。この場合、100 Continue が返ってこないと継続データを送りません。全てが1度の write buffer に収まってる場合は追加で送るデータはありませんが、100 Continue が期待の時間内に返ってこなければ切断してしまうので処理がされません。

[Proxy => Backend]
POST / HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: curl/7.29.0
Accept: */*
Content-Type: application/x-www-form-urlencoded
Expect: 100-Continue
X-Forwarded-For: 127.0.0.1
X-Forwarded-Host: 127.0.0.1
X-Forwarded-Server: ::1
Connection: Keep-Alive
Content-Length: 3

a=b

[Proxy <= Backend]
HTTP/1.1 100 Continue

[Proxy => Backend]
POST データの残り(あれば)

[Proxy <= Backend]
HTTP/1.1 200 OK
Date: Sat, 01 Jul 2017 10:29:25 GMT
Server: Apache/2.4.26 (Unix) OpenSSL/1.0.1e-fips
Last-Modified: Mon, 11 Jun 2007 18:53:14 GMT
ETag: "2d-432a5e4a73a80"
Accept-Ranges: bytes
Content-Length: 45
Keep-Alive: timeout=62, max=100
Connection: Keep-Alive
Content-Type: text/html

Apache のまとめ

接続できてしまったらリトライされません。遅いサーバーはとっとと諦めて次に移るということができない。エラー状態になればダウンしているサーバーへのアクセスを試みることがなくなり、無駄なタイムアウト待ちを減らせますが、retry の指定秒を経過すると実際のサーバーの状態にかかわらず(まだダウンしているかもしれないのに)再度リクエストを割り振られてしまいます。

nginx

nginx mainline repository の RPM package を CentOS 7 で実行して試しています (nginx-1.13.1-1.el7.ngx.x86_64)

nginx 1.9.13 から POST, LOCK, PATCH メソッドの場合、デフォルトではリトライされないようになっています。

nginx で Load Balance & retry を行うには upstream module を使う必要があります。これを使わないで

proxy_pass http://backend.example.com;

とした場合は接続に失敗しても retry が行われません。

upstream backend {
  server backend1:8080;
  server backend2:8080;
  server backend3:8080;
}
...
server {
  location / {
    proxy_pass http://backend;
  }
}

と、upstream module を使うと接続に失敗したり、timeout すると順に次のサーバーに対して retry が試みられます。

proxy 先が1つしか無い場合にも接続に失敗したら retry して欲しい場合にはどうしたら良いか?

upstream backend {
  server backend1:8080;
  server backend1:8080;
  server backend1:8080;
}

同じサーバーを並べることで retry が可能になります (Apache の場合は同一の proxy 先は1つにまとめられてしまいます)。

タイムアウト時の retry が続く場合はどこかで諦めないと10台とかに2秒ずつとか待っていられないのでどこかで早めに打ち切らせるべきです。試行回数を制限したい場合は proxy_next_upstream_tries で回数を指定します。デフォルトでは upstream に指定したサーバーの数となります。Apache と違い失敗した後の試行回数ではなく最初の試行からカウントされます。合計の待ち時間を制限したい場合は proxy_next_upstream_timeout で時間を指定します。retry を繰り返す間にこの時間に到達すると 504 Gateway Timeout がクライアントに返されます。retries と timeout を両方していすることもできます。どちらか先に達した方でエラーが返されます。
接続の失敗やタイムアウトしたサーバーは一時的に無効状態にされ、しばらくリクエストを割り振られなくなります。このしばらくという時間は server のパラーメーター fail_timeout で指定します。デフォルトは10秒です。何回失敗したら無効にするかというと、fail_timeout の間に max_fails 回となっています。デフォルトは fail_timeout=10, max_fails=1 となっているので1度の失敗で10秒間無効にされます。
失敗、失敗と書きましたが何を持って失敗とするかは proxy_next_upstream の値が関係してきます。デフォルトでは接続の失敗とタイムアウトとサーバーから異常なレスポンスがあった場合です。これらは proxy_next_upstream で指定せずとも失敗としてカウントされます。http_500, http_502, http_503, http_504, http_429proxy_next_upstream で指定した場合のみカウントされます。http_403http_404proxy_next_upstream で指定したとしてもカウントされません。proxy_next_upstream はリトライするかどうかの指定でもあり、その意味では設定したもののみがリトライ対象となります。デフォルト値から変更し connecttimeout を指定しなければ、失敗としてカウントされはするもののリトライはされません。

HTTP CODE の 500 や 503 でも失敗としたい場合は proxy_next_upstream http_500 http_503 と指定します。error, timeout, invalid_header は指定しなくても常に失敗として扱われます。http_403, http_404 は指定しても失敗扱いになりません。(POST, LOCK, PATCH でもリトライさせたい場合は non_idempotent を指定します)

サーバーのパラメーター指定は次のようにします。

upstream backend {
  server backend1:8080 max_fail=1 fail_timeout=15;
  server backend2:8080 max_fail=1 fail_timeout=15;
  server backend3:8080 max_fail=1 fail_timeout=15;
}

パラメーターはこれ以外にも weight, max_conns, backup, down が使えます。

失敗が続き、retry 可能な別のサーバーが無くなった場合は最後のレスポンスを返します。すでに全てのサーバーが無効状態であれば 502 Bad Gateway が返されます。

Name Type Default Description

weight number 1 サーバー振り分けの重み付け
mx_conns number 0 同時接続数の上限を指定する。共有メモリを使うように zone が指定されていなければ workker プロセス単位で制限となる。非商用版で使えるのは 1.11.5 以降のみ
max_fails number 1 fail_timeout 単位時間にここで指定した回数の失敗が発生すると fail_timeout の間サーバーを無効にする (リクエストを割り振らない)
fail_timeout time 10秒 max_fails の説明を参照
backup backup でないサーバーが全て無効になった場合にのみ有効になる
down ずっと無効にしておく

有償版の nginx ではさらに resolve, route, service, slow_start という便利そうなパラメーターが使えるようです。

nginx のまとめ

失敗が続く限りは upstream に指定したサーバー全てに順にリトライする。失敗の定義は proxy_next_upstream で行い、リトライの回数、時間の制限は proxy_next_upstream_tries, proxy_next_upstream_timeout で制限する。サーバーの数を超えるリトライ回数は意味をなさない。単位時間(fail_timeout)内の失敗の回数(max_fails)によってサーバーが一時的(fail_timeout)に無効になる。
Proxy 先が1台でも retry したい場合は同じサーバーを複数定義する。

retry したくない場合は proxy_next_upstream off とする (proxy_next_upstream_tries 1 でも良さそうな気がする)。

upstream backend {
  server backend1:8080 max_fail=1 fail_timeout=20 weight=1;
  server backend2:8080 max_fail=1 fail_timeout=20 weight=2;
  server backend3:8080 max_fail=1 fail_timeout=20 weight=3;
  server backend4:8080 backup;
}
...
server {
  location / {
    proxy_connect_timeout 2s;
    proxy_read_timeout 2s;
    proxy_next_upstream_timeout 5s;
    proxy_next_upstream_tries 3;
    proxy_pass http://backend;
  }
}

HAProxy

HAProxy 1.5.18 で確認しました (CentOS 6 の haproxy-1.5.18-1.el6.x86_64 で試したが CentOS 7 でも yum で入るのは haproxy-1.5.18-3.el7_3.1.x86_64 なので同じでしょう)

HAProxy には active check があります、任意の間隔で proxy 先のチェックを行えます。(Apache の mod_proxy_hcheck はまだイマイチで nginx は有償版だけで使える)

Active Check を行わない場合 (リトライしない)

backend test-be
    balance roundrobin
    server test1 backend1:8080
    server test2 backend2:8080
    server test3 backend3:8080

単純に順番に server で指定したサーバーに proxy します。接続できない場合は別のサーバーにトライせずに 503 Service Unavailable を返します。

Active Check を行わない場合 (リトライする)

redispatch を有効にすると接続できない場合は次のサーバー、それでもダメならさらに次のサーバーへとリトライしてくれます。

backend test-be
    balance roundrobin
    option redispatch
    server test1 backend1:8080
    server test2 backend2:8080
    server test3 backend3:8080

Active Check をする場合 (リトライしない)

servercheck パラメータをつけるとそのサーバーに接続できるかどうかをチェックします。default-server でチェック間隔を指定します。inter で通常時の間隔を、downinter で DOWN 状態の間隔、fastinter で DOWN から UP に変わる途中の間隔を。
途中というのは下の例では rise 3 としているので DOWN 状態になると 3 回チェックに成功しないと UP 状態にならなず、1 回成功した後 UP になるまでの間です。rise のデフォルトは 2 で、fall のデフォルトは 3 で、3 回エラーになると DOWN 状態になります。下の例では 1 回の失敗で DOWN になります。

定期的に監視して DOWN 状態になれば DOWN 中はそのサーバーにリクエストを振り分けなくなりますが、それまでは振り分けてしまってエラーを返してしまいます。

backend test-be
    balance roundrobin
    default-server inter 5000 downinter 10000 fastinter 3000 rise 3 fall 1
    server test1 backend1:8080 check
    server test2 backend2:8080 check
    server test3 backend3:8080 check

option httpchk 設定で TCP の接続確認だけでなく HTTP でのチェックができます。レスポンスが 2xx か 3xx であれば成功、それ以外が失敗です。

backend test-be
    balance roundrobin
    option httpchk GET /healthcheck
    default-server inter 5000 downinter 10000 fastinter 3000 rise 3 fall 1
    server test1 backend1:8080 check
    server test2 backend2:8080 check
    server test3 backend3:8080 check

Active Check をする場合 (リトライする)

option redispatch を設定すれば、接続できなかったりしても次のサーバーにリトライしてくれるのでクライアントにエラーを返さないですみます。

backend test-be
    balance roundrobin
    option redispatch
    default-server inter 5000 downinter 10000 fastinter 3000 rise 3 fall 1
    server test1 backend1:8080 check
    server test2 backend2:8080 check
    server test3 backend3:8080 check

通常のリクエストもエラーカウントの対象にする

check に加えてて observe layer4 を設定することで通常のリクエストの処理でエラーになったものも fall のカウントに使われます。短い間隔で check を実行するよりもこの機能を使う方が監視のアクセスによる負荷を減らせます。沢山の HAProxy サーバーから頻繁な監視アクセスがあるとその処理内容によっては負荷が気になるかもしれないので。layer7 も使えます。この場合 100 から 499 と 501, 505 が成功として扱われます。404 Not Found で DOWN になったりしないように。

backend test-be
    balance roundrobin
    option httpchk GET /healthcheck
    default-server inter 5000 downinter 10000 fastinter 3000 rise 3 fall 1
    server test1 backend1:8080 check observe layer4
    server test2 backend2:8080 check observe layer4
    server test3 backend3:8080 check observe layer4

まだ書きかけ