CLOVER🍀

That was when it all began.

Prometheusのストレージのドキュメントをさらっと読んでみて、retentionの設定もしてみる

これは、なにをしたくて書いたもの?

Prometheusのストレージまわりの、お勉強に、と。

Prometheusのデータ(TSDB)のSnapshotを取得して、リストアまで - CLOVER🍀

こちらの続きで、今度はストレージのドキュメントを読み、オプションについて見ていこうと思います。

対象とするPrometheusのバージョンは、2.9.2とします(ドキュメントは2.9)。

ちょっと調べてみると、過去の情報と現在のドキュメントの内容はそれなりに異なる箇所が多そうなので、その時点の
バージョンのドキュメントをちゃんと確認した方が良いと思います。

あくまで、現時点(2.9.2)での話として。

Prometheusのストレージ

Prometheusのストレージまわりのドキュメントは、こちらです。

Storage | Prometheus

見ていくと、ざっくりこんな感じのことが書かれています。

  • Prometheusには、ローカルディスク上に持つ時系列データベースが含まれている
  • オプションで、リモートのストレージシステムと統合することもできる

リモートとのストレージシステムと統合することについては、また機会を改めて見ていきましょう。

ここからは、ローカルストレージについて見ていきます。

ローカルストレージについて

Prometheusのローカルの時系列データベースは、データを独自のフォーマットでディスクに保存します。

ディスク上のレイアウトについてですが、以下のような要素で構成されるようです。

  • 取得したデータは、2時間ごとのブロックにまとめられる
  • 2時間ごとの各ブロックは、以下の内容を含む
    • その期間のすべてのデータを含む、チャンクディレクトリ(1つ以上のチャンクファイルを含む)
    • メタデータファイル
    • インデックスファイル(メトリクス名と、チャンクファイル内の時系列にラベルを付与したものがインデックスされる)
    • Tombstoneファイル
      • APIを使用して、時系列データを削除した場合にできるファイル
      • チャンクファイルからすぐにデータが削除されるわけではない

また、Prometheusは収集したデータをすぐに永続化するのではなく、まずメモリ上に保持されています。

Prometheusがクラッシュしたりして再起動した場合は、WAL(Write Ahead Log)を使うことでクラッシュから保護されています。

WALは、以下の特徴を持ちます。

  • 「wal」ディレクトリに128MBごとに保存されている
  • 「wal」ディレクトリに含まれるファイルは、まだ圧縮されていないRawデータが含まれている
    • このため、通常のブロックファイルよりかなり大きい
  • Prometheusは最低3つのWALファイルを保持している
    • 高トラフィックなサーバーでは、少なくとも2時間分のRawデータを保存する必要があるため、3つを超えるWALファイルができることがある

で、ドキュメントに書かれているディレクトリ構造がこちら。ここまで登場した要素が書かれている感じですね。

./data/01BKGV7JBM69T2G1BGBGM6KB12
./data/01BKGV7JBM69T2G1BGBGM6KB12/meta.json
./data/01BKGTZQ1SYQJTR4PB43C8PD98
./data/01BKGTZQ1SYQJTR4PB43C8PD98/meta.json
./data/01BKGTZQ1SYQJTR4PB43C8PD98/index
./data/01BKGTZQ1SYQJTR4PB43C8PD98/chunks
./data/01BKGTZQ1SYQJTR4PB43C8PD98/chunks/000001
./data/01BKGTZQ1SYQJTR4PB43C8PD98/tombstones
./data/01BKGTZQ1HHWHV8FBJXW1Y3W0K
./data/01BKGTZQ1HHWHV8FBJXW1Y3W0K/meta.json
./data/01BKGV7JC0RY8A6MACW02A2PJD
./data/01BKGV7JC0RY8A6MACW02A2PJD/meta.json
./data/01BKGV7JC0RY8A6MACW02A2PJD/index
./data/01BKGV7JC0RY8A6MACW02A2PJD/chunks
./data/01BKGV7JC0RY8A6MACW02A2PJD/chunks/000001
./data/01BKGV7JC0RY8A6MACW02A2PJD/tombstones
./data/wal/00000000
./data/wal/00000001
./data/wal/00000002

より詳細な内容を知りたければ、TSDBのドキュメントへ。

tsdb/README.md at v0.7.1 · prometheus/tsdb · GitHub

ちなみに、起動直後のdataディレクトリの中身は、こんな感じです。

$ find data -type f
data/wal/00000000
data/lock

lockというファイルは出てきていませんでしたね。

$ ls -l data/lock 
-rw-r--r-- 1 xxxxx xxxxx 0 May  2 10:51 data/lock

0バイトのファイルです。

このファイルは、文字通り排他に使うファイルのようで、すでにPrometheusが起動した状態で、リッスンポートを
変えて起動したりしようとすると、ロックが取れずに起動に失敗します。

$ ./prometheus --web.listen-address="0.0.0.0:9091"

...

level=error ts=2019-05-02T10:53:06.753Z caller=main.go:717 err="opening storage failed: lock DB directory: resource temporarily unavailable"

https://github.com/prometheus/prometheus/blob/v2.9.2/vendor/github.com/prometheus/tsdb/db.go#L280-L283

先に進みましょう。

最初の2時間のブロックは、最終的にはバックグラウンドでより長いブロックに圧縮されます。

ローカルストレージの制限

ローカルストレージの制限は、クラスタ化もされておらず、またレプリカも持たないことです。

なので、ディスクやノード障害対して、耐性がありません。スケーラブルでも耐久性もない、直近の短命な
スライディングウィンドウデータとして扱われるべきです。

耐久性の要件が厳しくなければ、ローカルストレージでも最大数年のデータを保存できるかもしれない、くらいのことが
書かれています…。

このあたりのことから、ローカルストレージのスケーラビリティと耐久性についての課題を解決するために、リモートの
ストレージシステムと統合する仕組みがある、ということみたいです。

Remote storage integrations

ローカルストレージに関するオプション

ストレージに関するドキュメントの以下の部分に、ストレージに関して指定できるオプションが書かれています。

Operational aspects

Prometheus自身のヘルプから、「storage」を含むオプションを表示してみると、

$ ./prometheus -h 2>&1 | grep storage
      --storage.tsdb.path="data/"  
                                 Base path for metrics storage.
      --storage.tsdb.retention=STORAGE.TSDB.RETENTION  
                                 storage. This flag has been deprecated, use
                                 "storage.tsdb.retention.time" instead
      --storage.tsdb.retention.time=STORAGE.TSDB.RETENTION.TIME  
                                 How long to retain samples in storage. When
                                 "storage.tsdb.retention". If neither this flag
                                 nor "storage.tsdb.retention" nor
                                 "storage.tsdb.retention.size" is set, the
      --storage.tsdb.retention.size=STORAGE.TSDB.RETENTION.SIZE  
      --storage.tsdb.no-lockfile  
      --storage.tsdb.allow-overlapping-blocks  
      --storage.remote.flush-deadline=<duration>  
      --storage.remote.read-sample-limit=5e7  
      --storage.remote.read-concurrent-limit=10  

「--storage.remote.〜」については、リモートストレージシステムとの統合に関するオプションなので、ここでは対象外。

その他のオプションについて、説明を見てみます。

      --storage.tsdb.path="data/"  
                                 Base path for metrics storage.
      --storage.tsdb.retention=STORAGE.TSDB.RETENTION  
                                 [DEPRECATED] How long to retain samples in storage. This flag has been deprecated, use "storage.tsdb.retention.time" instead
      --storage.tsdb.retention.time=STORAGE.TSDB.RETENTION.TIME  
                                 How long to retain samples in storage. When this flag is set it overrides "storage.tsdb.retention". If neither this flag nor
                                 "storage.tsdb.retention" nor "storage.tsdb.retention.size" is set, the retention time defaults to 15d.
      --storage.tsdb.retention.size=STORAGE.TSDB.RETENTION.SIZE  
                                 [EXPERIMENTAL] Maximum number of bytes that can be stored for blocks. Units supported: KB, MB, GB, TB, PB. This flag is experimental and can be
                                 changed in future releases.
      --storage.tsdb.no-lockfile  
                                 Do not create lockfile in data directory.
      --storage.tsdb.allow-overlapping-blocks  
                                 [EXPERIMENTAL] Allow overlapping blocks which in-turn enables vertical compaction and vertical query merge

すでにDeprecatedなオプションは無視します。

  • --storage.tsdb.path … Prometheusがデータを保存する際のベースパス。デフォルトは、「data」
  • --storage.tsdb.retention.time … 古いデータをいつ削除するかを指定する。デフォルトは、「15d」
  • --storage.tsdb.retention.size … (実験的)ストレージブロックが最大で使用できるサイズを指定する
  • --storage.tsdb.no-lockfile … dataディレクトリ内に、ロックファイルを作成しない
  • --storage.tsdb.allow-overlapping-blocks … (実験的)オーバーラップするブロックを許可する。圧縮、クエリーの縦方向のマージが可能になる

「--storage.tsdb.retention.time」のデフォルト値は15日で、フォーマットにはy、w、d、h、m、s、msが利用できます。

prometheus/db.go at v2.9.2 · prometheus/prometheus · GitHub

https://github.com/prometheus/prometheus/blob/v2.9.2/vendor/github.com/prometheus/common/model/time.go#L192-L208

データの保持期間に関するリテンションポリシーは時間とサイズの2つがありますが、使われるのは最初に動作した方だとか。
※サイズはまだ実験的なので、スルーしましたが

「--storage.tsdb.allow-overlapping-blocks」については、まだ実験的な感じがするので、スルーします…。

Implement vertical query merging and compaction · Issue #90 · prometheus/tsdb · GitHub

なお、2時間ごとにブロックにまとめられる、というのは、このあたりから来ている気がします。

https://github.com/prometheus/prometheus/blob/v2.9.2/vendor/github.com/prometheus/tsdb/db.go#L51

ところでですね、PrometheusのWeb Consoleから「Status」→「Command-Line Flags」を見ると、もう少し指定できそうな
オプションが多いように見えるのですが…。

f:id:Kazuhira:20190502222000p:plain

今回は、とりあえず深追いしない…。

ローカルストレージで必要とするディスク容量

Prometheusは、ひとつのサンプルあたり、1〜2バイトほどを使用するそうです。

なので、Prometheusが要求するサーバーのディスク容量は、以下の計算式で算出できます、と。

needed_disk_space = retention_time_seconds * ingested_samples_per_second * bytes_per_sample
必要なディスクサイズ = 保持期間(秒) × 秒あたりの取得サンプル数 * サンプルあたりのバイト数

1秒あたりに取得するサンプルを小さくするには、取得する時系列データの数を減らす(収集するターゲットを減らす、
ターゲットあたりの取得項目を減らす)か、取得間隔を長く設定するかのどちらかです。

ドキュメントによると、取得したデータは圧縮されるため、取得する項目数を減らす方が効果的だとか。

なお、ローカルストレージが破損した場合は、以下の対処になるとか。

  • Prometheusをシャットダウンして、データディレクトリを削除する(これが最善らしい)
    • 当然、全データを失う
  • 個々のブロックディレクトリを削除する
    • 削除したブロックに含まれる、2時間のデータは失う

どちらにせよ、Prometheusのローカルストレージは長期間のデータ保存を意図していないことがドキュメントでは強調されています。

データの保持期間を指定してみる

それでは、データの保持期間を短めにして、動作を確認してみましょう。

以下のような、データの取得間隔を短めにしたPrometheusの設定ファイルを用意し、起動。
prometheus.yml

global:
  scrape_interval:     1s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 1s # Evaluate rules every 15 seconds. The default is every 1 minute.

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['localhost:9090']

データの取得対象は、デフォルト(自分自身)のままです。

起動オプションとして、早めに消えることを確認したくて「--storage.tsdb.retention.time」を3分に設定しました。
あと、「--storage.tsdb.min-block-duration」を指定していますが、なんでこれを付けたかはまた後で…。

$ ./prometheus --storage.tsdb.retention.time 3m --storage.tsdb.min-block-duration 1m
level=info ts=2019-05-02T14:09:07.183Z caller=main.go:321 msg="Starting Prometheus" version="(version=2.9.2, branch=HEAD, revision=d3245f15022551c6fc8281766ea62db4d71e2747)"

Web Consoleで、起動オプションを認識していることを確認。

f:id:Kazuhira:20190502231638p:plain

とりあえず、Web ConsoleでCPU使用時間(process_cpu_seconds_total)を表示。10分間の表示で見ています。

f:id:Kazuhira:20190502231018p:plain

1分経過すると、こんなログが出力され

level=info ts=2019-05-02T14:10:44.219Z caller=compact.go:499 component=tsdb msg="write block" mint=1556806153112 maxt=1556806200000 ulid=01D9WE45RQ73CF18Y6J2BPMN1Q duration=100.343317ms
level=info ts=2019-05-02T14:10:44.237Z caller=head.go:540 component=tsdb msg="head GC completed" duration=1.785374ms

ブロックができます。

$ find data -type f
data/wal/00000000
data/lock
data/01D9WE45RQ73CF18Y6J2BPMN1Q/meta.json
data/01D9WE45RQ73CF18Y6J2BPMN1Q/index
data/01D9WE45RQ73CF18Y6J2BPMN1Q/tombstones
data/01D9WE45RQ73CF18Y6J2BPMN1Q/chunks/000001

このログが4回出力されるまでは、ブロックディレクトリが追加されていきます。

level=info ts=2019-05-02T14:10:44.219Z caller=compact.go:499 component=tsdb msg="write block" mint=1556806153112 maxt=1556806200000 ulid=01D9WE45RQ73CF18Y6J2BPMN1Q duration=100.343317ms
level=info ts=2019-05-02T14:10:44.237Z caller=head.go:540 component=tsdb msg="head GC completed" duration=1.785374ms
level=info ts=2019-05-02T14:11:30.196Z caller=compact.go:499 component=tsdb msg="write block" mint=1556806200000 maxt=1556806260000 ulid=01D9WE5JP7Y6FFGZMKKS1KK2AD duration=77.507689ms
level=info ts=2019-05-02T14:11:30.217Z caller=head.go:540 component=tsdb msg="head GC completed" duration=1.549613ms
level=info ts=2019-05-02T14:12:30.229Z caller=compact.go:499 component=tsdb msg="write block" mint=1556806260000 maxt=1556806320000 ulid=01D9WE7D97CMB7R8RS12G9E9YW duration=109.711593ms
level=info ts=2019-05-02T14:12:30.251Z caller=head.go:540 component=tsdb msg="head GC completed" duration=1.421509ms
level=info ts=2019-05-02T14:13:30.197Z caller=compact.go:499 component=tsdb msg="write block" mint=1556806320000 maxt=1556806380000 ulid=01D9WE97W9WEKZCQ0957KVH2GX duration=75.948389ms
level=info ts=2019-05-02T14:13:30.215Z caller=head.go:540 component=tsdb msg="head GC completed" duration=1.492116ms

こんな感じに。

$ find data -type f
data/wal/00000000
data/01D9WE7D97CMB7R8RS12G9E9YW/meta.json
data/01D9WE7D97CMB7R8RS12G9E9YW/index
data/01D9WE7D97CMB7R8RS12G9E9YW/tombstones
data/01D9WE7D97CMB7R8RS12G9E9YW/chunks/000001
data/lock
data/01D9WE45RQ73CF18Y6J2BPMN1Q/meta.json
data/01D9WE45RQ73CF18Y6J2BPMN1Q/index
data/01D9WE45RQ73CF18Y6J2BPMN1Q/tombstones
data/01D9WE45RQ73CF18Y6J2BPMN1Q/chunks/000001
data/01D9WE5JP7Y6FFGZMKKS1KK2AD/meta.json
data/01D9WE5JP7Y6FFGZMKKS1KK2AD/index
data/01D9WE5JP7Y6FFGZMKKS1KK2AD/tombstones
data/01D9WE5JP7Y6FFGZMKKS1KK2AD/chunks/000001
data/01D9WE97W9WEKZCQ0957KVH2GX/meta.json
data/01D9WE97W9WEKZCQ0957KVH2GX/index
data/01D9WE97W9WEKZCQ0957KVH2GX/tombstones
data/01D9WE97W9WEKZCQ0957KVH2GX/chunks/000001

f:id:Kazuhira:20190502231415p:plain

ここで、5回目のコンパクションが起こると

level=info ts=2019-05-02T14:10:44.219Z caller=compact.go:499 component=tsdb msg="write block" mint=1556806153112 maxt=1556806200000 ulid=01D9WE45RQ73CF18Y6J2BPMN1Q duration=100.343317ms
level=info ts=2019-05-02T14:10:44.237Z caller=head.go:540 component=tsdb msg="head GC completed" duration=1.785374ms
level=info ts=2019-05-02T14:11:30.196Z caller=compact.go:499 component=tsdb msg="write block" mint=1556806200000 maxt=1556806260000 ulid=01D9WE5JP7Y6FFGZMKKS1KK2AD duration=77.507689ms
level=info ts=2019-05-02T14:11:30.217Z caller=head.go:540 component=tsdb msg="head GC completed" duration=1.549613ms
level=info ts=2019-05-02T14:12:30.229Z caller=compact.go:499 component=tsdb msg="write block" mint=1556806260000 maxt=1556806320000 ulid=01D9WE7D97CMB7R8RS12G9E9YW duration=109.711593ms
level=info ts=2019-05-02T14:12:30.251Z caller=head.go:540 component=tsdb msg="head GC completed" duration=1.421509ms
level=info ts=2019-05-02T14:13:30.197Z caller=compact.go:499 component=tsdb msg="write block" mint=1556806320000 maxt=1556806380000 ulid=01D9WE97W9WEKZCQ0957KVH2GX duration=75.948389ms
level=info ts=2019-05-02T14:13:30.215Z caller=head.go:540 component=tsdb msg="head GC completed" duration=1.492116ms
level=info ts=2019-05-02T14:14:30.248Z caller=compact.go:499 component=tsdb msg="write block" mint=1556806380000 maxt=1556806440000 ulid=01D9WEB2F4SWD5FDP5EBV7BD0E duration=132.373486ms
level=info ts=2019-05-02T14:14:30.270Z caller=head.go:540 component=tsdb msg="head GC completed" duration=1.201336ms

ブロックディレクトリが増えなくなります。「01D9WE45RQ73CF18Y6J2BPMN1Q」という名前のディレクトリがいなくなりました。

$ find data -type f
data/01D9WEB2F4SWD5FDP5EBV7BD0E/meta.json
data/01D9WEB2F4SWD5FDP5EBV7BD0E/index
data/01D9WEB2F4SWD5FDP5EBV7BD0E/tombstones
data/01D9WEB2F4SWD5FDP5EBV7BD0E/chunks/000001
data/wal/00000000
data/01D9WE7D97CMB7R8RS12G9E9YW/meta.json
data/01D9WE7D97CMB7R8RS12G9E9YW/index
data/01D9WE7D97CMB7R8RS12G9E9YW/tombstones
data/01D9WE7D97CMB7R8RS12G9E9YW/chunks/000001
data/lock
data/01D9WE5JP7Y6FFGZMKKS1KK2AD/meta.json
data/01D9WE5JP7Y6FFGZMKKS1KK2AD/index
data/01D9WE5JP7Y6FFGZMKKS1KK2AD/tombstones
data/01D9WE5JP7Y6FFGZMKKS1KK2AD/chunks/000001
data/01D9WE97W9WEKZCQ0957KVH2GX/meta.json
data/01D9WE97W9WEKZCQ0957KVH2GX/index
data/01D9WE97W9WEKZCQ0957KVH2GX/tombstones
data/01D9WE97W9WEKZCQ0957KVH2GX/chunks/000001

この結果、最初の時間のデータがなくなります。「01D9WE45RQ73CF18Y6J2BPMN1Q」というのは、最初に作られた
ディレクトリでした。

f:id:Kazuhira:20190502231541p:plain

というわけで、「--storage.tsdb.retention.time」で指定した保持期間を越えたデータは削除されていることが確認できました。

ところで、今回、起動オプションに「--storage.tsdb.retention.time」だけではなくて「--storage.tsdb.min-block-duration」も
指定しました。

$ ./prometheus --storage.tsdb.retention.time 3m --storage.tsdb.min-block-duration 1m

「--storage.tsdb.min-block-duration」を指定したことで、ブロックの範囲が1分になっています。

最初、以下のように「--storage.tsdb.retention.time」を指定しただけだとデータがまったく消えず、「もしかしてWALにいる間の
データは対象に入らないのでは?」と思い、変更に至りました。

$ ./prometheus --storage.tsdb.retention.time 3m

つまり、長時間待つのが嫌で、極端に短い時間をretentionに設定したのが完全に裏目に出たようです。

実際、削除対象はブロック単位のようです。

https://github.com/prometheus/prometheus/blob/v2.9.2/vendor/github.com/prometheus/tsdb/db.go#L670

ブロックディレクトリ内のmeta.jsonを見ると、minTimeとmaxTimeで、どの時間の範囲のデータが含まれているか確認することが
できます。
data/01D9WEEQN5CH9F53GAFCN3G14E/meta.json

{
    "ulid": "01D9WEEQN5CH9F53GAFCN3G14E",
    "minTime": 1556806500000,
    "maxTime": 1556806560000,
    "stats": {
        "numSamples": 27120,
        "numSeries": 452,
        "numChunks": 452,
        "numBytes": 77220
    },
    "compaction": {
        "level": 1,
        "sources": [
            "01D9WEEQN5CH9F53GAFCN3G14E"
        ]
    },
    "version": 1
}

ここからわかることは、「--storage.tsdb.min-block-duration」を設定することはそうないかもしれませんが、
「--storage.tsdb.retention.time」はブロックディレクトリで持つ範囲(デフォルト2時間)の倍数でなければ意味がない
ということですね。

覚えておきましょう。

いい確認になりました。