リアルタイムサーバリソースモニタをつくる[dstat => fluentd => Elasticsearch => Kibana]

# 目標

全く更地の状態、しかもfluentdもESもなんとなくしかわかってない状態から、どうにかKibanaでリソース監視Dashboardを作るまでこぎつける。

同じことをやっている記事はいっぱいあるので、fluentdとESをよくわかってない状態で始めようとして躓いたポイントを中心に手続き的に。

**太字になってるところがわかりづらい点・注意を要する点・推奨する内容**。

# 参考

- 公式:
    - Fluent: http://docs.fluentd.org/articles/config-file
    - ES:  https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
- http://blog.nomadscafe.jp/2014/03/dstat-fluentd-elasticsearch-kibana.html
- http://nsrzakki.hateblo.jp/entry/2015/08/02/031315
- ほかにもいっぱい

:white_check_mark: **Tips!** fluentdとESに関しては、**困ったらまず公式Doc！** これ鉄則だと思う。

# Disclaimer

* 「リアルタイム」というのは嘘。ごぬんね
    * 触ってみるとわかるけどポーリング間隔は5秒が最小
    * 毎秒とかやりたければパッチするのかな

# 用意するもの

* `dstat`が使えるLinux（筆者はUbuntu）
* `dstat`
    * `apt-get`なり`yum`なりで
* `td-agent` v0.12.20
    * 上に同じ
* `elasticsearch` **v2.2.0**
    * 上に同じ
* `kibana` **v4.4.0**
    * バイナリDLしてきて解凍……でもいいが、
    * 筆者は気づかずに進めてしまったのだが、`apt`/`yum`レポジトリもある
        * [こちら](https://www.elastic.co/guide/en/kibana/current/setup.html#setup-repositories)参照

**ESとKibanaはバージョンの互換性に注意**が必要。Kibana4.4はES2.2以降を要求する。

# `dstat`ってなんだ

こういうもの
![dstat2.png](https://qiita-image-store.s3.amazonaws.com/0/76980/eecdac80-dda7-27b6-7e5e-030da362d082.png)
リソースの状況をひたすら吐き出してくれる。**ひたすら便利**。

便利だし、これをfluentdに流し込むpluginが公開されている。

# [`fluent-plugin-dstat`](https://github.com/shun0102/fluent-plugin-dstat)

ということで入れる。**`td-agent`でプラグインを入れるときは`td-agent-gem`コマンド**。

```bash
$ sudo td-agent-gem install fluent-plugin-dstat
Successfully installed fluent-plugin-dstat-0.3.1
1 gem installed
```

`sudo apt-get`経由で`td-agent`を入れた場合、何もしないとその後も`sudo`が必要になる。
`td-agent`それ自体はtd-agentユーザで実行されるので、rootではない。

# [`fluent-plugin-elasticsearch`](https://github.com/uken/fluent-plugin-elasticsearch)

ついでに`elasticsearch`にデータを流しこむためのプラグインもいれておく。

```bash
$ sudo td-agent-gem install fluent-plugin-elasticsearch
```

# fluentd(`td-agent`)の設定

そもそもfluentdって何するの？
=> **`source`つまりデータ発生源から、決めた方法で決めた行き先にデータを流し続けるよ。**

起動するだけなら`$ sudo service td-agent start`で一発だが、当然設定しないと意味がない。
`/etc/td-agent/td-agent.conf`をroot権限で開く。

## `source`

```
<source>                                                                    
  type dstat
  tag dstat.__HOSTNAME__
  option -cmdgn
  delay 3
</source>
```

まずは`source`。
さっき`dstat`プラグイン入れたので、`dstat`が`type`に使える。
`type`は入力の種類。

こう書くと、`td-agent`が「`dstat`に`option`を渡して実行した結果」を入力としてくれる。

* `type`にはほかに`tail`とか`forward`とかがある
    * `tail`はそのまんま、既存のファイルの尻から吸い出す。当然ファイル指定が必要
    * `forward`はポートを開いてそこで外部からのPOSTを受け取る。よってポート指定が必要

**`tag`が非常に重要**。
要は`source`から吸ったデータに意味のある識別子を与える。

上では`dstat.hostname`という形になる。`__HOSTNAME__`というプレースホルダでホスト名を代入できるのは[rewrite-tag-filter](https://github.com/fluent/fluent-plugin-rewrite-tag-filter)プラグインの機能で、`td-agent`にセットでついてくる。
`dstat`のオプションについては色々試してみよう。`delay`は3秒毎に出力の意。


## `match`

ここからがややこしくなる。
分かりやすかった参考記事は[こちら](http://qiita.com/harukasan/items/0e69f5c17f12db7b2e98)

`match`ディレクティブは入力をどう扱うか決める要の設定。
こんな感じになる。

```
<match dstat.**>
  type copy
  <store>
    type elasticsearch
    host localhost
    port 9200
    logstash_format true
    logstash_prefix logstash
    type_name dstat
    flush_interval 20
  </store>
  <store>
    type file
    path /tmp/fluent.dstat.log
  </store>
</match>
```

`<match pattern>`この書式で、**`pattern`の部分に対象としたい`source`の`tag`パターンを入れる**。
だからここで的確に取捨選択できるよう、`tag`を設計する必要がある。とりあえず最初は適当でもいいけど。
パターンにはワイルドカード`*`や`**`が使える。詳細は[Doc](http://docs.fluentd.org/articles/config-file#2-ldquomatchrdquo-tell-fluentd-what-to-do)で。

* `match`は上から順に評価して、最初にマッチした設定を使う
* **同じパターンの`match`を複数書いても、あとの設定は到達不可能**
    * だから`copy`が必要になる

こっちにも`type`があり、データの扱い方を決める。
デフォルトで`file`や`forward`があるほか、td-agentのおまけで色々ついてくる。
`copy`はその一つで、**複数出力先に分岐**できる。出力先は`store`ディレクティブで指定。
`store`ディレクティブの中身は、`copy`を使わないで書く場合に`match`直下に来る内容がそのまま入る。

さて、`type elasticsearch`。これもプラグインを入れたので使える。
詳しく見る前に先にESを起動しておこう。

# Elasticsearch(ES)

ESは全文検索システムというやつだ。中で全文検索エンジン[Lucene](https://lucene.apache.org/core/)が動いている。

検索というと`grep`があるが、こいつは（多分）シーケンシャル。
つまり基本的には対象ファイルをズァーっと順になめてマッチをかける。

全文検索システムは違う。事前にインデックスを作る。**[転置インデックス](https://www.wikiwand.com/ja/%E8%BB%A2%E7%BD%AE%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9)**というやつだ。
**「検索される語がどのファイルに含まれているのか」という情報を予め調べあげておいて記録しておく。**
だからいざ検索された時、即座に「はいこれとこれとこれね」と返せる。

こちらはAPI経由で設定からデータ入力から検索から全て行うことになるので、とりあえず起動すればいい。

```
$ sudo service elasticsearch start
```

デフォルトポートは9200、そのままでOK。
設定は後回しにする。

# ふたたび`td-agent.conf`

```
<match dstat.**>
  type copy
  <store>
    type elasticsearch
    host localhost
    port 9200
    logstash_format true
    logstash_prefix logstash
    type_name dstat
    flush_interval 20
  </store>
  <store>
    type file
    path /tmp/fluent.dstat.log
  </store>
</match>
```

戻ってきた。`type`から`port`までは自明だ。

もう一つの`store`の`file`の方も自明だ。
`path`で指定したファイルにもデータを流し込んでいるということ。
とりあえず動いていることを確認するには最適だ。

**が、まだ動かさないほうがいい。**
とりあえず動かしてみたいなら、

```
<match dstat.**>
  type copy
  <store>
    type file
    path /tmp/fluent.dstat.log
  </store>
</match>
```

これで。つまりES部分は「待った」。

ES設定に戻り、`logstash_format`の部分。
[Logstash](https://www.elastic.co/products/logstash)というのはfluentdとはまた違うデータ転送エージェントで、ここではフォーマットをそちらに合わせるということ。
ESとKibana、Logstashは今Elastic社というESやLuceneを開発していたメンバが集まって作った会社が強力にバックアップしており、親和性が高い。
`logstash_prefix`もデフォルトの`logstash`を使うのが早い。

`type_name`を理解するにはESにおけるIndexとTypeを理解すればよい。おすすめの参考記事は[こちら](http://code46.hatenablog.com/entry/2014/01/21/115620)。
**Typeは要はIndexの中のグループみたいなもの。Databaseに対するTableみたいなもの**と思っておけばいい。ここではそれに`dstat`と名前をつけた。

`index_name`も指定できるのだが、**Logstashでは`logstash-YYYY.MM.DD`という名前で1日1インデックスが自動で作成される**ので、ここでは意味が無い。
実はLogstash形式にはサポートが同じ会社という以外にもう一つ利点があるので後述する。

`flush_interval`はデータ転送が実際に行われる間隔（秒単位）。デフォルトは60なので、**起動後1分待たないと最初のデータがESに入らない**。あまり早くし過ぎるとそのためにリソースが食われてしまうが、遅すぎてもリアルタイムっぽくなくて寂しい。
製品環境で使うならチューニングが必要なところだろう。ローカルなら5秒でも余裕。実務上はそこまで細かい必要ないので、30とか60とかか。

これでデータを**流し込む側**の準備ができた。
上記「とりあえず試す場合」をやってみるとわかるが、`td-agent`が実際に吐き出すデータはこうなっている。

```
2016-02-21T02:57:39+09:00	dstat.Naxxramas	{"hostname":"Naxxramas","dstat":{"total_cpu_usage":{"usr":"51.186","sys":"1.017","idl":"47.797","wai":"0.0","hiq":"0.0","siq":"0.0"},"memory_usage":{"used":"3023953920.0","buff":"76234752.0","cach":"467513344.0","free":"576991232.0"},"dsk/total":{"read":"0.0","writ":"15018.667"},"paging":{"in":"0.0","out":"0.0"},"net/total":{"recv":"0.0","send":"0.0"}}}
2016-02-21T02:57:42+09:00	dstat.Naxxramas	{"hostname":"Naxxramas","dstat":{"total_cpu_usage":{"usr":"27.027","sys":"4.730","idl":"68.243","wai":"0.0","hiq":"0.0","siq":"0.0"},"memory_usage":{"used":"2998079488.0","buff":"76251136.0","cach":"472776704.0","free":"597585920.0"},"dsk/total":{"read":"0.0","writ":"76458.667"},"paging":{"in":"0.0","out":"0.0"},"net/total":{"recv":"0.0","send":"0.0"}}}
2016-02-21T02:57:45+09:00	dstat.Naxxramas	{"hostname":"Naxxramas","dstat":{"total_cpu_usage":{"usr":"3.072","sys":"1.365","idl":"95.563","wai":"0.0","hiq":"0.0","siq":"0.0"},"memory_usage":{"used":"2996457472.0","buff":"76251136.0","cach":"467537920.0","free":"604446720.0"},"dsk/total":{"read":"0.0","writ":"0.0"},"paging":{"in":"0.0","out":"0.0"},"net/total":{"recv":"0.0","send":"0.0"}}}
```

`<timestamp> <tag> <record JSON>` これがゴールデンルール。

タイムスタンプとタグはそのまんまなのでいいとして、recordの中身が重要だ。
**全てJSON**で、ESに吐き出されるLogstash形式には`@timestamp`や`hostname`も自動で挿入される。

# データを視るための準備: Kibana

Kibanaのインストール経路がどうであれ、ローカルに落としてきた`kibana`バイナリを実行すれば少なくとも起動するはずだ。
自動起動したい場合はinitスクリプトの用意が必要。(筆者はまだやってない)

デフォルトでは5601ポートで待ち受ける。

![kibana.png](https://qiita-image-store.s3.amazonaws.com/0/76980/d1d1f05a-d28f-2d34-198a-cb7ba8ee08b4.png)


Elasticsearchが動いていれば接続され、上のような画面が出るはず。

この画面が言っているのは、「まだIndexが一つも指定されていなくて何を見たらいいかわからないから教えろ」ということ。

最初から`logstash-*`というインデックスパターンがデフォルト値として待ち受けていて、我々としては好都合。

## 一瞬立ち止まって: Sense/ElasticHQ

[Sense](https://www.elastic.co/guide/en/sense/current/introduction.html)という、これもまたElasticの提供しているKibana用のプラグインがある。
![sense.png](https://qiita-image-store.s3.amazonaws.com/0/76980/4eeca0da-6db8-6b50-75af-ceec96b563ca.png)
左半分で補完やコマンドヒストリーの恩恵を受けながらAPIコールし、右半分で結果をブラウズできるという、いわばElasticsearch用WebConsoleだ。
**ぜひ入れよう。絶対入れよう。**

ElasticsearchにはUIがない。全てはAPIコールだ。
つまりこういった補助具がなければ**複雑な設定まで`curl`に頼ることになる**。[^1]
Senseは単純なUIではあるが存外洗練されており、

- 入出力JSONは自動整形されるし、
- シンタックス(?)ハイライトもあるし、
- Enter改行、Ctrl(Cmd)+Enterでリクエスト送信と直感操作可能、

と行き届いている。普通に他業務でも`curl`の代わりに欲しいくらいだ(ほかにありそうだけど)。

よりGUI的にESを管理したいなら、[ElasticHQ](http://www.elastichq.org/index.html)というのもある。
こちらはElasticsearchのプラグインで、エンドポイントも`<es_hostname>:9200/_plugin/hq/`。
![elastichq.png](https://qiita-image-store.s3.amazonaws.com/0/76980/a63db425-def8-2577-6ca5-a087dafdf419.png)
クラスタの状態監視等に重きをおくならこちらがいいかもしれない。
ヘルスチェックやリソースモニタがあるし、
![nodediag.png](https://qiita-image-store.s3.amazonaws.com/0/76980/6316f60c-75e1-ce7f-ce7e-1c67d1aca861.png)
実用性は疑問だが、クラスタ可視化機能もある。
![nodevisual.png](https://qiita-image-store.s3.amazonaws.com/0/76980/edbea2f2-935d-5e81-641b-cfac789e51c5.png)

こちらにもコンソール的なものがあるのだが、残念ながらSenseと違ってフリーインプットのコンソールではなく、対応したコマンドを送ることしかできない。
今のところ使い分けが必要。

ほかに[elasticsearch-head](https://mobz.github.io/elasticsearch-head/)というのもあったが、見たところUIがちょっと野暮ったいのでスルー。機能的にはHQ寄りのようだった。

# Index Template

お待ちかね、インデックスの時間だ。
といったものの、`fluentd_plugin_elasticsearch`がLogstash形式のインデックスを勝手に作ってくれる。
ここまで設定が終わった状態で`sudo service td-agent start`すると、もうそれだけでESにデータが流れ始める。

が、特にMappingの設定を与えてないので、**全ての値がAnalyzed Stringとしてマップされてしまう**。
これが一旦起動を待ったほうがいい理由。まあ作ってしまったら削除すればいいのだが。
ちなみに、**Mappingは要はスキーマ**だと思えばいい。JSONフィールドの値の型を指定し、StringであればAnalyzed（分かち書きあり）かNon-Analyzed（なし）かも選択できる。

`dstat`のデータはほとんど浮動小数点数なのでStringは不都合。ということでMappingを指定して制御したい。

ESでは`_mapping/`APIでこれを通常制御するのだが、Logstash形式では毎日新しいインデックスが作られる。
毎日Mappingを繰り返すのは現実的じゃないので、**特定パターンの名前を持つインデックスに共通のMappingを適用するIndex Templateという機能を使う**。

1. Index Templateに対象IndexのMappingを指定して登録
    - ＋＠で、[Settings](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html)も登録できる
    - ただし公式[Doc](https://www.elastic.co/guide/en/elasticsearch/guide/current/_index_settings.html)いわく、「慣れるまでいじるな」
2. `td-agent`を稼働させるとLogstash形式のインデックスが作成され、データが流れこむ。テンプレートに指定したマッピングがここで適用される
3. 深夜00:00に日が変わると、やはりテンプレートを元に翌日のインデックスが作成される

ここで、日ごとにインデックスが変わるというLogstashの利点が発揮される。
**テンプレートを更新すれば、次の日のインデックスから自動で適用される**ということ。

ESは膨大な入力データからせっせせっせと転置インデックスを作り続ける。
元となるインデックスの型情報が変更されると、それに合わせて転置インデックスは全て作り直しになる。
よって、既存インデックスの直接的なマッピング変更は認められていない（フィールドの追加は可能、更新は不可）。[^2]

Index TemplateとLogstash形式を使っておけば、**特別な設定なしに、1日未満のギャップで新規マッピングを適用できる**。古いインデックスに対する適用は、必要ならバックグラウンドでやればいいし、必要なければ無視してしまえる。
ということで、さっと始めるにはLogstash形式はまあ適当かと。[^3]

ここまで把握すればあとは見るのが早い。

```bash
PUT _template/logstash-tmpl
{
  "template": "logstash-*",
  "mappings": {
    "dstat": {
      "properties": {
        "dstat": {
          "properties": {
            "total_cpu_usage": {
              "properties": {
                "usr": { "type": "double" },
                "sys": { "type": "double" },
                "hiq": { "type": "double" },
                "idl": { "type": "double" },
                "siq": { "type": "double" },
                "wai": { "type": "double" }
              }
            },
            "memory_usage": {
              "properties": {
                "used": { "type": "double" },
                "buff": { "type": "double" },
                "cach": { "type": "double" },
                "free": { "type": "double" }
              }
            },
            "dsk/total": {
              "properties": {
                "read": { "type": "double" },
                "writ": { "type": "double" }
              }
            },
            "paging": {
              "properties": {
                "in": { "type": "double" },
                "out": { "type": "double" }
              }
            },
            "swap": {
              "properties": {
                "used": { "type": "double" },
                "free": { "type": "double" }
              }
            },
            "net/total": {
              "properties": {
                "recv": { "type": "double" },
                "send": { "type": "double" }
              }
            }
          }
        }
      }
    }
  }
}
```

見ての通り、`td-agent`が出力するJSONの階層に合わせて`properties`キーを挟んでネストしていき、末端のキーにたどり着いたところで`type`を指定するだけ。
**`mappings`キーの直下に来るのはType名で、それ以降がJSONrecordのキーであることに注意**。わかりにくければ`td-agent.conf`で`type_name`を別のにする。
キーの名前は`fluentd-plugin-dstat`の出力そのまま。

`template`フィールドで対象インデックス名のパターンを指定したら完成。Senseコンソールから送信する。

`{ "acknowledged": true }`が帰ってくれば準備は完了。`td-agent`を起動する。

# Kibana Visualization

`flush_interval`で指定した秒数以上待てば、ESにデータが入るはず。
ここまでで既に起動してしまっていた場合、例の**analyzed stringでマップされた状態になってしまっているので、グラフ表示等が使えない**。
まだデータが少ないので、一旦`td-agent`を`stop`して、インデックスを削除してやり直すのが早い。

```bash
DELETE logstash-YYYY.MM.DD
```

これをSenseから送信する（YYYY.MM.DDはやった日付を代入）。それからまた`td-agent`を再起動。

正しくデータが入り始めると、Kibanaで認識できるようになる。

![indexready.png](https://qiita-image-store.s3.amazonaws.com/0/76980/00b90da3-cea2-57a9-007e-763aa2d0858e.png)

Index name or patternを指定した際に、Time-field nameに`@timestamp`が読み込まれていればLogstash形式のインデックスが認識されているとわかる。この状態でCreateを押す。

![mappings.png](https://qiita-image-store.s3.amazonaws.com/0/76980/5ef59ac8-389f-e6a1-95b2-d6fd544ec3b0.png)

Typeが正しく認識されて、Indexが検索可能になった。

あとの操作は結構直感的なので、大して説明も必要ないと思う。

1. Visualizeタブに行き、適当なグラフを選ぶ
2. "From new search"でエディタを開く
3. Y-axis/X-axisを適当に設定。例えば以下
    - Y-axisのAggregationを"Average"にして、適当なフィールドを指定
    - X-axisのAggregationを"Date Histogram"にして、フィールドは`@timestamp`でIntervalは`auto`
4. 緑三角のボタンを押す
![cpuusage.png](https://qiita-image-store.s3.amazonaws.com/0/76980/2f226809-fe5b-d9ce-9bb2-5b3185494b6b.png)
5. 喜びを噛みしめる。やったね！

X-axisの"Date histogram"は時系列による区間頻度で、Intervalは区間の幅。
Y-axisのAggr.をAverageにしているので、表示されているグラフは「**該当データの指定時間単位の平均値の推移**」となる。

作ったグラフは右上のパレットで保存できる。
保存したグラフはDashboardに表示させることもできる。色々集めて可視化しよう。

![dashboard.png](https://qiita-image-store.s3.amazonaws.com/0/76980/5e75f2d6-2ca8-44d7-6cff-5b38fba4fc7e.png)

**楽しくなってきた**。
といったところでお開き。

いくつか補足：

* KibanaにもESにもアクセス制御機能はないので、置く場所は考える必要がある
    * Elasticの[Shield](https://www.elastic.co/products/shield)があるが、[商用](https://www.elastic.co/guide/en/shield/current/license-management.html)
* アラート機能もデフォルトではない
    * Elasticの[Watcher](https://www.elastic.co/products/watcher)があるが、やはり商用。[elastalert](https://github.com/Yelp/elastalert)というオープンソースはある
* 複数ホストを監視するなら、Visualizeの際にHostnameで検索をかければ良さそう
* 画面右上の時計アイコンからポーリングの設定ができる
    * ずっと開きっぱにしているとブラウザのメモリがパンパンになっていくのは注意

# Next Step

`dstat`をソースにリソース可視化までこぎつけることはできた。
あとは細かな体制整理とマルチホスト対応をすれば、チーム内で使うための監視ダッシュボードとしては使えそうだ。

次なるステップはアプリケーションログをパースしてリクエストパターンの可視化といきたいところ。
GeoLocationを使った地図上での可視化なんかも面白そう。
プラス、自前クラスタの管理運用あるいはAWS ElasticSearch Serviceの評価とか。

Elasticsearchを中心としてできること、身につく知識は幅広い。夢を見ていこう。

---


[^1]: もちろん各言語にライブラリがあるとは思うが、最初の段階でいきなりバックエンドの保守作業を網羅したスクリプト群はないわけで。
[^2]: といってもやり方はある。[このブログ記事](https://www.elastic.co/blog/changing-mapping-with-zero-downtime)が詳しい。旧インデックスのデータに新マッピングを適用しつつダウンタイムレスで新インデックスに移行する方法もある。
[^3]: ならfluentdでなくLogstash使えばいいんじゃない？ という説もあるが、Logstashの知見が個人的にも世の中的にもまだ少ないのが辛い。Qiitaの[fluentd](http://qiita.com/tags/fluentd)と[Logstash](http://qiita.com/tags/Logstash)で比べると歴然だ。