mod_proxy_hcheck で BalancerMember の healthcheck

最近は 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](http://httpd.apache.org/docs/2.4/mod/mod_proxy_hcheck.html) が追加されました。

## 設定例

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

```
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) でも状態が保存されたままとなります。
`BalancerPersist` を `On` にすると `*.persist` というファイルに 
`hcpasses`, `hcfails` の設定値もカウンターも `hcuri` なども保存されています。そして、この状態では設定ファイルを変更して restart しても balancer の設定は `*.persist` ファイルから読み込まれるため反映されません。
stop して `*.persist` ファイルを削除してから start させるか、balancer manager インターフェースから変更する必要があります。

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

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

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

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

ちなみにヘルスチェックでない通常のアクセス時に ProxyTimeout にひっかかると 502 エラーを返します。他の worker への retry はされません。接続できなかった場合は retry されます。
また、その worker の状態が正常である限りアクセスは割り振られます。これを回避するためには `failontimeout=On` を設定することで1度タイムアウトが発生したらエラー状態となり、一定時間（`retry`設定）アクセスを割り振らなくなります。
その間にヘルスチェックでダウン状態と判断されるか復旧すると被害を抑えられそうです。

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

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

```c:mod_proxy.h
/* The watchdog runs every 2 seconds, which is also the minimal check */
#define HCHECK_WATHCHDOG_INTERVAL (2)
```

## まとめ

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