監視システムPrometheusとTICK Stackを両方使ってみた

この記事は[ニフティグループ Advent Calendar 2018](https://qiita.com/advent-calendar/2018/nifty)の21日目の記事です。
昨日は@megmismさんの「[モバイル端末環境でのjavascript injectionをするためには](https://qiita.com/megmism/items/a79ab25106c7dd8679d9)」でした。

# はじめに

弊社のサーバ監視はサービスごとにmuninやzabbixなどを選択して使っていますが、中には10年物の監視システムが残っていたりします。
それらの置き換えを目指し、今年度から[Prometheus](https://prometheus.io/)を一部に導入し、試験運用を行っています。

一方で個人としては別途[TICK Stack](https://www.influxdata.com/time-series-platform/)を自宅サーバの監視に使っていたので、比較してみようと思います。
なお本記事では詳細なセットアップ方法は取り扱いませんので、他の記事を参照ください。

# 概要

## Prometheus

![prometheus-overview.png](https://qiita-image-store.s3.amazonaws.com/0/223156/c4a58ec8-1c56-58ca-c43a-ba33371da55d.png)


PrometheusはGoogleの内製監視システムBorgmonにインスパイアされて作られた監視システムです。
元々はSoundCloud社で開発されていたようですが、2018/12現在ではCloud Native Computing Foundationの傘下プロジェクトとなっています。

中核となるPrometheusサーバが

- データ収集
- データ保存
- アラート発報
- クエリ応答

の主要機能を担っており、監視対象サーバに設置したエージェント(exporter)からデータをポーリングするPull型モデルとなっています。

## TICK Stack

![tick-overview.png](https://qiita-image-store.s3.amazonaws.com/0/223156/8ed28be9-7c99-5d5e-d25e-a11ad84ea387.png)


TICK Stackは時系列DBであるInfludDBを中核とした解析システムです。
センサーデータの収集、解析などにも使われてるDBで、Prometheusと構成は似ていますが以下の点で違いがあります。

- エージェント(Telegraf)からInfluxDBにデータを送信するPush型
- アラートはデータ処理エージェント(Kapacitor)が全て担う

グラフ表示にはChronografを使っていますが、Grafanaを使うことも可能です。


#クエリとデータモデル

CPUの値を取ってみます。

## Prometheus (Key-Value + 独自クエリ)

| key| value | timestamp | tags |
| --- | --- | --- | --- |
| node_cpu_seconds_total | 12727474 | 2018/12/21 10:00:00 | instance="hoge", group="fuga", mode="idle", cpu="0" |
| node_cpu_seconds_total | 3460902 | 2018/12/21 10:00:00 | instance="hoge", group="fuga", mode="user", cpu="0" |

Prometheusで収集するデータはざっくり上のようなKey-Valueペアにタイムスタンプとタグを足したようなモデルになっています。
クエリ時には以下のような独自のクエリ(PromQL)で計算します。

```
# 1分間のCPU使用率(全コア平均)
1 - avg(rate(cpu_user_seconds_total{instance="hoge", mode="idle"}[1m]))
```

exporterで公開するデータは大抵累積値で表現されているため、rate()関数で一定時間の差分を取らないと使用率が出せません。
CPU使用率を出すだけで計算が必要で、しかも独自のクエリ言語なので、使いこなすには慣れが必要になってきます。

しかもGrafanaでのグラフ定義にもこのクエリを使うので、表示の度に計算が走ります。
このため、TICK Stackに比べてクエリ負荷は高めです。
Grafanaで表示期間をを広げるとCPU使用率が100%に貼りつくことがあり、運用に難儀しています。
この辺りは[Recording Rules](https://prometheus.io/docs/practices/rules/)で事前計算するなど、調整が必要そうです。

## TICK Stack

#### cpu


| cpu_idle | cpu_user | timestamp | tags |
| --- | --- | --- | --- |
| 10 | 40 | 2018/12/21 10:00:00 | host="hoge", cpu="cpu-total" |

TICK StackはRDBMSのテーブルにタイムスタンプとタグを足したような形です。
Prometheusと異なり、CPU使用率は%の値が返ってきます。

```
# 1分間のuser CPU使用率
SELECT 100 - mean(cpu_idle) FROM cpu WHERE host = "hoge" and cpu = "cpu-total" GROUP BY time(1m)
```

クエリもtime()が登場している以外はほぼSQLです。

と、ここまで見た限りではTICKの方が読みやすく、計算を要する部分も少ないので負荷も軽そうです。
実際、監視対象サーバ5,6台程度の自宅環境ではRaspberry PiでTICK Stackを動かしても十分余裕があります。

# アラート

## アラート管理の問題

一方で監視の肝、アラート機能についてはどうでしょうか。
私の所属するチームでは約500インスタンスのVMを扱っており、100インスタンス規模での冗長構成を取っている箇所もあります。
したがって、適切に制御しないとアラートの雨に埋もれて必要なアラートを見逃す可能性があります。
少なくとも、以下の機能は必要そうです。

1. 1度鳴ったアラートを抑制する
2. 一定時間継続しないとアラートにならない
3. アラートレベルやサービスによって送り先担当者を変える
4. 同一サービスのアラートをまとめる

## Prometheus

```yaml:prometheus.yml(一部)
- alert: 
      expr: 1 - avg by (instance) (rate(cpu_user_seconds_total{instance="hoge", mode="idle"}[1m])) * 100 > 90
      for: 10m
      labels:
        serverity: critical
      annotations:
        identifier: '{{ $labels.host }}'
        description: 'CPU usage over 90%'
```

prometheusのアラート発火条件は設定ファイル(prometheus.yml)に記載します。
上の例では各ホストごとの平均CPU使用率が90%を超えたらアラートが鳴るように指定しています。
`for:10m`の指定でアラート発火までの継続時間を指定でき、2の要件は満たせました。
ついでにアラートに表示するメッセージも作っています。

```yaml:alertmanager.yml(一部)
route:
  group: wait: 1m      // 初回アラートの待機時間
  group_interval: 5m   // (group_byした中で)状態変化があった場合の次回アラート待機時間
  repeat_interval: 1d  // (group_byした中で)状態変化がなかった場合の次回アラート待機時間
  group_by: [ 'alertname', 'group' ]
  receiver: team-default

  routes:
    - match:
        group: frontend
      receiver: team-frontend
    - match:
        group: backend
      receiver: team-backend

receivers:
  - name: team-default
    slack_configs:
      - channel: 'general'
  - name: team-frontend
    slack_configs:
      - channel: 'team_frontend'
  - name: team-backend
    slack_configs:
      - channel: 'team_backend'
```

Prometheusで発火したアラートの制御はAlertManagerが行います。
group_byの設定で、特定のタグごとにアラートをグルーピングすることができます。
グルーピングを行うことで、複数のアラートを1つのアラートにまとめることができます。

サーバごとにアラート発火タイミングにズレがある場合でも、group_waitとgroup_intervalの値を調整することで対応可能です。
またroutesの設定で、タグによってアラート先を振り分けています。

![alertmanager.png](https://qiita-image-store.s3.amazonaws.com/0/223156/8266f74f-d6ce-d32e-73ea-f51033ec34a9.png)

これは実際に弊社環境でSlackに投げられたアラートの例です。複数のアラートが1つのメッセージにまとめられていることがわかるかと思います。
(Slackアラートのテンプレートは次の記事を参考に、一部改変しています。また機密保持のため、ホスト名をマスクしています)
https://medium.com/quiq-blog/better-slack-alerts-from-prometheus-49125c8c672b


以上、Prometheusでは必要な要件を満たすことができました。

## TICK Stack

```:TICKScript
var base = stream
    |from()
        .measurement('cpu')
        .groupBy('host')

base
    |where(lambda: "group" == 'frontend')
    |alert()
        .id('{{ index .Tags "host" }}')
        .crit(lambda: "usage_user" > 90)
        .message('{{ .ID }} is {{ .Level }} host: {{{ index .Tags "host" }})
        .stateChangesOnly()
        .slack()
          .channel('#team_frontend')

base
    |where(lambda: "group" == 'backend')
    |alert()
        .id('{{ index .Tags "host" }}')
        .crit(lambda: "usage_user" > 90)
        .message('{{ .ID }} is {{ .Level }} host: {{{ index .Tags "host" }})
        .stateChangesOnly()
        .slack()
          .channel('#team_backend')
```

TICK Stackの場合はChronografのWebインターフェース上で、ストリームデータ処理エージェントであるKapacitorに処理内容を定義することができます。
Reactive Ectensionsなどストリーム処理系を触ったことがある人はわかりやすいかもしれません。

stateChangesOnly()の指定をすることで、アラートレベルが変わった時のみ発報するようになっています。
振り分けはbaseオブジェクトで共通の設定を流用しつつ、whereでフィルタをかけることで振り分けを行っています。
とはいえ、アラート条件やメッセージの定義はほぼ丸コピー状態なので、イケてない感は否めません。
また、2,4の要件を満たすようなスクリプトは書けませんでした...

Prometheusのようにグルーピングはできていませんが、Slackアラートは以下のような形でやってきます。

![kapacitor-no-groupby.png](https://qiita-image-store.s3.amazonaws.com/0/223156/88275488-dff2-433f-9dec-63223e39e4cd.png)


# まとめ

クエリを投げて可視化する分にはTICK Stackの方が使い勝手がよかったのですが、多数のサーバがある環境下ではアラート管理の面でPrometheusに軍配が上がりました。
数百台規模の環境での監視をする場合は、TICK Stackでは辛くなりそうです。
一方で台数が少ない環境であれば、GUIから手軽に設定が行えるTICK Stackの方が運用が楽でしょう。
ということで、会社ではPrometheus、自宅ではTICK Stackという環境は継続しようと思います。

明日は@hicka04さんの「[【Swift】UISplitViewControllerを使ってiPhoneとiPadのUIを共通のコードで実現させる](https://qiita.com/hicka04/items/b2eac7491abc9c061e07)」です。お楽しみに！