Synthetic Monitoring ServiceにElasticsearch Machine Learningを適用し、Webサイト性能の劣化と改善を自動検知した

私の所属する[Airitech株式会社](http://www.airitech.co.jp/)では、Webサイトの[Synthetic Monitoring サービス](http://www.airitech.co.jp/synthetic_monitoring/)（コードネームは「ラディカルグッドスピード」）を開発中です。

監視対象のWebサイトの性能劣化や改善を見つけたいのですが、サイトごとに普段の性能が全く異なるので、一律の閾値では見つけることができません。
そこで、ElasticsearchのX-Pack Machine Learningを利用して、異常検知機能を追加しました。

短期間で異常検知機能を実現することができたので、X-Pack MLを適用方法や適用結果、得られた知見をご紹介します。

# X-Pack MLとは
Elasticsearch上で動作する有償のプラグインで、機械学習を用いた時系列データの異常値検知機能を提供するものです。
最大の売りは、機械学習の知識がなくても、異常検知が使えます！という点で、UIから設定するだけで、異常検知が利用できます。

※参考
・[X-Pack ML紹介ブログ](https://www.elastic.co/jp/blog/introducing-machine-learning-for-the-elastic-stack) ・[X-Pack ML紹介動画](https://www.elastic.co/jp/videos/intro-to-machine-learning-workshop)

# X-Pack MLで異常値検知し、結果をSlackに通知する
実現したのは、X-Pack MLを用いた、監視対象のWebサイトのパフォーマンス改善、劣化の自動検知となります。
また、異常値を検知した際にすぐ気づけるように、検知時にアラートをSlackに投稿できるようにしました。

![システム構成図.png](https://qiita-image-store.s3.amazonaws.com/0/226631/f7a16dba-74ec-7273-1997-0c09c9978ea9.png)

注意点としては、X-Pack MLがElasticsearchとは別プロセスでいくつか立ち上がるので、メモリ容量は多い方が良いというところでしょうか。
今回の環境では、物理メモリ32GBのマシンで、Elasticsearchのヒープは10GBに設定しています。

# 異常値検知対象のデータフォーマット
異常値検知対象となる、Webパフォーマンス計測結果のデータフォーマットは以下となります。
こちらのデータを1h毎に計測して、データをElasticsearchに投入しています。

| No       |       field名 |    データ概要    |
|:-----------------|:------------------|:------------------|
| 1 | @timestamp |性能計測時間|
| 2 | webpage |計測対象のURL|
| 3 | FMP  |[First Meaningful Paint](https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint?hl=ja) <br> ユーザにとって、意味ある画面が表示されるまでの時間|
| 4 | firstPaint |画面に何か表示されるまでの時間|
| 5 | loadTime|画面ローディング完了までの時間|
| 6 | duration|すべての描画処理が完了した時間|

以下のようなデータがelasticsearchに投入されます。

```json
{
  "@timestamp": "2017-12-26T17:28:35.409634+09:00",
  "webpage": "https://www.airitech.co.jp/",
  "FMP": 1174.364,
  "firstPaint": 722.8300004005432,
  "loadTime": 1400.0340003967285
  "duration": 1411.251000404358,
}
```

こちらの値を使って、FMPを時系列プロットすると、以下のように可視化できます。
![FMP時系列データ.png](https://qiita-image-store.s3.amazonaws.com/0/69235/0f524794-d822-15c9-58cc-b88db1075d07.png)

こちらのサイトのFMPは1秒前後で推移していますね。

他にWebパフォーマンス監視で重要な指標として[Speed Index](https://developers.google.com/web/tools/lighthouse/audits/speed-index?hl=ja)という値もあります。
Speed Indexの計測にも対応予定ですので、後日、Speed Indexも異常検知の対象にする予定です。

# X-Pack MLの設定
私たちのサービスでは、

* 性能改善検出
* 性能劣化検出

を別々に異常検知したかったため、2つMLのJob設定を作っています。
両方とも、X-Pack MLのMulti metricを選択して、設定を作成しました。

以下それぞれの設定値となります。

| No       |       field名 |    概要    |   性能改善検出    |   性能劣化検出  |
|:-----------------|:------------------|:------------------|:------------------|:------------------|
| 1 | job ID |  | improve-perf | degrade-perf |
| 2 | Fields(function) | 異常値検知対象フィールド名 | FMP(Low median) | FMP(High median) |
| 3 | Split Data | 異常値学習時のカテゴリフィールド | webpage.keyword | webpage.keyword |
| 4 | Key Fields (Influencers) | 影響因子 | - | - |
| 5 | Bucket span | データ集約期間 | 4h | 4h |

ページ毎に別々に異常を検知したいので、Split Dataに対し、URLを格納している「webpage」を指定しています。

またBucket span(データ集約期間)は4hとしました。

1hに一回の性能計測を行っているのですが、まれに瞬間的なネットワークの遅延で、通常の計測値の10倍程度時間のかかる結果が取れることがあります。
今回はコンテンツの良し悪しによる性能を検出対象としたいので、この遅延は無視したいところです。

そこで、以下のようにしました。

* 4回計測すれば、少なくとも3回は外れ値なしで計測できていたことから、Bucket spanを4hとして4回分の計測値から集約する。
* 異常値検知のfunctionを平均値ではなく、中央値に設定する。

こうすれば、ネットワーク遅延等が原因で発生する頻度の少ない遅延は無視することができます。
(ただし、異常検知が4時間ごとになってしまいます。素早く検出したい場合は、再測定の仕組みが必要です。)

# 異常値検知結果
前述の設定で適用したところ、以下のように、X-Pack MLが異常検知しました。
![improve-detect-single-metric-ぼかし.png](https://qiita-image-store.s3.amazonaws.com/0/69235/6169e832-a0eb-1a89-ca61-164d20dbd894.png)

図の赤い点が異常値であること示しています。

詳細な検知結果はこのグラフ下に表示されます。
![improve-detect-detail-2-ぼかし.png](https://qiita-image-store.s3.amazonaws.com/0/69235/97b89fdd-d61c-49e3-8f00-4dea80422bd4.png)

「typical」はX-Pack MLが予測した値です。
5287msecと予測していますね。

「actual」が実際に計測した値です。
1604msecに改善していることがわかります。

狙い通り、X-Pack MLを用いることで、Webサイトのパフォーマンスが改善したことを検知することができました。

同じように劣化についても検知しました。
![degrade-detect-single-metric-ぼかし.png](https://qiita-image-store.s3.amazonaws.com/0/69235/0c040d13-07d2-0ef5-b141-ba6ec153c31a.png)
![degrade-detect-detail-1-ぼかし.png](https://qiita-image-store.s3.amazonaws.com/0/69235/ef63248e-72a1-1ddf-ded1-2945a3c14d2f.png)

X-Pack MLを利用することで、サイトごとに個別に閾値を設けることなく、改善や劣化を検知することができるようになりました。

# 異常検知時のアラート
性能の改善や劣化を検知できるようになったので、検知結果をリアルタイムにSlackに通知できるようにします。
常にX-Pack MLを見ているわけにもいきませんからねw

アラートの通知の条件ですが、今回は以下のように作成しました。

| No       | 条件内容    |   設定値    |
|:-----------------|:------------------|:------------------|
| 1 | アラート発火のタイミング | 4h毎に発火 |
| 2 | アラート発火条件1 | 異常値スコア(severity)の閾値が50以上 |
| 3 | アラート発火条件2 | FMPが0.5秒以上 |
| 4 | 通知先 | Slack |
| 5 | 通知先に含める内容 | 異常検知時の情報(スコアなど)<br>Kibana Dashboardへのリンク<br>Kibana Saved Searchへのリンク |

異常検知条件には、X-Pack MLのスコアの閾値のほかにFMPが0.5秒以上であることも追加しました。
X-Pack MLの出力するSeverity(Anomaly Score)は実際の値の大小に関係がなく、通常の値からどれくらいの変動があったかで評価が決まります。

そのため、0.1秒が0.2秒に変わったことも、1秒が2秒に変わったこともどちらも2倍ということで、同じ異常値となってしまいます。
前者はネットワーク遅延などにより頻繁に発生するので、コンテンツの問題で発生する後者が埋もれてしまいます。
そこで、アラートのFMPが0.5秒よりも大きな場合でしか検知しないように設定し、コンテンツによる性能劣化・改善が埋もれてしまわないようにしています。

また、アラートからの解析をしやすくするために、アラートのメッセージに、Single Metric ViewerやDashboard、Saved Searchへのリンクも入れました。

以下がWatcherの設定となります。

<details>
<summary>X-Pack Watcherによる異常検知アラート設定</summary><div>

```json:wathcer-setting
{
  "trigger": {
    "schedule": {
      "cron": "0 * * * * ?"
    }
  },
  "input": {
    "search": {
      "request": {
        "search_type": "query_then_fetch",
        "indices": [
          ".ml-anomalies-*"
        ],
        "types": [],
        "body": {
          "sort": [
            {
              "timestamp": {
                "order": "asc"
              }
            }
          ],
          "query": {
            "bool": {
              "must": [
                {
                  "match": {
                    "result_type": "record"
                  }
                },
                {
                  "match": {
                    "job_id": "{{ctx.metadata.job_id}}"
                  }
                },
                {
                  "range": {
                    "record_score": {
                      "gt": "{{ctx.metadata.threshold}}"
                    }
                  }
                },
                {
                  "range": {
                    "timestamp": {
                      "from": "now-{{ctx.metadata.detect_interval}}-{{ctx.metadata.ml_process_time}}",
                      "to": "now-{{ctx.metadata.ml_process_time}}"
                    }
                  }
                }
              ]
            }
          }
        }
      }
    }
  },
  "condition": {
    "compare": {
      "ctx.payload.hits.total": {
        "gt": 0
      }
    }
  },
  "actions": {
    "notify_slack": {
      "transform": {
        "script": {
          "id": "create_partition_notify_for_slack"
        }
      },
      "slack": {
        "message": {
          "to": [
            "#elastic-notify"
          ],
          "text": "Elasticsearch ML Anomaly Detection",
          "attachments": [
            {
              "color": "{{ctx.payload.severityColor}}",
              "title": "{{ctx.payload.severity}}",
              "text": "{{ctx.payload.message}}"
            }
          ]
        }
      }
    }
  },
  "metadata": {
    "quate": "'",
    "link_dashboards": [
      {
        "id": "AWAAR4Z1hIAnOgqnarfW",
        "title": "Performance-Monitoring-Timeseries-hcperf"
      }
    ],
    "subject": "Webサイトの性能が改善しました。",
    "kibana_display_term": 28800,
    "detect_interval": "7d",
    "description": "Webサイトの性能が改善した際に通知します。",
    "threshold": 75,
    "locale": "Asia/Tokyo",
    "kibana_url": "http://localhost:15601/",
    "alert_type": "mla",
    "ml_process_time": "28924s",
    "double_quate": "\"",
    "job_id": "improve-perf",
    "compareOption": {
      "compareType": "gte",
      "operator": "≧"
    },
    "filterByActualValue": false,
    "date_format": "yyyy/MM/dd HH:mm:ss",
    "link_saved_searches": [
      {
        "id": "53f0e7c0-ead5-11e7-b520-6b73813f8f5b",
        "title": "Performance-Result-Summary"
      }
    ],
    "actualValueThreshold": 0.5
  }
}
```

</div></details>

長い・・・。
これを作成、管理するのはかなり大変ですね。

そこで、自分たちで設定・管理用のKibanaプラグインを開発しました。
こちらはOSSとして公開もしています。
[elastic-ml-alert-plugin](https://github.com/serive/elastic-ml-alert-plugin)

UIのみで異常検知時のアラートを設定できます。
![es-ml-alert-plugin-画面.png](https://qiita-image-store.s3.amazonaws.com/0/69235/6cdb15fb-2e5f-071d-b2bd-5951889a2d3f.png)


こちらを使うことで、
・X-Pack MLのJob設定から最適な発火条件などを自動で設定する
・異常検知スコアだけでなく実値もアラート通知条件に設定する
・通知内容にDashboardやSaved Searchのリンクを追加する。
といったことがUI上からできるようになります。
(こちらのOSSについては、別の投稿で紹介します)

こちらのプラグインを利用して、アラート設定することで、X-Pack MLが異常検知時に、以下のようにSlackに通知が来ます。

![Slack通知-合成-ぼかし.png](https://qiita-image-store.s3.amazonaws.com/0/69235/e5200bf9-b2f1-fe50-c489-04afd7f258e4.png)

通知のリンクをクリックすると、対象の異常値で絞り込んだSingle Metric Viewerや、Dashboardを表示することができます。
・Single Metric Viewer
![SingleMetricViewerリンク-ぼかし.png](https://qiita-image-store.s3.amazonaws.com/0/69235/eb537a80-83b4-5f3f-712f-c0b8de7535fa.png)

異常値近辺のデータに絞り込んで、Single Metric Viewer表示します。

・Dashboard
![dashboardリンク2-ぼかし.png](https://qiita-image-store.s3.amazonaws.com/0/69235/ef21169b-0952-8003-c908-720f490c5d0c.png)

異常値近辺のデータに絞り込んで、Dashboardを表示します。

このDashboardの右下のグラフには、html,js,cssの描画処理にかかる時間の時系列推移を表示しています。

このグラフを見るに、こちらのサイトでは、改善前後において、Javascriptの処理がほぼなくなっていることから、
初期描画時のJavascript処理をなくすことで、高速化を図ったということがわかります。
おそらく、Javascriptは遅延ロードさせて、描画が終わった後に処理するようにしたのでしょう。

このように、あらかじめフィルタした結果を表示するようにしているので、解析もはかどります。

# 適用の成果
X-Pack MLの運用を開始して1ヶ月が経ちます。

以下のように、性能劣化・改善を検知できています。
赤いマスが異常値を示しています。

・性能改善の異常検知
![improve-detect-ぼかし.png](https://qiita-image-store.s3.amazonaws.com/0/69235/4fb4ce4e-0d39-a63d-0a60-7ef10c05ef32.png)
・性能劣化の異常検知
![degrade-detect-summary-ぼかし.png](https://qiita-image-store.s3.amazonaws.com/0/69235/e9bac5a0-74da-4986-656b-f7147ad2076d.png)

今のところ、改善したサイトで多いのは、初期描画時のJavascriptの処理をほぼ0に改善しているケースでした。
初期描画に不要なJavascript処理を遅延ロードなどで、取り除くことで改善しているのだと思います。

またとある劣化してしまったサイトでは、描画処理は変わらず、キャッシュの設定のみ変わっていたため、
コンテンツのキャッシュ周りの設定変更が影響していると思われます。

これで、
当初の目的であるWebサイトの性能改善及び劣化を、自動的に見つけることが可能になりました！

# さいごに
今回は、ElasticsearchのX-Packを用いたことで、機械学習の知識のない私でも、異常検知導入を短期間で実現できました。

UIで設定するだけで、即異常検知してくれるというのは、本当に便利だと思います。

機械学習はよくわからないけど、データの異常を検知を利用したいという人にはおすすめと思います。
ぜひ試してみてください。


最後までお読みいただきありがとうございました。

みなさん、よいお年を！