オープンソース時系列データベースの分析 - Part 3

この記事では、人気のある**オープンソースの時系列データベースエンジン**の時系列データの保存と計算能力を分析しています。

周 肇峰著

#InfluxDB
InfluxDBはDB-Enginesの時系列データベースの中で第1位にランクインしており、これは本当に当然のことです。機能の豊富さ、使いやすさ、最下層の実装という観点から、多くのハイライトがあり、詳細な分析を行う価値があります。

まず、いくつかの重要な属性を簡単にまとめておきます。

- シンプルなアーキテクチャ:スタンドアロンのInfluxDBの場合、バイナリだけをインストールする必要があり、外部からの依存関係なしに使用できます。ここでは、いくつかのネガティブな例を紹介します。OpenTSDBの最下層はHBaseなので、ZooKeeperやHDFSなどを併用する必要があります。Hadoopの技術スタックに慣れていないと、一般的に運用や保守が難しいのも不満の一つです。KairosDBの方が若干良いです。CassandraやZooKeeperに依存しており、スタンドアローンでのテストにはH2を使うことができます。一般的に、外部の分散データベースに依存するTSDBは、完全に自己完結したTSDBよりもアーキテクチャがやや複雑です。結局のところ、成熟した分散データベースはそれ自体が非常に複雑ではありますが、それはクラウドコンピューティングの時代になって完全に解消されました。

- TSMエンジン：自社開発のTSMストレージエンジンを最下層に採用しています。TSMもLSMの考え方に基づいており、非常に強力な書き込み能力と高い圧縮率を実現しています。より詳細な分析は、以下のセクションで行います。
- InfluxQL:SQLライクなクエリ言語が提供されており、データベースの利用を大幅に促進します。ユーザビリティにおけるデータベースの進化の最終目標は、クエリ言語の提供です。
- 継続的なクエリ:CQでは、データベースは自動ロールアップと事前集計をサポートすることができます。一般的なクエリ操作については、CQを使用して事前計算による高速化を行うことができます。
- 時系列インデックス:タグは効率的な検索のためにインデックス化されています。OpenTSDBやKairosDBなどと比較して、この機能によりInfluxDBのタグ検索の効率化が図られています。OpenTSDBではタグの検索に関して多くのクエリ最適化が行われています。しかし、HBaseの関数やデータモデルによって制限されており、これらの最適化は機能していません。しかし、現在の安定版の実装では、メモリベースのインデックスを使用しており、実装が比較的簡単で、クエリ効率が最も高いです。しかし、これにも多くの問題があり、以下のセクションで詳しく説明します。

- プラグインのサポート:データベースはカスタムプラグインをサポートしており、 Graphite、collectd、OpenTSDBなどの様々なプロトコルをサポートするように拡張することができます。
以下のセクションでは、主に基本的な概念、TSMストレージエンジン、連続クエリ、TimeSeriesインデックスについて詳細に分析します。

#基本的な概念
まずは、InfluxDBの基本的な概念を学んでみましょう。具体的な例としては、以下のようなものがあります。

```
INSERT machine_metric,cluster=Cluster-A,hostname=host-a cpu=10 1501554197019201823
```

InfluxDBにデータエントリを書き込むコマンドラインです。このデータの構成要素は以下の通りです。

- 測定（Measurement）:測定の概念はOpenTSDBのメトリックと似ており、データの監視指標の名前を表しています。例えば、上の例では機械指標の監視なので、そのMeasurementはmachine_metricという名前になっています。
- タグ:OpenTSDBのタグの概念と同様に、タグは対象の異なる次元を記述するために使用されます。1 つ以上のタグが許可されており、各タグはタグキーとタグ値で構成されています。
- フィールド:OpenTSDBの論理データ・モデルでは、メトリック・データの行は1つの値に対応します。InfluxDBでは、測定データの1行が複数の値に対応し、各値はフィールドによって区別されます。
- Timestamp（タイムスタンプ）:時系列データの必須属性。データの時間点を表します。InfluxDBの時間粒度はナノ秒単位まで正確にできることがわかります。
- TimeSeries:計測＋タグの組み合わせで、InfluxDBではTimeSeriesと呼ばれています。TimeSeriesは時系列のことです。時間に基づいて特定の時間点を位置づけることができるので、TimeSeries + Field + Timestamp を使って特定の値を位置づけることができます。これは重要な概念であり、後のセクションで述べます。
最後に、各測定のデータは、次の図に示すように、論理的に大きなデータテーブルに整理されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/ecb5af1d-7d14-0282-92fa-2ae2b034e3b3.png)

クエリを実行するとき、InfluxDB は測定内の任意のディメンジョンの基準クエリをサポートします。任意のタグまたはフィールドを指定してクエリを実行できます。上記のデータケースに従って、以下のクエリ基準を構築できます。


```
SELECT * FROM "machine_metric" WHERE time > now() - 1h;  
SELECT * FROM "machine_metric" WHERE "cluster" = "Cluster-A" AND time > now() - 1h;
SELECT * FROM "machine_metric" WHERE "cluster" = "Cluster-A" AND cpu > 5 AND time > now() - 1h;
```

データモデルや問い合わせ基準からすると、タグとフィールドに違いはありません。セマンティクスの観点からは、タグは測定値を記述するために使用され、フィールドは値を記述するために使用されます。内部実装の観点からは、タグは完全にインデックス化されていますが、フィールドはインデックス化されていないため、タグに基づく基準クエリは、フィールドに基づくものよりもはるかに効率的です。

#TSM
InfluxDBの最下層ストレージエンジンは、LevelDBからBlotDBへ、そして自社開発のTSMへとプロセスを経てきました。全体の選択と変換の検討は、[公式サイトの資料](https://docs.influxdata.com/influxdb/v1.3/concepts/storage_engine/?spm=a2c65.11461447.0.0.290e6a8eEb5I6G#leveldb-and-log-structured-merge-trees)で見ることができます。全体の検討プロセスは学ぶ価値があります。技術の選択と変革に関する考察は、単に製品の属性を記述するだけではなく、常に感動を与えてくれます。

ストレージエンジンの選定から変換までの全体の流れを簡単にまとめてみました。第一段階はLevelDBです。LevelDBを選択した主な理由は、最下層のデータ構造がLSMを採用しており、書き込み性が高く、書き込みスループットが高く、時系列データの属性と比較的一致していることです。LevelDBでは、データはKeyValueに格納され、Keyでソートされます。InfluxDBで使用するKeyはSeriesKey＋Timestampの組み合わせであるため、同じSeriesKeyのデータをTimestampでソートして格納することで、時間範囲による非常に効率的なスキャニングが可能になります。

しかし、LevelDBを利用する上での最大の問題点は、InfluxDBが履歴データの自動削除（Retention Policy）をサポートしていることです。時系列データのシナリオでは、データの自動削除とは、通常、連続した時間帯のヒストリカルデータの大きなブロックを削除することです。LevelDBはRange DeleteやTTLをサポートしていないため、削除は一度に1つのキーでしか行えず、削除トラフィックの圧力が大きくなってしまいます。そして、LSMのデータ構造では、実際の物理的な削除は瞬間的なものではなく、コンパクションが有効になっているときにのみ有効になります。様々なTSDBのデータ削除の実践は、大きく分けて2つのカテゴリーに分けることができます。

1、データのシャーディング：データは、異なる時間範囲に応じて異なるシャードに分割されます。時系列データの書き込みは時間の経過とともに直線的に生成されるため、生成されるシャードも時間の経過とともに直線的に増加します。書き込みは通常、複数のシャードにハッシュされることなく、最新のパーティションで行われます。シャーディングの利点は、リテンションの物理的な削除が非常に簡単なことです。単純にシャード全体を削除することができます。欠点は、リテンションの精度がシャード全体という比較的大きなものであるのに対し、リテンションの時間粒度はシャードのタイムスパンに依存することです。シャーディングは、アプリケーション層でもストレージエンジン層でも実装することができます。例えば、RocksDBのカラム・ファミリーをデータ・シャードとして使用することができます。InfluxDBはこのモデルを採用しており、デフォルトの保持ポリシー下のデータは7日間のタイムスパンでシャードを形成します。

2、TTL：最下層のデータエンジンが直接、データの自動期限切れ機能を提供します。データ入力ごとに有効期限を設定することができ、その時間に達するとストレージエンジンが自動的に物理データを削除します。この方法の利点は、保持の精度が非常に高く、保持の第2レベルと行レベルに達することです。欠点は、LSMの実装上、コンパクション時に物理削除が発生するため、タイムリー性が低いことです。RocksDB、HBase、Cassandra、Alibaba Cloud Table StoreはいずれもTTL機能を提供しています。

InfluxDBは、データを7日周期で複数の異なるシャードに分割し、各シャードは独立したデータベースインスタンスとする第1のポリシーを採用しています。実行時間が長くなるにつれて、シャードの数も増えていきます。各シャードは独立したデータベースインスタンスであり、最下層は独立したLevelDBのストレージエンジンであるため、各ストレージエンジンが開くファイル数が多くなるという問題がありますが、シャードの増加に伴い、最終処理で開くファイルハンドルの数がすぐに上限に達してしまいます。LevelDBでは最下層でレベルコンパクションポリシーを使用しており、これがファイル数の多さの原因の一つとなっています。実際、レベルコンパクションポリシーは時系列データの書き込みには適しておらず、InfluxDBではその理由については言及されていません。

過剰なファイルハンドルに関する顧客からの重要なフィードバックがあったため、InfluxDBは新しいストレージエンジンの選択において、LevelDBに代わるBoltDBを選択しました。BoltDBの最下層のデータ構造は、mmap B+ツリーです。これを選択した理由は以下の通りです。1. LevelDBと同じセマンティクスを持つAPIを採用していること、2.統合が容易でクロスプラットフォームに対応した機能性を持つPure Goの実装を採用していること、3.1つのデータベースで1つのファイルしか使用しないため、ファイルハンドルを過剰に消費するという問題を解決していること、などです。これがBoltDBを選ぶ最大の理由です。しかし、BoltDBのB+ツリー構造は、書き込み能力の点ではLSMほどではなく、B+ツリーでは大量のランダム書き込みが発生します。そのため、BoltDBを使用した後、InfluxDBはすぐにIOPSの問題に直面しました。データベースのサイズが数GBに達すると、しばしばIOPSのボトルネックに遭遇し、書き込み能力に大きな影響を与えます。InfluxDBはその後、BoltDBの前にWAL層を追加し、最初にWALにデータを書き込むなど、書き込みの最適化策もいくつか採用していますが、WALはディスクへのデータの書き込みを確実に順番に行うことができます。しかし、最終的にBoltDBへの書き込みは依然として大量のIOPSリソースを消費しています。

BoltDBのいくつかのマイナーバージョンを経て、最終的にInfluxDB用のTSMを内部で開発することになりました。TSMの設計目標はLevelDBの過剰なファイルハンドルの問題を解決することであり、2つ目はBoltDBの書き込み性能の問題を解決することでした。TSMはTime-Structured Merge Treeの略です。その考え方はLSMに似ているが、時系列データの属性に基づいていくつかの特別な最適化を行っています。TSMの重要なコンポーネントは以下の通りです。

1、Write Ahead Log (WAL)：データは最初にWALに書き込まれ、メモリインデックスとキャッシュに流れます。WALに書き込まれたデータは、データの永続性を確保するために同期的にディスクにフラッシュされます。キャッシュ内のデータは、非同期的にTSMファイルにフラッシュされます。キャッシュ内のデータがTSMファイルに永続化される前にプロセスがクラッシュした場合、WAL内のデータはキャッシュ内のデータを復元するために使用され、この動作はLSMのそれと似ています。
2、キャッシュ：TSMのキャッシュはLSMのMemoryTableに似ています。内部データはWAL内のデータで、TSMファイルには永続化されていません。プロセスにフェイルオーバーが発生した場合、キャッシュ内のデータはWAL内のデータを元に再構築されます。キャッシュ内のデータはSortedMapに格納されており、マップのKeyはTimeSeries＋Timestampで構成されています。したがって、メモリ内のデータはTimeSeriesで整理され、TimeSeriesのデータは時系列で格納されます。

3、TSMファイル：TSMファイルは、LSMのSSTableに似ています。TSMファイルは、ヘッダ、ブロック、インデックス、フッタの4つの部分から構成されています。最も重要な部分はブロックとインデックスです。
　　　1、**ブロック**: 各ブロックは、ある期間のTimeSeriesの値、すなわち、ある期間のある測定のタグセットに対応するフィールドのすべての値を格納します。最適なコンパクション効率を達成するために、フィールドの異なる値の種類に基づいて、異なるコンパクションポリシーがブロック内で採用されます。
　　　2、**インデックス（Index）**：ファイル内のインデックス情報は、各TimeSeriesの下にあるすべてのデータブロックの位置情報を格納します。インデックスデータは、TimeSeriesキーの字句順に従ってソートされます。非常に大 き い完全な インデックスデータはメモリに読み込まれません。そのかわりに、いくつかのキーのみがインデックスされます。この indirectIndex には、時間の最小値と最大値、ファイル内のキーの最小値と最大値など、いくつかの補助的な位置情報が含まれています。最も重要なのは、一部のキーのファイルオフセット情報とそのインデックスデータが保存されていることです。TimeSeriesのインデックスデータの位置を特定するためには、まず、メモリ内のいくつかのKey情報に基づいて最も類似したインデックスオフセットを見つけ、ファイルの内容を始点から順次スキャンして、そのKeyのインデックスデータの位置を正確に特定する必要があります。

4、コンパクション（Compaction）：コンパクションとは、書き込み最適化されたデータストレージ形式を、読み取り最適化されたデータストレージ形式に最適化するプロセスです。これは、ストレージとクエリの最適化のためのLSM構造のストレージエンジンの重要な機能です。ストレージエンジンの品質は、コンパクション戦略とアルゴリズムの品質によって決まります。時系列データのシナリオでは、更新操作や削除操作はほとんど行われず、データは時系列で生成されるため、基本的に重複はない。コンパクションは主にコンパクションとインデックス最適化の役割を担っています。

   1、**LevelCompaction（レベルコンパクション）**:InfluxDBはTSMファイルを4つのレベル(レベル1～4)に分割します。コンパクションは同じレベルのファイル内でのみ発生します。同一レベルのファイルは、コンパクションされた後に次のレベルに昇格します。このルールから、時系列データ生成の属性に基づいて、レベルが高いほどデータ生成時間が早くなり、アクセス熱が低くなります。このようにして初めてキャッシュ内のデータによって生成されたTSMファイルをスナップショットと呼びます。複数のスナップショットが圧縮された後にLevel1のTSMファイルが生成され、Level1のファイルが圧縮された後にLevel2のTSMファイルが生成されます。低レベルファイルと高レベルファイルの圧縮には、異なるアルゴリズムが使用されます。低レベルファイルの圧縮には、CPU消費量の少ない方法が用いられます。例えば、ブロックデコンパクションやブロックマージは行いません。高レベルファイルの圧縮率をさらに向上させるために、ブロックデコンパクションとブロックマージを行っています。この設計はトレードオフであると理解しています。比較は通常、バックグラウンドで動作します。リアルタイムのデータ書き込みに影響を与えないように、コンパクションで消費されるリソースは厳密に制御されていますが、リソースが限られている状況ではコンパクションの速度に影響が出てしまいます。しかし、レベルが低いほどデータが新しくてホットなため、クエリを高速化するコンパクションが必要になります。そこで、InfluxDBでは、時系列データの書き込み属性や問い合わせ属性に合わせて完全に設計された下位レベルでのリソース消費の少ないコンパクションポリシーを採用しています。
　　　2、**IndexOptimizationCompaction**：Level4のファイルがある程度の数まで蓄積されると、インデックスが非常に大きくなり、クエリの効率が相対的に低くなります。クエリ効率が低い主な要因は、同じTimeSeriesデータが複数のTSMファイルに含まれているため、複数のファイルにまたがるデータ統合が避けられないことです。そのため、IndexOptimizationCompactionは、主に同じTimeSeriesの下のデータを同じTSMファイルに統合して、異なるTSMファイル間のTimeSeriesの重複率を最小化するために使用されます。
　　　3、**FullCompaction**：InfluxDBは、シャードが長い間書き込まれたデータを持たないと判断した後、データのフルコンパクションを実行します。FullCompactionはLevelCompactionとIndexOptimizationを統合したものです。フルコンパクションの後、新しいデータが書き込まれるか、削除が発生しない限り、そのシャードに対してそれ以上のコンパクションは実行されません。このポリシーはコールドデータに対する照合であり、主にコンパクション率の向上を目的としています。


#Continuous Query
InfluxDBにおけるデータの事前集計と精度の低下については、2つの方針が推奨されています。1つはInfluxDBのデータ計算エンジンであるKapacitorを利用すること、もう1つはInfluxDBに付属している継続的なクエリを利用することです。

```
CREATE CONTINUOUS QUERY "mean_cpu" ON "machine_metric_db"
BEGIN
SELECT mean("cpu") INTO "average_machine_cpu_5m" FROM "machine_metric" GROUP BY time(5m),cluster,hostname
END
```

上記は、連続的なクエリを構成するシンプルなCQLです。これにより、InfluxDBがタイミングタスクを開始して、測定値「machine_metric」の下にあるすべてのデータをクラスタ＋ホスト名の次元で5分ごとに集計し、フィールド「cpu」の平均値を計算し、最終結果を新しい測定値「average_machine_cpu_5m」に書き込むことができるようになっています。

InfluxDBの連続クエリは、KairosDBの自動ロールアップ機能に似ています。これらはすべて単一のノード上でスケジュールされています。データ集約は、リアルタイムのStreamComputeよりも遅延したStreamComputeであり、その間、ストレージは大きなリードプレッシャーを受けることになります。

#時系列インデックス
時系列データの保存と計算をサポートするだけでなく、時系列データベースは多次元クエリを提供できる必要があります。InfluxDBはTimeSeriesをインデックス化して、より高速な多次元クエリを実現しています。データとインデックスについては、InfluxDBは以下のように説明されています。

>InfluxDBは、実際には時系列データストアと測定、タグ、フィールドのメタデータのための転置インデックスという2つのデータベースのように見えます。

InfluxDB 1.3以前では、TimeSeriesインデックス(以下TSI)はメモリベースの方法しかサポートされておらず、つまり全てのTimeSeriesインデックスがメモリに格納されており、これは有益ではあるが多くの問題点もあった。しかし、最新のInfluxDB 1.3では、選択可能な別のインデックス作成方法が提供されています。新しいインデックス作成方法は、ディスク上にインデックスを格納します。

#メモリベースのインデックス

```
    // Measurement represents a collection of time series in a database. It also
    // contains in memory structures for indexing tags. Exported functions are
    // goroutine safe while un-exported functions assume the caller will use the
    // appropriate locks.
    type Measurement struct {
     database string
     Name     string `json:"name,omitempty"`
     name     []byte // cached version as []byte

     mu         sync.RWMutex
     fieldNames map[string]struct{}

     // in-memory index fields
     seriesByID          map[uint64]*Series              // lookup table for series by their id
     seriesByTagKeyValue map[string]map[string]SeriesIDs // map from tag key to value to sorted set of series ids

     // lazyily created sorted series IDs
     sortedSeriesIDs SeriesIDs // sorted list of series IDs in this measurement
    }

    // Series belong to a Measurement and represent unique time series in a database.
    type Series struct {
     mu          sync.RWMutex
     Key         string
     tags        models.Tags
     ID          uint64
     measurement *Measurement
     shardIDs    map[uint64]struct{} // shards that have this series defined
    }
```


以上がInfluxDB 1.3のソースコードにおけるメモリベースのインデックスデータ構造の定義です。主に2つの重要なデータ構造から構成されています。

 **Series**：TimeSeriesに対応し、TimeSeriesとそれが属するシャードに関連するいくつかの基本的な属性を格納します。

- Key: 対応する測定+タグのシリアル化された文字列。
- Tag: Timeseriesの下にあるすべてのタグキーとタグ値。
- ID: 一意の整数 ID。
- measurement: シリーズが属する測定。
- shardIDs: シリーズを含むすべてのShardIDのリスト。

**測定**:各測定はメモリ内の測定構造に対応しており、クエリを高速化するために構造内にいくつかのインデックスがあります。

- seriesByID: SeriesIDを介してSeriesをクエリするマップ。
- seriesByTagKeyValue。2層のマップ。1層目はタグキーに対応するすべてのタグ値、2層目はタグ値に対応するすべてのSeriesのIDです。ご覧のように、TimeSeriesのベースが大きくなると、このマップはかなり多くのメモリを消費します。
- sortedSeriesIDs: ソートされたSeriesIDのリスト。

フルメモリベースのインデックス構造は、高効率な多次元クエリを提供できるという利点がありますが、いくつかの問題点もあります。

- TimeSeriesのベースは主にメモリサイズによって制限されています。TimeSeriesの数が上限を超えると、データベース全体が利用できなくなります。この種の問題は一般的にタグキーの設計が間違っていることが原因です。例えば、タグキーはランダムなIDです。一度この問題が発生すると、復旧は困難です。手動でデータを削除するしかありません。
- プロセスが再起動した場合、メモリ内にインデックスを構築するためにすべてのTSMファイルからTimeSeriesの完全な情報をロードする必要があるため、データを回復するのに長い時間がかかります。

#ディスクベースのインデックス
フルメモリベースのインデックスの問題については、最新のInfluxDB 1.3で追加のインデックス実装が提供されています。コード設計のスケーラビリティのおかげで、インデックスモジュールとストレージエンジンモジュールはプラグインです。設定で使用するインデックスを選択することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/647921/d3f476be-ab4b-fc86-a92f-b720cde81bb1.png)

InfluxDBでは、インデックスデータを格納するために特殊なストレージエンジンが実装されています。その構造もLSMと似ている。上図のように、ディスクベースのインデックス構造となっています。詳細は[設計書](https://github.com/influxdata/influxdb/blob/master/tsdb/index/tsi1/doc.go?spm=a2c65.11461447.0.0.290e6a8eEb5I6G&file=doc.go)を参照してください。

インデックスデータは、まずWAL（Write-Ahead-Log）に書き込まれます。WAL 内のデータは LogEntry で構成されており、各 LogEntry は TimeSeries に対応しており、測定、タグ、チェックサムに関する情報が含まれています。WAL への書き込みが成功すると、データはメモリベースのインデックス構造に入ります。WAL が一定のサイズまで蓄積されると、LogFile は IndexFile にフラッシュされます。IndexFileの論理構造はメモリベースのインデックス構造と一致しており、測定からtagkeyへ、tagkeyからtagvalueへ、tagvalueからTimeSeriesへのマップ構造を示しています。InfluxDB は mmap を使用してファイルにアクセスし、クエリを高速化するためにファイル内の各マップに対して HashIndex を保存します。

また、IndexFilesが指定した量まで蓄積されると、InfluxDBは複数のIndexFilesを1つにマージするコンパクション機構を提供し、ストレージスペースを節約してクエリを高速化します。

#概要
InfluxDBのコンポーネントはすべて自己開発されています。自前開発の利点は、時系列データの属性に基づいて各コンポーネントを設計し、パフォーマンスを最大化できることです。コミュニティ全体の動きも活発ですが、ストレージ形式の変更やインデックスの実装変更など、大規模な機能アップグレードが頻繁に発生しており、ユーザーにとってはかなり不便です。一般的に、私はInfluxDBの開発に楽観的になっています。残念ながら、InfluxDBのクラスタ版はオープンソースではありません。

*本ブログは英語版からの翻訳です。オリジナルは[こちら](https://www.alibabacloud.com/blog/a-comprehensive-analysis-of-open-source-time-series-databases-3_594732?spm=a2c65.11461544.0.0.23e753559GvF6A)からご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。*

*アリババクラウドは日本に２つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
[アリババクラウドジャパン公式ページ](https://www.alibabacloud.com/ja)*