Skip to content
Published on

[Prometheus] TSDB内部構造:WAL、Chunks、Blocks、Compaction

Authors

1. 概要

PrometheusのTSDB(Time Series Database)は時系列データに最適化されたローカルストレージエンジンです。FacebookのGorilla論文に基づく圧縮技法とLSM-treeに基づくブロックベース構造を組み合わせて、高い書き込み性能と効率的なストレージを提供します。

この記事ではTSDBのディレクトリ構造からWAL、Head Block、永続ブロック、コンパクションまで全ストレージ階層を分析します。

2. TSDBディレクトリ構造

Prometheusのデータディレクトリは以下の構造を持ちます:

data/
  |-- wal/
  |     |-- 00000001
  |     |-- 00000002
  |     +-- 00000003
  |
  |-- chunks_head/
  |     |-- 000001
  |     +-- 000002
  |
  |-- 01BKGV7JBM69T2G1BGBGM6KB12/   (Block ULID)
  |     |-- meta.json
  |     |-- index
  |     |-- chunks/
  |     |     |-- 000001
  |     |     +-- 000002
  |     +-- tombstones
  |
  |-- 01BKGTZQ1SYQJTR4PB43C8PD98/   (Block ULID)
  |     |-- meta.json
  |     |-- index
  |     |-- chunks/
  |     +-- tombstones
  |
  |-- lock
  +-- queries.active

各ディレクトリとファイルの役割:

  • wal/: Write-Ahead Logセグメントファイル
  • chunks_head/: Head Blockのメモリマップされたチャンクファイル
  • ULIDディレクトリ/: 各永続ブロック(ULIDは時間ベースの一意ID)
  • lock: プロセス排他アクセス保証用ロックファイル
  • queries.active: 現在のアクティブクエリ追跡

3. Write-Ahead Log(WAL)

3.1 WALの目的

WALはデータ耐久性を保証するコアメカニズムです。すべての受信データはまずWALに記録された後、メモリ(Head Block)に適用されます。Prometheusが異常終了した場合、WALをリプレイしてHead Blockを復旧します。

3.2 セグメントファイル構造

WALは固定サイズ(デフォルト128MB)のセグメントファイルで構成されます:

セグメントファイル内部構造:
+----------+----------+----------+-----+
| Record 1 | Record 2 | Record 3 | ... |
+----------+----------+----------+-----+

各Record:
+--------+--------+---------+------+
| Type   | Length | CRC32   | Data |
| 1 byte | varint | 4 bytes | ...  |
+--------+--------+---------+------+

3.3 レコードタイプ

WALには4つの主要レコードタイプがあります:

Series Record(タイプ1): 新しい時系列の登録

Series Record:
+----------+----------------------------+
| Series ID| Labels (name/value pairs)  |
+----------+----------------------------+

Samples Record(タイプ2): サンプルデータ

Samples Record:
+----------+-----------+-------+
| Series ID| Timestamp | Value |
+----------+-----------+-------+

Tombstones Record(タイプ3): 削除マーキング

時系列データの特定時間範囲の削除を要求する際に記録されます。実際の削除はコンパクション時に実行されます。

Exemplars Record(タイプ4): Exemplarデータ

トレースIDなどの追加メタデータを含むExemplarサンプルです。

3.4 WAL管理

  • セグメントローテーション: セグメントが128MBに達すると新セグメントが作成されます
  • セグメントクリーンアップ: Head Blockがコンパクトされて永続ブロックが生成されると、該当時間範囲のWALセグメントは削除されます
  • チェックポイント: WALの高速リプレイのために定期的にチェックポイントが作成されます
WALチェックポイントプロセス:
1. 現在のアクティブ時系列リスト収集
2. チェックポイントディレクトリ作成(checkpoint.NNNNN)
3. アクティブ時系列のSeries Record記録
4. 以前のチェックポイントと該当WALセグメントを削除

3.5 WAL圧縮

--storage.tsdb.wal-compressionフラグでWAL圧縮を有効化できます。Snappy圧縮を使用し、一般的にWALサイズを約半分に削減します。CPUオーバーヘッドは軽微です。

4. Head Block

4.1 Head Block構造

Head BlockはTSDBで最新のデータを保持するインメモリ構造です:

Head Block:
+------------------------------------------+
| memSeries Map                             |
|   series_id_1 -> memSeries_1             |
|   series_id_2 -> memSeries_2             |
|   ...                                     |
+------------------------------------------+
| Posting Lists (in-memory index)          |
+------------------------------------------+
| Stripe Lock Pool                         |
+------------------------------------------+

4.2 memSeries構造

各時系列はmemSeries構造体で表現されます:

memSeries:
+------------+
| ref (ID)   |
| labels     |
| chunks     | --> [chunk_0] -> [chunk_1] -> [chunk_current]
| headChunk  | --> 現在のアクティブチャンク(書き込み可能)
| firstTs    |
| lastTs     |
| lastValue  |
+------------+

4.3 チャンクエンコーディング

PrometheusはGorilla論文の圧縮技法を使用します:

タイムスタンプエンコーディング(Delta-of-Delta):

Sample 1: t1(元の値を保存)
Sample 2: d1 = t2 - t1(最初のdelta)
Sample 3: dd = (t3 - t2) - (t2 - t1)(delta-of-delta)

スクレイピングが規則的ならddはほとんど0に近い
-> 非常に少ないビットで表現可能

ビットエンコーディング:
dd == 0: 1 bit ('0')
-63 <= dd <= 64: 2 + 7 = 9 bits
-255 <= dd <= 256: 2 + 9 = 11 bits
-2047 <= dd <= 2048: 2 + 12 = 14 bits
その他: 4 + 32 = 36 bits

値エンコーディング(XOR):

Sample 1: v1(元のfloat64を保存、64 bits)
Sample 2: xor = v2 XOR v1
Sample 3: xor = v3 XOR v2

連続した値が類似していればXOR結果のほとんどが0
-> leading zerosとtrailing zerosを利用した圧縮

ビットエンコーディング:
xor == 0: 1 bit ('0')
leading/trailingが前回と同じ: 2 + significant bits
その他: 2 + 5(leading) + 6(significant_length) + significant bits

この圧縮方式でサンプルあたり平均1.37バイトという驚異的な圧縮率を達成します。

4.4 チャンクライフサイクル

1. 新サンプル到着
2. headChunkに追加
3. headChunkが120サンプルまたはchunkRange(2時間)に到達
4. headChunk完了処理(immutable)
5. chunks_head/ディレクトリにmmapファイルとして記録
6. 新headChunk生成
7. 以前のチャンクはmmapでアクセス(メモリから解放可能)

4.5 Memory Mapped Chunks

Prometheus 2.19以降、Head Blockの完了チャンクはchunks_head/ディレクトリにmmapファイルとして記録されます。これにより:

  • メモリ使用量を大幅に削減(OSがページキャッシュで管理)
  • クラッシュ復旧時のWALリプレイ時間が短縮
  • チャンクデータは必要な時のみメモリにロード

5. 永続ブロック構造

5.1 ブロック概要

Head Blockのデータは一定時間(デフォルト2時間)後に永続ブロックにコンパクトされます:

Block Directory:
01BKGV7JBM69T2G1BGBGM6KB12/
  |-- meta.json      (ブロックメタデータ)
  |-- index          (時系列インデックス)
  |-- chunks/
  |     |-- 000001   (チャンクデータ)
  |     +-- 000002
  +-- tombstones      (削除マーキング)

5.2 meta.json

ブロックのメタデータを含みます:

{
  "ulid": "01BKGV7JBM69T2G1BGBGM6KB12",
  "minTime": 1602547200000,
  "maxTime": 1602554400000,
  "stats": {
    "numSamples": 1234567,
    "numSeries": 5678,
    "numChunks": 9012
  },
  "compaction": {
    "level": 1,
    "sources": ["01BKGV7JBM69T2G1BGBGM6KB12"]
  },
  "version": 1
}

5.3 インデックス構造

インデックスファイルは時系列の高速検索のための構造を含みます:

Index File Structure:
+------------------+
| Symbol Table     |  (全ラベル名/値の辞書)
+------------------+
| Series           |  (時系列別ラベルとチャンク参照)
+------------------+
| Label Index      |  (ラベル名 -> 可能な値)
+------------------+
| Postings         |  (ラベルペア -> 時系列IDリスト)
+------------------+
| Postings Offset  |  (ポスティングリストのオフセットテーブル)
+------------------+
| TOC              |  (Table of Contents)
+------------------+

5.4 Posting List

Posting Listは転置インデックスの核心です:

例:
job="prometheus" -> [1, 3, 5, 7, 9]
job="node"       -> [2, 4, 6, 8, 10]
instance="localhost:9090" -> [1, 2]
instance="localhost:9100" -> [3, 4]

クエリ: job="prometheus" AND instance="localhost:9090"
  -> [1, 3, 5, 7, 9] INTERSECT [1, 2]
  -> [1]

Posting Listはソートされた状態で保存され、積集合/和集合演算がO(n)時間で実行されます。

5.5 チャンクファイル

チャンクファイルには実際の時系列サンプルデータが圧縮保存されます:

Chunk File Format:
+--------+--------+--------+-----+
| Chunk 1| Chunk 2| Chunk 3| ... |
+--------+--------+--------+-----+

各Chunk:
+----------+----------+--------+------+
| Length   | Encoding | Data   | CRC  |
| uvarint  | 1 byte   | ...    | 4B   |
+----------+----------+--------+------+

6. コンパクション

6.1 コンパクション概要

コンパクションは小さなブロックを大きなブロックにマージするプロセスです:

Level 0: [2h] [2h] [2h] [2h] [2h] [2h]
                     |
                     v (compaction)
Level 1: [  6h  ] [  6h  ] [  6h  ]
                     |
                     v (compaction)
Level 2: [      18h       ] [  6h  ]

6.2 レベルベースコンパクション

Prometheusはレベルベースのコンパクション戦略を使用します:

コンパクション判定基準:
1. 同レベルの3つ以上のブロックが存在する時
2. マージ結果の時間範囲がリテンション期間の10%を超えない時
3. マージ結果がmax-block-durationを超えない時

コンパクションプロセス:
1. マージするブロックを選択
2. 新ブロックディレクトリ作成(一時)
3. 全ソースブロックの時系列をマージ
4. tombstone適用(削除データ除去)
5. 新インデックス生成
6. 新チャンクファイル生成
7. meta.json更新(level増加、sources記録)
8. アトミックに新ブロックをアクティブ化
9. 以前のブロック削除(遅延)

6.3 Verticalコンパクション

時間範囲が重複するブロックが存在する際に実行される特殊コンパクションです。バックフィルやout-of-orderサンプル許可設定時に発生します。

6.4 削除処理

データ削除は2段階で処理されます:

段階1 - 削除リクエスト(API呼び出し):
  - tombstonesファイルに削除範囲を記録
  - 実際のデータはまだ存在

段階2 - コンパクション時:
  - tombstonesを適用して削除範囲のデータを除外
  - 新ブロックには削除データが含まれない

7. リテンション管理

7.1 時間ベースリテンション

--storage.tsdb.retention.time=15d(デフォルト)

動作:ブロックのmaxTimeが現在時刻 - retentionより前であれば削除対象。
コンパクション周期ごとに確認し、ブロック単位で削除されます。

7.2 サイズベースリテンション

--storage.tsdb.retention.size=10GB

動作:全ブロックの総ディスク使用量を計算し、
制限超過時に最も古いブロックから削除します。
WALとchunks_headはサイズ計算から除外されます。

8. mmapとメモリ管理

Prometheus TSDBはmmap(メモリマップドファイル)を積極活用します:

mmap使用領域:
1. 永続ブロックのインデックスファイル
2. 永続ブロックのチャンクファイル
3. Head Blockの完了チャンク(chunks_head/)

mmapの利点:
- OSがページキャッシュで自動的にメモリ管理
- 必要な部分のみメモリにロード(lazy loading)
- メモリ不足時にOSが自動的にページ解放
- Goプロセスのヒープメモリ負担軽減

9. Out-of-Orderサンプル処理

Prometheus 2.39以降、out-of-orderサンプルをサポートします:

--storage.tsdb.out-of-order-time-window=30m

動作:
1. Head Blockの最新タイムスタンプより過去のサンプルが到着
2. OOOウィンドウ内であれば別のWBL(Write-Behind Log)に記録
3. Head BlockにOOOチャンクとして保存
4. コンパクション時にin-orderチャンクとマージ

10. パフォーマンス特性

10.1 書き込みパフォーマンス

書き込みパス:
WAL Write(順次I/O)-> Head Block Update(メモリ)

特性:
- WALは順次書き込みのみでディスクI/Oを最適化
- Head Blockはメモリ演算で非常に高速
- 毎秒数百万サンプルの収集が可能

10.2 読み取りパフォーマンス

読み取りパス:
1. クエリパースと最適化
2. ポスティングリストで関連時系列検索
3. 該当時間範囲のチャンクロード
4. チャンクデコードと結果計算

最適化:
- ポスティングリストの積集合で高速時系列フィルタリング
- mmapで必要なチャンクのみロード
- 時間範囲で不要なブロックをスキップ

11. まとめ

Prometheus TSDBは時系列データの特性を最大限活用した設計を示しています。delta-of-deltaとXORエンコーディングでサンプルあたり1.37バイトの圧縮率を達成し、WALでデータ耐久性を保証し、レベルベースコンパクションでクエリ効率を維持します。

次の記事ではPromQLエンジンの内部構造を分析します。レキサーとパーサー、AST構造、クエリ評価エンジンの動作方式を解説する予定です。