PromCon 2017 のセッション紹介 - Storing 16 Bytes at Scale

少し時間が経ってしまっていますが, 8 月に Prometheus に特化したカンファレンス [PromCon 2017](https://promcon.io/2017-munich/) がミュンヘンにて開かれたので, [Z Lab](https://qiita.com/organizations/zlab) からは私と @tkusumi が参加して来ました.

この記事ではそこで聞いたセッションの中から, [先日リリースされた Prometheus 2.0](https://prometheus.io/blog/2017/11/08/announcing-prometheus-2-0/) での最も大きな変更の一つである Time Series Database (TSDB) に関するセッションについて, 紹介させてもらおうと思います.

## 概要

**スピーカー**: Fabian Reinartz (CoreOS)
**ビデオ**: <https://youtu.be/b_pEevMAC3I>
**スライド**: <https://www.slideshare.net/FabianReinartz/storing-16-bytes-at-scale-81282712>

Prometheus 2.0 で刷新される TSDB の実装者による, 詳解とベンチマークについてのセッションでした.

**NOTE**: この記事で使われている画像は全て, スピーカーからの承諾を得た上で上記のスライドから引用しています.

## 内容

### Prometheus の TSDB のデータ構造

![1.0.time_series.png](https://qiita-image-store.s3.amazonaws.com/0/18658/03bf77ab-3aef-e128-1fcf-3f06425683a7.png)

TSDB のデータ構造を模式化すると上の図のようになります. 縦軸は time series の列を表していて, 一つ一つがメトリクスとそれに付けられたラベルの組み合わせに対応します. 例えば次のようなメトリクスのそれぞれが time series のそれぞれに対応します.

```
requests_total{path="/status", method="GET", instance=”10.0.0.1:80”}
requests_total{path="/status", method="POST", instance=”10.0.0.3:80”}
requests_total{path="/", method="GET", instance=”10.0.0.2:80”}
...
```

横軸は時間を表していて, 時間が経つに連れ Prometheus がスクレイプを行うことにより図の右端にデータサンプルが追加されていきます. 図で歯抜けている部分があるように見えるのはスクレイプに失敗する可能性などを考慮しているからです.

![1.1.time_series_query.png](https://qiita-image-store.s3.amazonaws.com/0/18658/abdee725-bda2-c73b-54c7-db61c679aa75.png)

TSDB にクエリを実行する際には, メトリクスを取得した期間と, 複数のメトリクス名やラベルの組み合わせに対応した time series が関連します. そのため, 取得する必要のあるサンプルの範囲は赤い矩形で囲まれた複数の範囲に広がっていることになります.

### 必要なディスク容量の見積もり

![1.2.estimation.png](https://qiita-image-store.s3.amazonaws.com/0/18658/39fe53e6-04fd-a56b-076d-88f0dcc12bc9.png)

Prometheus では timestamp と値に 8 byte ずつが必要なため, そのままディスクへ格納すると, 必要なディスク容量は次の要件の場合には約 7 TB となります.

| 項目 | 値 |
|---|---|
| アクティブな time series 数 | 500 万 |
| スクレイプ間隔 | 30 秒に 1 回 |
| データ保持期間 | 1 ヶ月 |

### どうやって圧縮するか

![1.3.timestamp_compression.png](https://qiita-image-store.s3.amazonaws.com/0/18658/51ffefd8-1c93-4f1b-06d6-ba9e484b8c09.png)

timestamp の圧縮には, サンプル間の timestamp の差分が timestamp そのものより小さな値になることを利用して行います. さらに, 差分同士の差分を取ることでより小さな値にすることができます.

![1.4.value_compression.png](https://qiita-image-store.s3.amazonaws.com/0/18658/5638194d-e243-aebc-cd49-6feca8f9440e.png)

値の圧縮も同様に差分を取ることで小さな値にできる場合が多いことを利用します.

![1.5.revised_estimation.png](https://qiita-image-store.s3.amazonaws.com/0/18658/1e092d5a-c96c-c711-e590-238b51f4b5e4.png)

この圧縮により同じ要件による必要ディスク容量が 7 TB から 0.8 TB へ改善することができるようです.

**NOTE**: この圧縮方法については v1 の段階でも起動オプションの追加により利用できるようです. (Refs: <https://prometheus.io/docs/prometheus/1.8/storage/#chunk-encoding>)

### インデクスの改善

![1.6.chrun.png](https://qiita-image-store.s3.amazonaws.com/0/18658/64d9242d-c276-447e-5e48-ad88402f3132.png)

Prometheus を Kubernetes で利用する際, ローリングアップデートなどにより Pod が作り直されると, Pod やコンテナの ID などがメトリクスのラベルに入っている場合は time series がそこで途切れてしまい, 上の図のように全体のうちのアクティブな time series の比が小さくなってしまいます.

![1.7.re-revised_estimation.png](https://qiita-image-store.s3.amazonaws.com/0/18658/fbbe09c2-0d56-9896-307f-48fe7dd6ef43.png)

そのため, このような環境ではアクティブな time series の数よりも, 非アクティブなものを含む全体の time series の数がクエリのパフォーマンスに影響を与えます.

![1.8.index.png](https://qiita-image-store.s3.amazonaws.com/0/18658/d41d871b-fd76-ebeb-e6c9-fa476ed3c821.png)

Prometheus v2 ではラベルと time series (メトリクス) の間の転置インデクスを導入することで改善を図っています. 各 time series は ID を持ち, その ID は任意のラベルとその値の組み合わせを持つ time series のリスト (転置インデクス) の中で利用されます. このリストをメンテナンスしていくことで, クエリの実行時には検索条件に対応したリストの共通部分を探すだけで目的の time series を見つけることができます.

### ストレージレイアウトの変更

![1.14.chunks_v1.png](https://qiita-image-store.s3.amazonaws.com/0/18658/0913a0e4-e1b8-0750-5946-03643073af45.png)

Prometheus v1 では一つの time series に付き, 一つのファイルを用意することで管理しています. また, ファイルへの書き込みはチャンクと呼ばれる単位で行われています.

![1.15.blocks_v2.png](https://qiita-image-store.s3.amazonaws.com/0/18658/a2f73ea5-f413-0ff2-b58d-28d79a695bca.png)

Prometheus v2 では時刻で区切られたブロックにより, 全ての time series を管理するように変更されています. 

![1.16.storage_layout.png](https://qiita-image-store.s3.amazonaws.com/0/18658/0c52ddbc-3930-dada-8cec-de043b13f7d8.png)

それぞれのブロックはディレクトリに分けて管理され, それぞれにインデクスとチャンクが用意されています. 新しいデータは Write Ahead Log (WAL) に書き込まれ, ある程度の時間が経つとブロックとして書き出されます. そのため, ブロックへの書き込みは行なわれないような設計になっています.

### ベンチマーク

ベンチマークの環境は次の通りです.

| 項目 | 値 |
|---|---|
| 取得するサンプル数 | 110,000 個毎秒 |
| ターゲット数 | 850 |

比較する Prometheus の設定は次の通りです.

- Prometheus v1.5.2 - クエリの負荷あり
- Prometheus v1.5.2 - クエリの負荷なし
- Prometheus v2 (どの時点のものか不明) - クエリの負荷あり
- Prometheus v2 (どの時点のものか不明) - クエリの負荷なし

![1.9.benchmarks_memory.png](https://qiita-image-store.s3.amazonaws.com/0/18658/ca6b70e8-5aab-5a95-793f-798572b75839.png)

上の図はメモリの使用量 (GB) の推移を表しています. クエリの負荷のあり, なしに関わらず, v2 のメモリ使用量が v1.5.2 に比べて 3-4 分の 1 程度に抑えられています. 6 時間経過したあたりで v1.5.2 がスパイクしているのは, それがデータ保持期間に到達したからで, データの削除にもある程度の負荷がかかっていることを意味しています.

![1.10.benchmarks_cpu.png](https://qiita-image-store.s3.amazonaws.com/0/18658/272095db-f4d9-c240-3db5-07e7368c77d9.png)

上の図は CPU 使用率 (cores/second) の推移を表しています. メモリ使用率の場合と同様の傾向ですが, v1.5.2 に比べて v2 が 3-10 分の 1 程度の使用率に抑えられています.

![1.11.benchmarks_disk_writes.png](https://qiita-image-store.s3.amazonaws.com/0/18658/18e6bb7d-b9fc-7cb4-c503-3890c65543de.png)

上の図はディスクへの書き込み (MB/second) の推移を表しています. このメトリクスが最も劇的に改善されています.

![1.12.benchmarks_disk_size.png](https://qiita-image-store.s3.amazonaws.com/0/18658/aa3fe71b-3a91-560e-76a1-bf21e8a02e27.png)

上の図はディスクサイズ (GB) の推移を表しています. v1.5.2 に比べて v2 では 5 分の 1 以下に抑えられていることが分かります.

![1.13.benchmarks_query_latency.png](https://qiita-image-store.s3.amazonaws.com/0/18658/581b7908-5a5f-6595-f97a-7c2c43650a53.png)

上の図はクエリのレイテンシ (秒, 99パーセンタイル) の推移を表しています.