時系列データベースという概念をクラウドの技で再構築する

サーバ監視サービスMackerelにおいて開発中の、高解像度・長期間のサーバメトリック収集を実現するための新しい時系列データベースDiamondを紹介します。具体的には、Amazon ElastiCache、Amazon DynamoDB、Amazon S3を組み合わせ、Amazon Kinesis StreamsとAWS Lambdaによりコンポーネント間を接続した、階層構造のデータストアアーキテクチャの設計と実装を解説します。

2018/06/05 追記: この記事の内容をWSA研#2でより一般的なアーキテクチャレベルでの貢献として書き直しました。 サーバレス時代におけるヘテロジニアス時系列データベースアーキテクチャ - ゆううきブログ

はじめに

先日開催されたAWS Summit Tokyo 2017にて、「時系列データベースという概念をクラウドの技で再構築する」というタイトルで登壇しました。 この記事では、講演した内容に加えて、時間の限られた講演では触れられなかった内容を併せて議論します。

タイトルに時系列データベースとあるので、時系列データベースの話をもちろん書きます。しかし、AWS Summitでの発表ベースの記事なので、「AWS」を用いたシステム設計という切り口で話を進めます。 具体的には、Write-Intensive Application(書き込み要求が支配的となるアプリケーション)をAWS上で設計するための議論の土台になればと考えています。 時系列データベースは比較的単純なデータモデルを扱うため、よいモデルケースになるのではないかと思います。 時系列データベースとしての機能比較や実装の詳細についてはまた別の機会に紹介します。

背景

Mackerelでは、成長しつづけるサービスのスケーラビリティの確保と、データ量とI/Oがひと桁またはふた桁増加するような機能実現が求められています。

Mackerelの重要な機能のひとつに、メトリックのグラフ化機能があります。 開発中の新時系列データベースは、このグラフ化機能について、より高解像度でより長期間のメトリック収集と保持を実現しようとしています。 さらに、1つのグラフに1000以上のメトリックが含まれている状況でも高速に表示できるようにするなどの性能向上や、それ以外に、メトリックの異常検知など将来的な機能追加に対するワークロードに対して対応できるようにしたいと考えています。

Mackerelにおけるメトリックの収集とグラフ化の仕組みの概要は、以下の図のようになっており、前述の機能要求にとって、時系列データベースというコンポーネントが重要になってきます。

時系列データベースとはそもそも何でしょうか。時系列データベースとは、時系列データを扱うことに特化したデータベースであり、サーバモニタリングやIoTの文脈でのセンサーデータの収集のために使われます。有名な実装として、OpenTSDBInfluxDBGraphite などがあります。地味にみえて、多くの実装が存在し、学術研究論文になっているものもあります。id:rrreeeyyy さんの 時系列データベースに関する基礎知識と時系列データの符号化方式について - クックパッド開発者ブログ が詳しいので、一読することを薦めます。

Mackerelで運用している時系列データベースについては、Mackerelを支える時系列データベース技術 - ゆううきブログ の記事にまとめています。2年前の記事ですが、現状でも概ねこの通りの構成で運用しています。1台あたりピーク時で150k write IOPSを叩き出しており、サーバモニタリング向け時系列データベースの探究 / The study of time-series database for server monitoring - Speaker Deck で紹介したようにパッチをあててパフォーマンスを改善してきました。

ところが、MackerelのGraphite運用には、以下の4つの問題を抱えています。

まず、スケールアウトのための運用コストが大きいという問題です。書き込みI/Oのスケールアウト手法として、水平分割(sharding)が一般的です。Graphite自体にはconsistent-hashingにより、水平分割する仕組みがあります。しかし、シャードの増減により、ハッシュキーとノードのマッピングが変更されたときに、データを再配置するための仕組みはGraphite本体には実装されていないため、運用でカバーする必要があります。

次の問題は、データ保持期間を増やすと金銭的なコストが激増するというものです。前述したようにスケールアウトのための運用コストが大きいことから、書き込みI/Oをスケールアップするために、NANDフラッシュメモリを利用しています。非常に性能が高い一方で高価なハードウェアなので、容量単価が大きいという特徴があります。 したがって、データ保持期間を増やすと、ディスク上のデータ量が増加し、安価なハードウェアにのせることに比べてお金がかかることになります。 一般の解決策は、圧縮してディスクに格納するか、既に書き込まれたデータを低速で容量単価の小さいストレージに移動するような仕組みにすることです。 前者の圧縮については、1で紹介されている差分符号化やXOR符号化のテクニックをGraphiteに実装すれば、実現できそうではあります。しかし、これらのエンコーディングにより、ランダムアクセスできなくなるため、Graphiteのラウンドロビンデータベース構造とうまく噛み合うかがわからないところです。圧縮は基本的に、CPU負荷とトレードオフになるため、CPU負荷がボトルネックとなる可能性もあります。 後者のデータ移動については、Graphiteのラウンドロビンデータベースの、古いデータを新しいデータで上書きしていくという性質のため、別のストレージに効率よくデータを移動させることが難しく、データ構造を大きく変更する必要があります。

3つ目の問題は、データロスト耐性が低く、フル同期のための運用コストが大きいことです。アプリケーションから書き込みすると、Graphiteのメモリ上のキューにデータが非同期に書き込まれるため、サーバダウンによりキューの中身をロストしてしまいます。デュアルライトにより、別のサーバには書き込まれているので、そちらからデータを同期するなど、運用でカバーしなければなりません。これを解決するには、GraphiteにWAL(Write Ahead Log)を実装するなど、大きな変更を強いられます。

最後に、新規にメトリックが投稿されたときに、未来の領域をファイル作成時に確保するので、ディスク使用効率が低いという問題があります。これは2つめの問題と関連します。なぜこのようなデータ構造になっているかは http://graphite.readthedocs.io/en/latest/whisper.html?highlight=rollup#disk-space-efficiency に書かれています。このデータ構造は、コンテナのようなImmutable Infrastructureの概念と相性が悪く、非効率です。

これらの4つの現状の問題点と、冒頭に書いたような新しい機能要求を考慮すると、根本的なアーキテクチャの刷新が必要だと考えました。

既存の時系列データベース

まず、既存の時系列データベースの調査から始めました。Andreas Baderらのサーベイ論文2Open Source Time Series DB Comparison3にオープンソースの時系列データベース実装がまとめられています。

時系列データベースの概念

前述のサーベイ論文[^1]では、時系列データベースの定義は、以下のような性質を満たすDBMSのこととされています。

  • タイムスタンプ、値、属性(メトリック名など)で構成されるデータの行を格納できる
  • 時系列としてグループ化された複数の行を格納できる
  • データ行に対してクエリを発行できる
  • タイムスタンプまたは時間範囲をクエリに含められる

この定義を満たすものを実装するのはさほど難しくありません。しかし、実際には、分散/クラスタリング、Function、Rollup Aggregation(自動丸め)、タグ、複数のデータ解像度、クエリ言語などの時系列データベースにまつわる機能要求があります。 特に、今回のようなスケーラビリティを確保する目的であれば、分散/クラスタリングが重要になってきます。

時系列データベースの分類

時系列データベースのOSS実装は大きく2つの分類があると考えています。それは、他の汎用DBMS上に実装されたもの(TSDB on DBMS)と、時系列データベースに最適化したストレージエンジンをもつもの(TSDB standalone)です。 前者にあたるものは、例えば、OpenTSDBやKairosDBなどがあります。一方、後者にあたるものは、GraphiteやInfluxDB、Prometheusなどがあります。 前者のDBMSとして、HBase、Cassandra、Riak、ElasticsearchなどのいわゆるNoSQLが使用されます。もちろん、TimescaleDBのようにRDBMSであるPostgreSQLをベースにした実装も存在します。前述の時系列データベースとしての定義を満たすだけなら、ACID保証が必要ないので、NoSQLを利用するケースが多いようです。

時系列データベースを実装する側の視点でみると、TSDB on DBMSは、分散/クラスタリング機構を自前で実装せずにすむため、実装が楽というメリットがあります。一方で、TSDB standalineは分散/クラスタリング機構も実装しなければならないというデメリットがありますが、TSDB on DBMSと比較して時系列データベースに最適化できるというメリットがあります。

時系列データベースを運用する側の視点でみると、たいていのTSDB on DBMSはCassandraなどの分散システムとしての運用コストが高いといわれるDBMSを利用しており、一方で、TSDB standaloneは分散システムとしての信頼性を測るための実績が少ないといえます。実際には、製品ごとに細かくメリット・デメリットがありますが、大雑把にこのように認識しています。

さらに、Mackerelで採用するという視点でみると、既存の時系列データベースをそのまま採用するデメリットとして、Rollup Aggregationに対応していないものや、インタフェースが大きく変わるので変更コストがある、Mackerelの現在または将来のワークロードに対してスケールしない可能性があります。

新時系列データベースDiamondのアーキテクチャ

方針

調査の内容を踏まえて、既存の時系列データベースを採用せずに、Amazon DynamoDBなどのAWSマネージドサービスを採用し、マネージドサービス上に独自の時系列データベースアプリケーションを実装するという方針にしました。 マネージドサービスを採用したのは、時系列データベースの分散/クラスタリング機構に必ずついてまわるデータベースの検証・運用コストを削減するためです。はてなでは、特定ベンダーのロックインをなるべく避けるという技術選択方針がありますが、今回は、Mackerelの成長速度や将来の機能要求を考慮して、ある程度のロックインを受け入れることにしました。 独自に、TSDB on DBMSにあたるものを開発することにしたのは、ひとえにMackerelの要求を満たす実装がないか、要求を満たすかどうかの検証に時間がかかるためです。AWSマネージドサービス採用によりデータベースの検証/運用コストを削減できるので、その代わりの開発工数を確保できると考えました。

この大方針に沿って、Graphite運用の4つの問題点を解決できるか考えます。

まず、負荷分散の運用コストの解決です。前述したようにDynamoDBなどのフルマネージドサービスの利用により、運用コストを削減できます。水平分割のためのキーの設計を間違えなければ、お金を払った分だけI/Oスループットがスケールするので、検証も比較的楽になります。 ところが、DynamoDBのI/O課金が高いため、今のMackerelのI/Oを工夫せずにDynamoDBに向けるとインフラコストが大きく増加します。 したがって、個々のデータポイントを別々に書き込むのではなくまとめて書き込み、I/Oコストを下げる必要があります。 最終的には、前段にElastiCache(Redis)を配置し、ライトバックキャッシュ(講演ではバッファと呼んだ)として利用することにしました。

次に、データ保持期間についての解決です。DynamoDBのディスク容量課金は感覚的には安く感じましたが、1分解像度のデータを年単位で保持しようとすると、そこそこのコストがかかります。 S3のコストはスタンダードストレージでもDynamoDBの1/10程度に収まるので、ここでコストを浮かせておけば、他の機能拡充にコストをあてられるため、なるべくS3を使いたいと考えました。 とはいえ、S3のレイテンシはDynamoDBに比べて1桁以上大きいかつ、DynamoDBよりも内部的なパーティションあたりのスループットが低い可能性があります(要確認 リクエスト率およびリクエストパフォーマンスに関する留意事項 - Amazon Simple Storage Service)。 Mackerelの場合、古い高解像度データはほとんど参照されないという参照局所性があるので、参照回数が小さいデータの表示は多少遅くてもよいという性質があります。 そこで、この性質を利用し、ホットデータをDynamoDB、コールドデータをS3に配置することでコスト最適化することを考えました。

この時点で既に、階層型データストアアーキテクチャになることが決定しています。

3つ目に、データロスト耐性の向上です。これは簡単で、メモリ上のキューではなく、ディスク書き込みするメッセージキューを採用することです。OSSなら、KafkaやRabbitMQなどがこれにあたり、AWSでは、Amazon Kinesis Streamsになります。内部的な実装としてはキューではないと思いますが、キューのように利用できます。Kinesis Streamsは直近の24時間分のレコードをディスクに保存し、投稿したレコードをLambda functionの引数に渡すことも簡単です。Kafkaほどレイテンシが低いわけではないそうですが、どのみちMackerelのデータポイントはインターネットごしに投稿されるので、もともとユーザに反映されるまのでレイテンシが大きいので、無視できる程度のレイテンシの差だと考えました。キューというよりは、Using logs to build a solid data infrastructure (or: why dual writes are a bad idea) - Confluentに書かれているように、アプリケーションレベルのWAL(Write Ahead Log)を前段に配置していると捉えてもよいかもしれません。

最後に、ディスク使用効率の向上です。容量問題はS3の利用で解決できるので、ラウンドロビンデータベース構造にする必要がなく、未来の領域を確保することはしません。 圧縮や符号化については、RedisやDynamoDB上でレコード単位で圧縮しようとすると、レコードに対するデータポイントの更新時に、レコード内のデータを全て読み込んでからデータポイントを更新し、レコードの内容を差し替えるということをしなければならないため、余分なread I/Oとネットワーク帯域を圧迫する可能性があります。OSのファイルのようなseekできるストレージであれば、必要な分だけ読み出せるので、効率が良いと考えます。(しかし、これを書きながら、データサイズが小さくなっているのだから、ネットワーク帯域はさほど気にならない可能性もあるということに思いあたりました。)

さらに、時系列データを読み出すコンポーネントをMicroservicesとして開発しています。Graphite互換のインタフェースを実装し、新システム移行時のインタフェース変更コストを小さくしています。1つのクエリに1000以上のメトリックが含まれることがあるため、各メトリックのタスクを並行処理できるように、軽量スレッドでI/O多重化できるGo言語を実装言語として選択しました。

設計

前節の方針を元に、新時系列データベースのアーキテクチャの設計の概要を以下にまとめます。

  • 金銭コスト最適化のために参照局所性を利用する。データ参照頻度に応じて適切なデータストアへデータを移動させる
    • ホットデータをDynamoDB、コールドデータをS3に配置
    • ライトバックキャッシュのストレージとしてElastiCache(Redis)を利用
    • DynamoDBのTTL機能によりシームレスにデータ移動
  • Kinesis Streamsに対して先行ログ書き込みし、データロスト耐性と耐障害性の向上させる
  • Graphite互換インタフェースにより、各ストレージからデータ取得するWebアプリケーションを実装する

アーキテクチャ図を以下に示します。

このように複数のデータストアを組み合わせる場合、データをシンプルな手法で移動させるためのアイデアが必要です。 2017年2月にDynamoDBがTTLサポートされました。 TTLがexpireしたレコードをDynamoDB Triggers経由でLambda functionに渡すことができるため、簡単に古いデータをS3へ配置することができます。

この機能がリリースされたとき、ちょうどデータ移動の仕組みに困難さを感じていたところでした。当時は、タイムウィンドウごとにDynamoDBのテーブルを用意して、古いタイムウィンドウテーブルをテーブルごとDataPipelineによりS3にエクスポートすることを考えていました。いわゆる、MySQLで日時カラムに対してパーティションを切り、古いパーティションをパーティションごとDROPするという手法に近いと思います。 テーブルファイルがS3に配置されても、巨大なテーブルファイルに対してクエリするのは現実的ではないため、テーブルファイルをレコード単位に分割してS3に保存し直すような手間のかかる処理が必要でした。 テーブル単位による管理を強いられるため、アイテム単位で細かくデータ配置を制御したくなったときに、どうしようもなくなるという問題もありました。

古くなったデータを別のストレージに移動したいというのは一般的な要求なので、データベースのTTLとイベント通知というサーバーレスアーキテクチャらしい仕組みは、汎用的に利用できるアーキテクチャだと考えています。(2018/05/04追記: TimeFuzeアーキテクチャ構想 - 処理とデータとタイマーを一体化したデータパイプライン - ゆううきブログ として一般化して提案しました。)

実装

実装についての詳細は、スライドを参照してください。時系列データベース固有の話になります。Redis/DynamoDB上のスキーマ設計、Redisによるライトバックキャッシュ、Graphite互換アプリケーションのI/O多重化について簡単に紹介しています。

考察

Write-Intensive Applicationとして、例えばログ収集や、チャットメッセージングなどがあります。ログやチャットメッセージと違い、メトリックのデータポイントは固定長のデータなので、データ構造を最適化しやすいという特徴があります。したがって、可変なデータを書き込むアプリケーションの場合、時系列データベースでは考えなくてよかったようなデータ構造の工夫が必要かもしれません。

サービスの仕様変更により、データ構造を変更したくなったとき、3箇所にデータを保持していると、それぞれ変更を加えなければいけないというデメリットがあります。これについては、場合によっては、今あるデータ構造に追加の変更をするよりは、前段のKinesis Streamsから別のストレージに最適な構造で書き込むことも考えられます。

3層構造のデータストアアーキテクチャを提案しましたが、必ずしも3層にする必要はないと思います。ディスク使用が少なければElastiCacheとDynamoDB、書き込みI/Oが少なければ、DynamoDBとS3、readレイテンシを気にしなければ、RedisとS3というように、2層で対応できることもあるでしょう。 ちょうど手元のマシンでRAM+SSD+外部ストレージで大抵のことが事足りるように、3層あれば多くの状況でコスト最適化できると考えています。

DynamoDBにはDAXというライトスルーキャッシュが最近実装されました。もし、DAXにライトバックキャッシュが実装されれば、DiamondアーキテクチャのうちRedisの層は不要になるかもしれません。

その他、運用の観点で、スナップショットバックアップについては、ElastiCache自体にバックアップ機構があり、DynamoDBでは前述のDataPipelineによるS3への定期バックアップが可能であり、S3上のファイルは一旦作成されたら基本的に更新しないので、スナップショットバックアップはそれほど必要ないと考えています。

さらに、Lambda functionの実装にバグがあるとデータロストの危険性がないかについては、前段のKinesis Streamsに24時間(追加コストで1週間)データ保持されるので、原理上はそこからリカバリできるはずです。functionの実行がエラーになるケースでは、functionの実行がリトライされます。ただしこれには、functionの実行がべき等である必要があります。Diamondの場合、RedisとDynamoDBのデータ構造がタイムスタンプをキーとしたハッシュマップになっているので、データポイントの書き込みはべき等になっています。

Diamondアーキテクチャのデメリットとして、構成要素が多いことが挙げられます。Kinesis streamsもあわせると、4つのデータストアを併用することになります。一般に構成要素の数が増加すると運用コストも増加しますが、マネージドサービスの利用やCloudFormationのようなInfrastructure As Codeの利用により、運用・管理コストを抑えられると考えています。 しかし、Diamond自体のモニタリングは複雑になるため、モニタリング手法もあわせて提案する必要があると考えています。

資料

プレゼンテーション資料

参考文献

あとがき

「時系列データベースという概念をクラウドの技で再構築する」というタイトルは、少年ジャンプで連載中の食戟のソーマの第115話で、主人公 幸平創真の「親子丼という概念をフレンチの技で再構築する」という台詞から拝借したものです。 主人公は定食屋のせがれで、定食屋としての技術の限界を超えるため、フランス料理の技術を学び、日本の庶民の料理である親子丼を抽象化して、フレンチの技を用いて再構成するというお話になっています。 自分では、この話をとても気に入っていて、今回、ちょうど同じように、時系列データベースの構成要素を取り出し、マネージドサービスやサーバレスアーキテクチャなどのクラウドならではの技術を用いて再構築していると感じ、このタイトルをつけました。

今回話をしたかったのは、単に使っているOSSを列挙するようなものや込み入った実装の話ではなく、アーキテクチャの話です。 最近、アーキテクチャと実装の違いについて考えているのですが、まだあまりよい答えをもっていません。少なくともいえるのは、アーキテクチャには汎用性があり、実装するソフトウェアだけでなく他のソフトウェアにも通用するなにかがあるということです。 アーキテクチャの観点では、Webアプリケーションは参照局所性にあわせて複数のデータストアを併用するのは昨今では当たり前なので、今回のようなTTL + Lambda Functionのようにそこにもっとサーバレスというかイベント駆動な考え方を持ち込み、体系化できないかということを考えていたりします。

Mackerelでは、時系列データベース以外にも、おもしろいソフトウェアアーキテクチャを実現できるチャンスがまだまだたくさんあります。僕の場合は、最近は機械学習を支える効率のよいシステムやグラフDBのアーキテクチャに興味があります。 はてなでは、アーキテクチャを設計・実装し、本番投入し、運用まで持っていくことに興味があるエンジニアを募集しています。

また、今年のはてなインターンでは、Mackerelやはてなのインフラストラクチャを支えるシステムを開発できる、「大規模システムコース」と「クラウドサーバ管理システムコース」があります。 はてなサマーインターン2017

追記


  1. 時系列データベースに関する基礎知識と時系列データの符号化方式について - クックパッド開発者ブログ

  2. Andreas Bader, Oliver Kopp, Michael Falkenthal. “Survey and Comparison of Open Source time Series Databases”. In proceedings of BTW 2017.

  3. Open Source Time Series DB Comparison