Edited at

mod_proxy_hcheck で BalancerMember の healthcheck

More than 1 year has passed since last update.

最近は nginx が優勢で新たなプロジェクトで Apache httpd を選択することは少なくなっているかもしれませんが、昔から稼働してるものはまだ Apache httpd が多いのではないでしょうか。Apache ではさばききれないっていうサービスばかりではないですし、mod_rewrite による超絶技巧が仕込んであったりして移行が厳しいとか。

さて、そんな Apache httpd は mod_proxy_balancer を使うことで HTTP や AJP のロードバランサーとして機能させることができ、balancer manager からオンラインでメンバーサーバーの状態を変更することができます。が、ヘルスチェック機能がなく、突然不調になったサーバーを自動で無効にしたいという場合に不便でした。エラーとなった場合に一定時間アクセスを振り分けないという設定はできるのですが、きちんと復旧するまでアクセスを振り分けないということができませんでした。(他の監視システムとか Consul と consul-template とかで外から操作することは可能だけど面倒くさい)

そんなこんなで、やっぱり HAProxy とかがいいかなあなんて思っていたところ Apache 2.4.21 で mod_proxy_hcheck が追加されました。


設定例

ドキュメントにある例は次のようになっています

ProxyHCExpr ok234 {%{REQUEST_STATUS} =~ /^[234]/}

ProxyHCExpr gdown {%{REQUEST_STATUS} =~ /^[5]/}
ProxyHCExpr in_maint {hc('body') !~ /Under maintenance/}

<Proxy balancer://foo>
BalancerMember http://www.example.com/ hcmethod=GET hcexpr=in_maint hcuri=/status.php
BalancerMember http://www2.example.com/ hcmethod=HEAD hcexpr=ok234 hcinterval=10
BalancerMember http://www3.example.com/ hcmethod=TCP hcinterval=5 hcpasses=2 hcfails=3
BalancerMember http://www4.example.com/
</Proxy>

ProxyPass "/" "balancer://foo"
ProxyPassReverse "/" "balancer://foo"

ProxyHCExpr で worker が有効かどうかを判別する条件を指定します。HTTP Response の status code や body に含まれる文字列を判断材料にできます。

これに名前をつけて BalancerMember 指定の中で使うことができます。

hcmethod はヘルスチェックアクセスの HTTP メソッド、hcinterval はチェックの間隔(秒)。

hcpasses で何回成功したサーバを正常とするか、hcfails で何回失敗したらダウンと判断するかを指定します。

後で説明しますが、正常な状態では失敗だけがカウントされ、ダウン状態では成功だけがカウントされます。


設定項目

Parameter
Default
Description

hcmethod
None
OPTIONS, HEAD, GET から選択

hcpasses
1
何回成功したら正常とみなすか

hcfails
1
何回失敗したらダウンとみなすか

hcinterval
30
ヘルスチェックの間隔

hcuri

監視する URL の path

hctemplate

worker ごとに同じ設定を何度も書かなくて済むようにテンプレート登録する

hcexpr

成功とみなす条件、未指定の場合は HTTP Code の 2xx, 3xx が正常とみなされる


監視リクエスト

監視リクエストは次のように組み立てられます

wctx->req = apr_psprintf(ctx->p,

"%s %s%s%s HTTP/1.0\r\nHost: %s:%d\r\n\r\n",
method,
(wctx->path ? wctx->path : ""),
(wctx->path && *hc->s->hcuri ? "/" : "" ),
(*hc->s->hcuri ? hc->s->hcuri : ""),
hc->s->hostname, (int)hc->s->port);

次のような設定の場合

<Proxy balancer://test-lb>

BalancerMember http://backend1:8080 method=GET hcuri=/healthcheck
BalancerMember ...
</Proxy>

それぞれの変数はこのようになり

変数名

wctx->path
(null)

hc->s->hcuri
/healthcheck

hc->s->hostname
backend1

hc->s->port
8080

リクエストはこうなります

GET /healthcheck HTTP/1.0

Host: backend1

あまりなさそうなケースですが、Proxy 先がホストやポート毎に Path の異なる次のような場合は

<Proxy balancer://test-lb>

BalancerMember http://backend1:8080/abc method=GET hcuri=/healthcheck
BalancerMember http://backend2:8080/xyz method=GET hcuri=/healthcheck
...
</Proxy>

wctx-path/abc/xyz が入るため、リクエストは GET /abc//healthcheck となります。/ が重なって気持ち悪いですね。

どうしてこのようなコードになっているのか Subversion のログを見てもわからなかったのですが issue を上げたほうが良いのかな。

Host ヘッダーは BalancerMember で指定する URL のホスト部分です。Proxy 先が NameBased VirtualHost で ProxyPreserveHost On でないと機能しないような場合は困りますね(これもあまりなさそうではありますが)。また、HAProxy の様に任意のヘッダーを追加することもできません。

hc->s->hcuri がどうやっても空っぽでおかしいなと思ったら bug でした

https://bz.apache.org/bugzilla/show_bug.cgi?id=60038

これは監視用の URL が常に空っぽ GET HTTP/1.0 となるということで致命的なわけですが、この状態でリリースされてしまうというのがこのモジュールの現在の扱いというわけです。もう続きを読むのをやめましょうか?


カウンタの仕様

hcpasses, hcfails で helthcheck に何回成功すれば有効にし、何回失敗すれば無効にするかを指定できますが、成功したり失敗したりする状態ではどうなるか


  • Down 状態では pass だけがカウントされる

  • Up 状態では fail だけがカウントされる

  • Up / Down の状態が変わるときだけカウンタがリセットされる

という仕様のようです。ということで一度増えた fail カウントは Down になるまでリセットされることなく増え続けます。一時的なタイムアウトが少しづつ溜まった場合にも Down となります。逆に Down 状態で時々成功するといった場合にも復活してしまうことになります。

hcpasses=5, hcfails=5 の場合の動作

f f f f f (Down) f f f f s s s s f f f f f f f f f f f f s (Up)

1 2 3 4 5 - - - - 1 2 3 4 - - - - - - - - - - - - 5

f s s f s f s s s f s s s s s s s s s s s s s s s s f (Down)

1 - - 2 - 3 - - - 4 - - - - - - - - - - - - - - - - 5


状態の保存

BalancerPersist がデフォルトの Off では Restart (SIGHUP) / Graceful Restart で状態はクリアされてしまいますが、On にしておくと Graceful Restart はもちろん、Stop / Start, Restart (SIGHUP) でも状態が保存されたままとなります。

BalancerPersistOn にすると *.persist というファイルに

hcpasses, hcfails の設定値もカウンターも hcuri なども保存されています。そして、この状態では設定ファイルを変更して restart しても balancer の設定は *.persist ファイルから読み込まれるため反映されません。

stop して *.persist ファイルを削除してから start させるか、balancer manager インターフェースから変更する必要があります。


ヘルスチェックのタイムアウト

mod_proxy_hcheck の設定にはタイムアウトに関するものがありません。これは困ります。調べてみたところ ProxyTimeout の値が適用されるようです。未設定であれば core の Timeout の値となります。いずれも秒での指定です。

ここにもちょっとした罠(?)があります。mod_proxy_hcheckmod_watchdog を使って ProxyHCTPsize で設定した Thread 数(デフォルト16)を使ってヘルスチェックを行います。watchdog のイベントは2秒おきに発生しますが、ヘルスチェックのレスポンスが遅いと2秒おきにドンドン Thread を埋め尽くし、queue に溜まって行きます。そして、他の正常な worker の監視に影響を与えてしまう可能性があるのです。

ProxyTimeout は全体に影響してしまうので、どうしても遅い処理があったりすると困りますが、十分に短い秒数をセットするのが良さそうです。

ちなみにヘルスチェックでない通常のアクセス時に ProxyTimeout にひっかかると 502 エラーを返します。他の worker への retry はされません。接続できなかった場合は retry されます。

また、その worker の状態が正常である限りアクセスは割り振られます。これを回避するためには failontimeout=On を設定することで1度タイムアウトが発生したらエラー状態となり、一定時間(retry設定)アクセスを割り振らなくなります。

その間にヘルスチェックでダウン状態と判断されるか復旧すると被害を抑えられそうです。


ヘルスチェックのインターバル

hcinterval 設定でヘルスチェックの監視間隔を秒単位で指定できますが、2秒未満にすることはできません。


mod_proxy.h

/* The watchdog runs every 2 seconds, which is also the minimal check */

#define HCHECK_WATHCHDOG_INTERVAL (2)


まとめ

えっ!っていう致命的なバグがあったりしてまだ不安ですが、興味を持った方のフィードバックによって改善されて行くのではないでしょうか。