普罗米修斯2.0的存储(TSDB)结构

我将解释Prometheus 2.0最引人注目的修改——重写了存储prometheus/tsdb的结构,说明了有何变化。

请用中文进行总结。

(This is a paraphrase of “要約” in Chinese.)

在 Prometheus 2.0 中,存储系统完全重写,对以往的问题进行了改进,极大地提升了性能。

    • 大量のファイルができることによるパフォーマンスの問題

時間の範囲ごとに block という単位でまとめて管理されるようになった

メモリ管理の問題

mmap によってカーネル側のキャッシュ管理になった

歯抜データによるインデックスの問題

転置インデックスが導入された

我们比较了 Prometheus v1.6.3 和 v2.0.0 在运行了 24 小时后的数据,确认了官方公告所说的,无论是CPU还是内存性能都有显著提升。

image.png

过去 (Storage v2) 面临的问题

之前的 Prometheus 存储在内部被称为存储v2,它以时间序列为单位创建文件,并通过 Level DB 管理索引。每个时间序列根据唯一的标签组合进行创建,例如 node_cpu { instance = “x”,cpu = “cpu0” } 和 node_cpu { instance = “x”,cpu = “cpu1” } 将成为不同的时间序列(在这种情况下,cpu 标签不同)。

数据文件的结构如下所示。

.
├── 00 # time series の fingerprint (SHA 256) の先頭 2 文字がディレクトリ名
│   ├── 000011da572e51.db # time series ごとのデータファイル。ファイル名は fingerprint の先頭 2 文字目移行
│   ├── 00002c17915cc5.db
│   ├── 0000b1da572e51.db
│   ├── ....
│   └── fffffd7fd348a0.db
├── 01
├── .....
├── ff # 00 ~ ff までのディレクトリが必要に応じて作られる
│   ├── ....
│   └── ffff6136313c3f.db
├── archived_fingerprint_to_timerange # level DB の index
├── archived_fingerprint_to_metric    # level DB の index
├── labelname_to_labelvalues          # level DB の index
├── labelpair_to_fingerprints         # level DB の index
└── heads.db # head データ

Storage v2存在以下问题。

    • Time Series ごとにファイルができるため大量の小さいファイルができる

inode の限界の問題
SSD の Write amplification によるパフォーマンス低下
retention 期間を過ぎたデータの削除の CPU 負荷が高い

インデックスの問題

Kubernetes のような環境では Pod が頻繁に再作成され非アクティブな time series が多くできてしまう
この問題は Churn と呼ばれている
参考: https://qiita.com/kkohtaka/items/7e99e9eaadbbad6cbb2b

メモリ管理の問題

チャンクをオブジェクトとして管理しているので GC のような処理が必要

配置最新的Prometheus/TSDB

在新的Prometheus / TSDB中,引入了“block”的概念,用于将时间序列数据按照一定的时间间隔进行分组和处理。在每个block中,有一个名为“chunks”的目录,其中包含实际存储时间序列数据的文件(例如000001)。这些文件以512 MB为单位进行分割并进行管理。

undefined

阻挡

区块以目录的形式进行管理,保留着一定时间范围的时间序列。每个时间范围内会生成多个目录,并在后面的压缩处理中自动地将其合并成较大的区块,在一定的时间周期内。由于每个区块都具有索引和元数据,因此可以将每个区块视为一个小型数据库。目录名称采用了可排序的 ULID(Universally Unique Lexicographically Sortable Identifier)作为唯一标识符。该区块由以下文件组成。

ファイル用途chunks/Time Series が保存されているディレクトリ。512 MB ごとに分割。参考: Chunks Disk Formatindexラベルと Time Series の転置インデックス。参考: Index Disk Formatmeta.jsonメタデータ。ブロックの保持データの開始時間、終了時間などが記録されている。tombstones削除済みデータの情報。参考: Tombstones Disk Format
{
    "version": 1,
    "ulid": "01BZ033BRP0EQBFW51QKD1TT9Y",
    "minTime": 1510718400000, # 保持データの開始時間
    "maxTime": 1510747200000, # 保持データの終了時間
    "stats": {
        "numSamples": 21396291,
        "numSeries": 176339,
        "numChunks": 325936
    },
    "compaction": {
        "level": 3, # コンパクションのレベル
        "sources": [ # コンパクションした元のディレクトリ
            "01BYZ7MCSVJHG5D17M4YZF0JD1",
            "01BYZEG4434G74SCPWJGQ2TDEQ",
            "01BYZNBVBAJ34NWT3CWRFNB43Z",
            "01BYZW7JJ2YQVKM1WX08FDQ7ZN"
        ]
    }
}

预写日志 (Write Ahead Log)

仅写入最新数据,因此作为首要处理,并定期在Write Ahead Log(WAL)目录中持久化存储。

经过一段时间(在2.0.0时点的实现中为3个小时),它将转变为block状态。

使用mmap进行缓存管理

新的TSDB使用mmap系统调用将时间序列的数据文件映射到内存中的字节切片。由于使用mmap进行映射,文件缓存管理将通过内核透明处理。由于内核会根据需要释放缓存,因此prometheus不再需要占用内存来进行缓存管理,而不再需要通过storage.local.target-heap-size进行内存大小调优。

索引是标签和块引用的转置索引,其中块引用由文件偏移量(32位)+序列(32位)构成,以便直接从内存中访问。

参考:Chunk 的格式

┌────────────────────────────────────────┬──────────────────────┐
│ magic(0x85BD40DD) <4 byte>             │ version(1) <1 byte>  │
├────────────────────────────────────────┴──────────────────────┤
│ ┌───────────────┬───────────────────┬──────┬────────────────┐ │
│ │ len <uvarint> │ encoding <1 byte> │ data │ CRC32 <4 byte> │ │
│ └───────────────┴───────────────────┴──────┴────────────────┘ │
└───────────────────────────────────────────────────────────────┘

以下是将 Prometheus v1.6.3 和 v2.0.0 在 24 小时内使用的 CPU 和内存(RSS)进行比较的结果。它们都在 Kubernetes 上运行,并且配置如目标等均相同。两者的性能都有大幅提升。由于v1.6.3使用堆内存管理缓存,导致内存使用量(RSS)逐渐增加,而v2.0.0由于使用mmap由内核进行管理,因此可以看出内存大小基本保持不变。

image.png

数据压缩

为了提高查询的效率,数据块在一定时间间隔内通过压缩处理并合并成较大的块。压缩级别被作为TSDB的选项以数组形式传递,并且在Prometheus 2.0.0的实现中,默认为每2小时增加3倍。这个初始值(2小时)可以通过–storage.tsdb.max-block-duration来指定。最大时间默认为保留期的10%,可以通过–storage.tsdb.max-block-duration来指定。

# 3 倍ずつ 10 段階増えていく
rngs := tsdb.ExponentialBlockRanges(int64(time.Duration(opts.MinBlockDuration).Seconds()*1000), 10, 3)

# 秒単位の結果
[7200000 21600000 64800000 194400000 583200000 1749600000 5248800000 15746400000 47239200000 141717600000]

# 時間に直したもの
[2h, 6h, 18h, 2d6h, 6d18h 20d6h, 60d18h, 182d6h, 546d18h, 1640d6h]
undefined

在图中,处理会像上述描述的那样进行。在压缩的下一个级别的时间段(2小时、6小时、18小时,呈指数增长)内,将块分组并进行压缩。

在 Prometheus 2.0.0 版本中,当进行压缩时,会先对时间窗口进行对齐,并将这个时间窗口进行分组。因此不仅仅是简单地将 2 小时 * 3 块压缩为 6 小时,还可能存在多种组合的分组方式。根据压缩处理的测试代码 compact_test.go,编写测试案例如下。

compactor, err := NewLeveledCompactor(nil, nil, []int64{
    7200000,  // デフォルトの時間枠のブロックを 3 レベル分を設定
    21600000,
    64800000,
}, nil)

// 中略

{
    metas: []dirMeta{ // ブロックのメタ情報の配列
        //             開始時間        終了時間 (2時間後)
        metaRange("1", 1510704000000, 1510711200000, nil), // 開始時間が時間枠 21600000 (6h) の倍数
        metaRange("2", 1510711200000, 1510718400000, nil),
        metaRange("3", 1510718400000, 1510725600000, nil),
        metaRange("4", 1510725600000, 1510732800000, nil),
    },
    // コンパクト化される結果
    expected: []string{"1", "2", "3"}, // 時間枠の倍数ピッタリの場合だけ 1, 2, 3 がコンパクト化される。
},
{
    metas: []dirMeta{
        metaRange("1", 1510704000001, 1510711200001, nil), // 時間枠 21600000 の倍数を超えている場合
        metaRange("2", 1510711200001, 1510718400001, nil),
        metaRange("3", 1510718400001, 1510725600001, nil),
        metaRange("4", 1510725600001, 1510732800001, nil),
    },
    expected: []string{"1", "2"}, // 3 は時間枠をはみ出すため、1, 2 だけがコンパクト化される。
},
{
    metas: []dirMeta{
        metaRange("1", 1510703999999, 1510711199999, nil), // 時間枠 21600000 の倍数を下回る場合
        metaRange("2", 1510711199999, 1510718399999, nil),
        metaRange("3", 1510718399999, 1510725599999, nil),
        metaRange("4", 1510725599999, 1510732799999, nil),
    },
    expected: []string{"2", "3"}, // 1 が時間枠で連続しないため、2, 3 がコンパクト化される
},

{
    metas: []dirMeta{
        metaRange("1", 1510718400000, 1510725600000, nil), // 開始時間が時間枠の倍数でない
        metaRange("2", 1510725600000, 1510732800000, nil),
        metaRange("3", 1510732800000, 1510740000000, nil),
        metaRange("4", 1510740000000, 1510747200000, nil),
    },
    expected: []string{"2", "3", "4"},  // アラインの関係で 1 はコンパクト化されない
},

请参考

    • https://fabxc.org/blog/2017-04-10-writing-a-tsdb/

 

    • https://github.com/prometheus/tsdb/tree/master/Documentation/format

 

    https://www.percona.com/live/e17/sites/default/files/slides/Evolution%20of%20the%20Prometheus%20TSDB%20-%20FileId%20-%20115511.pdf
bannerAds