Elasticsearchを時系列データストアに

オープンソースのパフォーマンス監視ツールであるstagemonitorのプロジェクトマネージャーとして、優れてはいるものの経年劣化してきたGraphiteの時系列データベース(TSDB)に取って代わるバックエンドのデータベースを模索していました。TSDBとは、アプリの応答時間やサーバーのCPU使用率といった(パフォーマンス)指標データを保存する専用パッケージです。弊社では最終的に、簡単にインストールでき、スケーラブルで、さまざまな機能を備え、指標の視覚化もサポートするデータストアを探すことにしました。

Elasticsearchは以前に使ったことがあったため、簡単にインストールでき、スケーラブルで、多くのアグリゲーション機能を備えていることは知っていましたし、Kibanaにすばらしい視覚化ツールがあることも分かっていました。けれども、時系列データに向いているかどうかは不明でした。 こうした疑問を抱いたのは、 弊社だけではありませんでした。実際、CERN(欧州原子核研究機構)でもElasticsearch、InfluxDB、OpenTSDBの比較試験を実施して、 Elasticsearchが勝者に選ばれていました。

決定プロセス

Elasticsearchは、フリーテキスト、システムログ、データベースレコードをはじめとする構造化データや非構造化データを保存、検索、分析する優れたツールです。ひと工夫すれば、collectdやstatsdといったツールから取り込んだ時系列指標を保存する便利なプラットフォームにもなります。

さらに、指標の増加に応じて手軽に拡張できます。Elasticsearchにはシャードやレプリカを活用した冗長性が組み込まれ、スナップショット&リストア機能で簡単にバックアップできるため、クラスタやデータを容易に管理できます。

Elasticsearchはまた、高度なAPI駆動型のため、Logstashのような統合ツールを使用して、膨大なデータを効率的に処理するデータ処理パイプラインも簡単に構築できます。この混合システムにKibanaを追加すれば、複数のデータセットを取り込んで分析し、指標データや他のデータの相関を並べて表示するプラットフォームになります。

さほど目立たないのですがメリットはもう1つあります。値を計算した変換後の指標を保存し、表示されたエンドポイント値をグラフ化するのではなく、生の値を保存してから、Elasticsearchに搭載された一連の強力なアグリゲーション機能をこれらの保存済みの値に実行することです。つまり、指標を数か月間観察した後に異なる方法で計算または表示したいと思った場合、過去のデータと現在のデータのどちらのデータセットにも当初予定していたものとは違うアグリーション機能を簡単に実行できます。別の言い方をすれば、データを保存した時点では思いもよらなかった疑問が生じても、その答えを見つけることができるのです。

Elasticsearchが時系列データベースに適していることはわかりましたが、では実際にどのように設定すればよいのでしょうか。

最初のステップ:

まず何よりも大切なのがマッピングです。マッピングをあらかじめ定義しておくと、Elasticsearchでのデータの分析や保存が最適化されます。

以下は、stagemonitorで行ったマッピングのサンプルです。このオリジナル版は、弊社の GitHubレポジトリでご覧いただけます。

{
  "template": "stagemonitor-metrics-*",
  "settings": {
    "index": {
      "refresh_interval": "5s"
    }
  },
  "mappings": {
    "_default_": {
      "dynamic_templates": [
        {
          "strings": {
            "match": "*",
            "match_mapping_type": "string",
            "mapping":   { "type": "string",  "doc_values": true, "index": "not_analyzed" }
          }
        }
      ],
      "_all":            { "enabled": false },
      "_source":         { "enabled": false },
      "properties": {
        "@timestamp":    { "type": "date",    "doc_values": true },
        "count":         { "type": "integer", "doc_values": true, "index": "no" },
        "m1_rate":       { "type": "float",   "doc_values": true, "index": "no" },
        "m5_rate":       { "type": "float",   "doc_values": true, "index": "no" },
        "m15_rate":      { "type": "float",   "doc_values": true, "index": "no" },
        "max":           { "type": "integer", "doc_values": true, "index": "no" },
        "mean":          { "type": "integer", "doc_values": true, "index": "no" },
        "mean_rate":     { "type": "float",   "doc_values": true, "index": "no" },
        "median":        { "type": "float",   "doc_values": true, "index": "no" },
        "min":           { "type": "float",   "doc_values": true, "index": "no" },
        "p25":           { "type": "float",   "doc_values": true, "index": "no" },
        "p75":           { "type": "float",   "doc_values": true, "index": "no" },
        "p95":           { "type": "float",   "doc_values": true, "index": "no" },
        "p98":           { "type": "float",   "doc_values": true, "index": "no" },
        "p99":           { "type": "float",   "doc_values": true, "index": "no" },
        "p999":          { "type": "float",   "doc_values": true, "index": "no" },
        "std":           { "type": "float",   "doc_values": true, "index": "no" },
        "value":         { "type": "float",   "doc_values": true, "index": "no" },
        "value_boolean": { "type": "boolean", "doc_values": true, "index": "no" },
        "value_string":  { "type": "string",  "doc_values": true, "index": "no" }
      }
    }
  }
}

ご覧のとおり、 _source_all は無効になっています。弊社ではアグリゲーション機能のみを構築しているためで、保存されるドキュメントを小さくしてディスク容量を節約しています。この場合の欠点は、実際のJSONドキュメントの確認や、新しいマッピングやインデックス構造への再インデックス付けができなくなることです(詳細は、 「ソースの無効化」 を参照)。ただし、弊社の指標主体のユースケースでは、この点はたいした問題ではありません。

繰り返しますが、大半のユースケースでは、 ソースの無効化は望ましいことではありません

弊社では指標ドキュメントに対して全文検索を行わないため、文字列値を分析することがありません。つまり、弊社が行いたい作業は、正確な名前によるフィルタリングと、 metricNamehostapplicationなどのフィールドに対するtermアグリゲーションの実行に限られるため、特定のホストで指標をフィルタリングするか、すべてのホストのリストを取得すれば用が足りるわけです。また、ヒープ使用量を少なくするために、できればdoc_valuesを使用することをお勧めします。

他にもかなり挑戦的な最適化の手法が2つありますが、このどちらもすべてのユースケースに適しているわけではありません。1つ目の手法は、すべての指標値に"index": "no"を使用することです。この設定により、インデックスのサイズは小さくなりますが、同時に値の検索ができなくなります。けれども弊社ではグラフに、2.7182~3.1415の値というサブセットではなく、すべての値を表示するため、この点も問題ありません。最小のnumeric型(弊社の場合はfloat型)を使用すれば、インデックスをさらに最適化できます。各自のケースの要求値がfloat値の範囲外の場合は、double型を使用することが考えられます。

次のステップ: 長期保存に向けた最適化

データを長期保存するための最適化の次の重要なステップは、すべてのデータにインデックス付けした後でインデックスを強制マージすることです(以前は「最適化」と言っていました)。このステップでは、既存のシャードを2~3つにマージして、この同じステップで削除済みのドキュメントを除去します。ご存知かもしれませんが、「最適化」という用語は、Elasticsearchでは誤解を招く可能性があります。このプロセスでは確かにリソースの使用が改善されますが、大量のCPUとディスクリソースを要する場合があります。システムが削除済みのドキュメントをパージした後、すべての根幹的なLuceneセグメントをマージするためです。強制マージをオフピークの時間帯、あるいはCPUやディスクリソースの多いノードで実行するよう推奨するのはこのためです。

このマージプロセスはバックグランドで自動的に行われ、データがインデックスに書き込まれる最中にも行われます。弊社では、すべてのイベントがElasticsearchに送信され、追加、更新、削除などの操作によってインデックスが変更される可能性がなくなった時点で、このプロセスを明示的にコールするようにしています。

最適化は通常、(毎時、毎日、毎週の)最新インデックスの作成後に24~48時間待機して、最後のほうのイベントもElasticsearchに到達できるようにします。一定期間待機した後にCuratorを使用して、最適化コールを簡単に処理できます。

$ curator optimize --delay 2 --max_num_segments 1 indices --older-than 1 --time-unit days --timestring %Y.%m.%d --prefix stagemonitor-metrics-

すべてのデータが書き込まれた後で最適化を行うもう1つの大きなメリットは、ノードのクラスタリカバリ速度や再起動を補助するsynced flushが自動的に適用されることです。

stagemonitorを使用している場合は、最適化プロセスが毎晩自動的にトリガされるため、Curatorを使用する必要もありません。

結論

弊社では、上記のプロセスをテストするために、プラットフォームから2300万超の無作為のデータポイントのみをElasticsearchに送信しました。この数はおよそ1週間分のデータ量に匹敵します。以下は、データのサンプルです。

{
    "@timestamp": 1442165810,
"name": "timer_1",
    "application": "Metrics Store Benchmark",
    "host": "my_hostname",
    "instance": "Local",
    "count": 21,
    "mean": 714.86,
    "min": 248.00,
    "max": 979.00,
    "stddev": 216.63,
    "p50": 741.00,
    "p75": 925.00,
    "p95": 977.00,
    "p98": 979.00,
    "p99": 979.00,
    "p999": 979.00,
    "mean_rate": 2.03,
    "m1_rate": 2.18,
    "m5_rate": 2.20,
    "m15_rate": 2.20
}

インデックス付けと最適化のサイクルを数回繰り返したところ、次のような数値が示されました。

初期サイズ 最適化終了後
サンプル実行1 2.2G 508.6M
サンプル実行2 514.1M
サンプル実行3 510.9M
サンプル実行4 510.9M
サンプル実行5 510.9M

この表からも最適化プロセスがいかに大切かが分かります。Elasticsearchを使用したこのバックグラウンド処理だけでも、長期保存に対して十分に実行する価値があります。

これでElasticsearchにすべてのデータが保存されました。次に、このデータからどのようなことが分かるのかを見てみましょう。以下はその答えで、弊社のシステムで生成された図表のサンプルです。

stagemonitor-applications-metrics.png
stagemonitor-hosts-metrics.png
stagemonitor-instances-metrics.png
stagemonitor-response-times.png
stagemonitor-throughputstatuscode-line.png
stagemonitor-toprequests-metric.png
stagemonitor-pageloadtime-line.png
stagemonitor-slowestrequestsmedian-line.png
stagemonitor-highestthroughput-line.png
stagemonitor-slowestrequests-line.png
stagemonitor-mosterrors-line.png

弊社がここで行ったテストをそのまま行う場合は、stagemonitor Githubリポジトリでコードをご覧いただけます。

今後の展望

Elasticsearch 2.0のさまざまな機能を駆使すれば、時系列データを柔軟に操作して各自に適したものにすることができます。

Pipeline Aggregationを使えば、今まで以上に高度なデータポイントの分析と変換への道が開かれます。例えば、 移動平均を使用してグラフを円滑にすることや、 Holt Winters法による予測を使ってデータが過去のパターンと一致しているかどうかを確認すること、さらには導関数を計算することなどが考えられます。

最後になりますが、前述のマッピングではヒープの効率性を高めるために、doc_valuesを手動で有効にする必要がありました。2.0では、 doc_valuesがデフォルトで有効になっているため、ユーザーの手間が1つ省かれます。

著者について — Felix Barnsteiner

felix-barsteiner.jpegFelix Barnsteinerは、オープンソースのパフォーマンス監視プロジェクトであるstagemonitorの開発者です。日中はドイツのミュンヘンにあるiSYS Software GmbHでeコマースのソリューション開発に取り組んでいます。