kusanagi で hhvm が落ちた時、何が何でもリスタートする

# hhvm プロセスはあるけど http(s) 応答がないときの monit スクリプト
## 検証環境

* Conoha で配っている [kusanagi](https://kusanagi.tokyo/) ディストリビューション（CentOS7）を使用
* [hhvm](https://docs.hhvm.com/hhvm/) + apache + monit は同一サーバーに配置
* `/var/log/monit.log` など kusanagi ディストリビューションの設定です
* もうすでに WordPress などが稼働しているものとします
 * kusanagi 自身のインストール方法には触れません

## 起きている問題と解決する monit 設定ファイル
PHP 実行環境 hhvm で WordPress を 3 つ動かしたあたりからやたら hhvm が落ちだします。そういうのは想定済みたいで、kusanagi 標準添付の monit の `hhvm.conf` では hhvm の無応答またはプロセスが落ちたことを検出して再起動をかけくれます。しかし、時々次のような現象が起きます:

* hhvm プロセスは残ったままで
* hhvm のポート（9000/tcp）も開いたまま
* でも apache 経由でアクセスすると一切応答がない
* この状態で `service hhvm stop` するとやたら時間がかる
 * 一番ひどいときは 180 秒くらい
* hhvm を再起動すると復活するつまり apache の問題ではない

以上、hhvm プロセスが残ったまま http(s) の応答がないケースにも対応させるために以下 `hhvm.conf` の置き換えで [アプリケーション層](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E5%B1%A4) にて再起動を判断し、また、`service hhvm stop` にやたら時間がかかる現象にも対応します:

```shell:/etc/monit.d/hhvm.conf
check host heartbeathhvm with address heartbeat.selfnavi.com
    start program =   "/bin/systemctl start hhvm.service"
    stop program =    "/bin/systemctl stop hhvm.service" with timeout 360 seconds
    depends on httpd
    if failed
        port 80
        protocol http
        and request /
        with content = "It works!"
        with timeout 3 seconds
    then restart
    if 3 restarts within 3 cycles then unmonitor
    group hhvm
```

`heartbeat.selfnavi.com` は生死判定をするサイトで、同一サーバー内に設置してください。各自設置時はこの名前からは変更しなければなりませんが、この記事では `heartbeat.selfnavi.com` とします。 wget で **It works!** が出力されることを確認しときます。

```php:/home/kusanagi/heartbeat/DocumentRoot/index.php
<?php
echo "It works!";
```

```console
$ wget -qO - http://heartbeat.selfnavi.com
It works
```

以上でわかればここで終わり。もう少し詳しい説明が必要なら読み進めてください:

* 生死判定をするサイトの設置
* monit の設定
* `/etc/monit.d/hhvm.conf` についての検証

## 生死判定をするサイトの設置

1. kusanagi コマンドで apache の設定まで終わらせます
 * Webサイトで使用するホスト名(FQDN) はなんでも構わないのでご自身のものとしてください
 * 上記 FQDN は kusanagi により `/etc/hosts` に追記され、 127.0.0.1 を指すようになります
 * データベースを作らないと先に進んでくれませんので後でけします

    ```shell-session
# kusanagi provision --lamp heartbeat
ターゲットディレクトリは /home/kusanagi/heartbeat に変更されました。
↵
Webサイトで使用するホスト名(FQDN)を入力してください。 例) kusanagi.tokyo
heartbeat.selfnavi.com
Webサイトで使用するホスト名(FQDN)をもう一度入力してください。
heartbeat.selfnavi.com
↵
Let's Encryptを使用される場合、Let's Encrypt の使用規約に同意される必要があります。
使用規約に同意される場合、あなたのメールアドレスを入力してください。同意されない場合、Enterキーを二回押してください。
使用規約は次のURLより確認できます: https://letsencrypt.org/repository/
[Enter]
メールアドレスを再入力してください。
[Enter]
データベース名を入力してください。
heartbeat
データベース名を再度入力してください。
heartbeat
heartbeat のユーザー名を入力してください。
heartbeat
heartbeat のユーザー名を再度入力してください。
heartbeat
データベースユーザ'heartbeat'のパスワードを入力してください。[a-zA-Z0-9.!#%+_-]の文字列が使用できます。最小は8文字以上です。
heartbeat
再度 'heartbeat' のパスワードを入力してください。
heartbeat
Job for nginx.service invalid.
heartbeat のプロビジョニングは完了しました。heartbeat.selfnavi.com にアクセスし、lampをインストールしてください!
完了しました。
```

2. kusanagi コマンドで作ってしまったデータベースを消します
 * `your_mysql_root_password` は、ご自身の mysql root パスワードに置き換えてください

    ```shell-session
$ mysql -uroot -pyour_mysql_root_password
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 1847575
Server version: 10.0.29-MariaDB-wsrep MariaDB Server, wsrep_25.16.rc3fc46e
↵
Copyright (c) 2000, 2016, Oracle, MariaDB Corporation Ab and others.
↵
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
↵
MariaDB [(none)]> drop user 'heatbeat'@'localhost';
Query OK, 0 rows affected (0.00 sec)
MariaDB [(none)]> drop database heatbeat;
Query OK, 0 rows affected (0.48 sec)
MariaDB [(none)]> exit
```

3. 生きていると応答をする php スクリプトの配置

    ```php:/home/kusanagi/heartbeat/DocumentRoot/index.php
<?php
echo "It works!";
```
4. hhvm の設定（ `/etc/hhvm/php.ini` に `hhvm.server.safe_file_access = true` 時のみ）
 * `/etc/hhvm/php.ini`  に `hhvm.server.safe_file_access = true` の行が **あるときのみ**
 * `hhvm.server.safe_file_access = true` 自身を追記する必要はありません

        ```php5:/etc/hhvm/php.ini
hhvm.server.safe_file_access = true
hhvm.virtual_host[heartbeat[prefix] = heartbeat.selfnavi.com
hhvm.virtual_host[heartbeat][overwrite][server][allowed_directories][] = /home/kusanagi/heartbeat/DocumentRoot
```
 * もし追記したのであれば

        ```shell-session
# service hhvm restart
```

5. 動作確認

    ```shell-session
# service httpd reload
# exit
$ wget -qO - http://heartbeat.selfnavi.com
It works
```

## monit の設定

1. 要らない設定ファイルを保存する `disable` ディレクトリを作成し、元の `hhvm.conf` を退避します。

    ```shell-session
# cd /etc/monit.d
# mkdir disable
# mv hhvm.conf disable/hhvm.conf.orig
```

1. `/etc/monit.d` には 500 エラーを検出するよう kusanagi が設定ファイルを作成しているはずですが、わたしは jetpack から監視しているのですべて `disable` ディレクトリにいれてます。 `/etc/monit.d/hhvm.conf` を上記の通り新規で作成し、結局、次のようになりました:

    ```shell-session
# ls -la
total 36
drwxr-xr-x    3 root root   96 Jul 11 16:17 .
drwxr-xr-x. 100 root root 8192 Jul 11 10:39 ..
-rw-------    1 root root  351 Sep  6  2017 alert
drwxr-xr-x    2 root root 4096 Jul 12 10:06 disable
-rw-r--r--    1 root root  431 Jul 11 15:02 hhvm.conf
-rw-------    1 root root  175 Mar  7  2017 httpd.conf
-rw-r--r--    1 root root   51 Sep 13  2015 logging
-rw- ------    1 root root  175 Mar  7  2017 nginx.conf
```

2. apache を使っているので nginx を監視しないようにし、新しくいれた `hhvm.conf` を有効にします:

    ```shell-session
# monit start all
# monit unmonitor nginx
# monit summary
The Monit daemon 5.14 uptime: 19h 55m
↵
Program 'nginx'                     Not monitored
Program 'httpd'                     Status ok
Remote Host 'heartbeathhvm'         Online with all services
System 'kusanagi.cs2cloud.internal' Running
```

## `/etc/monit.d/hhvm.conf` についての検証

動作について説明しておきましょう:

```shell:/etc/monit.d/hhvm.conf（再掲）
check host heartbeathhvm with address heartbeat.selfnavi.com
    start program =   "/bin/systemctl start hhvm.service"
    stop program =    "/bin/systemctl stop hhvm.service" with timeout 360 seconds
    depends on httpd
    if failed
        port 80
        protocol http
        and request /
        with content = "It works!"
        with timeout 3 seconds
    then restart
    if 3 restarts within 3 cycles then unmonitor
    group hhvm
```

* monit で指定される間隔（ `/etc/monitrc` の `daemon` パラメータ: 30 秒）で http://heartbeat.selfnavi.com/ にアクセスして、 **It works!** という文字列が 3 秒以内に応答されることを確認する。
 * もし、3 秒以内に **It works!** が戻ってこなければ、hhvm の再起動をかける
 * 500 エラーでも hhvm の再起動
 * hhvm の再起動は stop, start の順で、stop 時は 360 秒まつ。
* 再起動を 3 回繰り返しても条件が満たさなければ、監視を停止する

###  stop program = "/bin/systemctl stop hhvm.service" with timeout 360

hhvm を停止させる際 `/bin/systemctl stop hhvm.service` が完了するまで最大 360 秒待ってくれるという意味です。 hhvm がなかなか止まってくれない状態を再現するために `hhvm.service` に

```properties
ExecStopPost=/bin/sleep 50
TimeoutStopSec=360
```

を挟みます。全体は以下のようになります（ `TimeoutStopSec="infinity"` も [あるらしい](https://unix.stackexchange.com/questions/227017/how-to-change-systemd-service-timeout-value) のですけど、認識してくれませんでした）:

```properties:/etc/systemd/system/hhvm.service
[Unit]
Description=HHVM virtual machine, runtime, and JIT for the PHP language
Documentation=http://www.hhvm.com/
After=network.target remote-fs.target nss-lookup.target

[Service]
ExecStart=/usr/bin/hhvm --mode daemon
ExecStopPost=/bin/sleep 50
TimeoutStopSec=360

[Install]
WantedBy=multi-user.target
```

書き換えたら

```shell-session
# systemctl daemon-reload
```

`index.php` を **It works!** **以外に** 書き換えて再起動条件を作る。 `with timeout 360 seconds` のおかげで、再起動条件検出から hhvm の停止まで根気よく待ってくれる。

```php:/home/kusanagi/heartbeat/DocumentRoot/index.php
<?php
echo "It works.";
```

```shell:/var/log/monit.log
[JST Jul 11 20:01:29] error    : 'heartbeathhvm' failed protocol test [HTTP] at [heartbeat.selfnavi.com]:80/ [TCP/IP] -- HTTP error: Regular expression doesn't match: No match
[JST Jul 11 20:01:29] info     : 'heartbeathhvm' trying to restart
[JST Jul 11 20:01:29] info     : 'heartbeathhvm' stop: /bin/systemctl
```

`with timeout 360 seconds` がなかったらどうなるか

```shell:/etc/systemd/system/hhvm.service（書き換え前）
stop program =    "/bin/systemctl stop hhvm.service" with timeout 360 seconds
```

```shell:/etc/systemd/system/hhvm.service（書き換え後）
stop program =    "/bin/systemctl stop hhvm.service"
```

```shell-session
# monit reload
Reinitializing monit daemon
```

結果だけみると、再起動してくれるのだが:

```shell:/var/log/monit.log
[JST Jul 11 20:57:14] error    : 'heartbeathhvm' failed protocol test [HTTP] at [heartbeat.selfnavi.com]:80/ [TCP/IP] -- HTTP error: Regular expression doesn't match: No match
[JST Jul 11 20:57:14] info     : 'heartbeathhvm' trying to restart
[JST Jul 11 20:57:14] info     : 'heartbeathhvm' stop: /bin/systemctl
[JST Jul 11 20:57:44] error    : 'heartbeathhvm' failed to stop (exit status -1) -- Program /bin/systemctl timed out
[JST Jul 11 20:58:14] error    : 'heartbeathhvm' failed protocol test [HTTP] at [heartbeat.selfnavi.com]:80/ [TCP/IP] -- HTTP error: Server returned status 503
[JST Jul 11 20:58:14] info     : 'heartbeathhvm' trying to restart
[JST Jul 11 20:58:14] info     : 'heartbeathhvm' stop: /bin/systemctl
[JST Jul 11 20:58:15] info     : 'heartbeathhvm' stopped
[JST Jul 11 20:58:15] info     : 'heartbeathhvm' start: /bin/systemctl
```

stop 時にデフォルトの待ち時間 30 秒を経過したところでタイムアウトが記録されている（[動作解説 CONFIGURATION EXAMPLES のあたり](https://mmonit.com/monit/documentation/monit.html)）ここで monit は動作を打ち切り start はしない:

```
failed to stop (exit status -1) -- Program /bin/systemctl timed out
```

タイムアウト後再び monit から監視がはいるが hhvm は stop のまま、httpd は生きていているので 500 が戻ってくる

```
failed protocol test [HTTP] at [heartbeat.selfnavi.com]:80/ [TCP/IP] -- HTTP error: Server returned status 503
```

500 の応答を受けもう一度再起動がかかるが、hhvm は stop のままなのでタイムアウトにはならず、再起動に成功する。

### with timeout 3 seconds

```shell:/etc/monit.d/hhvm.conf（抜粋）
    if failed
        port 80
        protocol http
        and request /
        with content = "It works!"
        with timeout 3 seconds
```

生死判定をするサイトに応答に 5 秒かけるよう細工を施す:

```php:/home/kusanagi/heartbeat/DocumentRoot/index.php
<?php
sleep(5);
echo "It works!";
```

Resource temporarily unavailable として処理されるようです:

```shell:/var/log/monit.log
[JST Jul 11 16:18:24] error    : 'heartbeathhvm' failed protocol test [HTTP] at [heartbeat.selfnavi.com]:80/ [TCP/IP] -- HTTP: Error receiving data -- Resource temporarily unavailable
[JST Jul 11 16:18:24] info     : 'heartbeathhvm' trying to restart
[JST Jul 11 16:18:24] info     : 'heartbeathhvm' stop: /bin/systemctl
[JST Jul 11 16:19:16] info     : 'heartbeathhvm' start: /bin/systemctl
```

### if 3 restarts within 3 cycles then unmonitor

3 回再起動（within 3 cycles）で 3 回リスタート条件を満たす（3 restarts）ときは監視からはずされる（unmonitor）。
`index.php` を **It works!** **以外** に書き換えて再起動条件を作る。

```php:/home/kusanagi/heartbeat/DocumentRoot/index.php
<?php
echo "It works.";
```

```shell:/var/log/monit.log
[JST Jul 11 21:51:33] error    : 'heartbeathhvm' failed protocol test [HTTP] at [heartbeat.selfnavi.com]:80/ [TCP/IP] -- HTTP error: Regular expression doesn't match: No match
[JST Jul 11 21:51:33] info     : 'heartbeathhvm' trying to restart
[JST Jul 11 21:51:33] info     : 'heartbeathhvm' stop: /bin/systemctl
[JST Jul 11 21:51:33] info     : 'heartbeathhvm' start: /bin/systemctl
[JST Jul 11 21:52:08] error    : 'heartbeathhvm' failed protocol test [HTTP] at [heartbeat.selfnavi.com]:80/ [TCP/IP] -- HTTP error: Regular expression doesn't match: No match
[JST Jul 11 21:52:08] info     : 'heartbeathhvm' trying to restart
[JST Jul 11 21:52:08] info     : 'heartbeathhvm' stop: /bin/systemctl
[JST Jul 11 21:52:08] info     : 'heartbeathhvm' start: /bin/systemctl
[JST Jul 11 21:52:43] error    : 'heartbeathhvm' failed protocol test [HTTP] at [heartbeat.selfnavi.com]:80/ [TCP/IP] -- HTTP error: Regular expression doesn't match: No match
[JST Jul 11 21:52:43] info     : 'heartbeathhvm' trying to restart
[JST Jul 11 21:52:43] info     : 'heartbeathhvm' stop: /bin/systemctl
[JST Jul 11 21:52:43] info     : 'heartbeathhvm' start: /bin/systemctl
[JST Jul 11 21:53:14] error    : 'heartbeathhvm' service restarted 3 times within 3 cycles(s) - unmonitor
```