共计 2344 个字符,预计需要花费 6 分钟才能阅读完成。
概述
Prometheus 是著名开源监控项目,其监控任务调度给具体的服务器,该服务器到目标上抓取监控数据,然后保存在本地的 TSDB 中。自定义强大的 PromQL 语言查询实时和历史时序数据,支持丰富的查询组合。
Prometheus 1.0 版本的 TSDB(V2 存储引擎)基于 LevelDB,并且使用了和 Facebook Gorilla 一样的压缩算法,能够将 16 个字节的数据点压缩到平均 1.37 个字节。
Prometheus 2.0 版本引入了全新的 V3 存储引擎,提供了更高的写入和查询性能。本文主要分析该存储引擎设计思路。
设计思路
Prometheus 将 Timeseries 数据按 2 小时一个 block 进行存储。每个 block 由一个目录组成,该目录里包含:一个或者多个 chunk 文件(保存 timeseries 数据)、一个 metadata 文件、一个 index 文件(通过 metric name 和 labels 查找 timeseries 数据在 chunk 文件的位置)。最新写入的数据保存在内存 block 中,达到 2 小时后写入磁盘。为了防止程序崩溃导致数据丢失,实现了 WAL(write-ahead-log)机制,将 timeseries 原始数据追加写入 log 中进行持久化。删除 timeseries 时,删除条目会记录在独立的 tombstone 文件中,而不是立即从 chunk 文件删除。
这些 2 小时的 block 会在后台压缩成更大的 block,数据压缩合并成更高 level 的 block 文件后删除低 level 的 block 文件。这个和 leveldb、rocksdb 等 LSM 树的思路一致。
这些设计和 Gorilla 的设计高度相似,所以 Prometheus 几乎就是等于一个缓存 TSDB。它本地存储的特点决定了它不能用于 long-term 数据存储,只能用于短期窗口的 timeseries 数据保存和查询,并且不具有高可用性(宕机会导致历史数据无法读取)。
Prometheus 本地存储的局限性,所以它提供了 API 接口用于和 long-term 存储集成,将数据保存到远程 TSDB 上。该 API 接口使用自定义的 protocol buffer over HTTP 并且并不稳定,后续考虑切换为 gRPC。
磁盘文件结构
内存中的 block
内存中的 block 数据未刷盘时,block 目录下面主要保存 wal 文件。
./data/01BKGV7JBM69T2G1BGBGM6KB12
./data/01BKGV7JBM69T2G1BGBGM6KB12/meta.json
./data/01BKGV7JBM69T2G1BGBGM6KB12/wal/000002
./data/01BKGV7JBM69T2G1BGBGM6KB12/wal/000001
持久化的 block
持久化的 block 目录下 wal 文件被删除,timeseries 数据保存在 chunk 文件里。index 用于索引 timeseries 在 wal 文件里的位置。
./data/01BKGV7JC0RY8A6MACW02A2PJD
./data/01BKGV7JC0RY8A6MACW02A2PJD/meta.json
./data/01BKGV7JC0RY8A6MACW02A2PJD/index
./data/01BKGV7JC0RY8A6MACW02A2PJD/chunks
./data/01BKGV7JC0RY8A6MACW02A2PJD/chunks/000001
./data/01BKGV7JC0RY8A6MACW02A2PJD/tombstones
mmap
使用 mmap 读取压缩合并后的大文件(不占用太多句柄),建立进程虚拟地址和文件偏移的映射关系,只有在查询读取对应的位置时才将数据真正读到物理内存。绕过文件系统 page cache,减少了一次数据拷贝。查询结束后,对应内存由 Linux 系统根据内存压力情况自动进行回收,在回收之前可用于下一次查询命中。因此使用 mmap 自动管理查询所需的的内存缓存,具有管理简单,处理高效的优势。
从这里也可以看出,它并不是完全基于内存的 TSDB,和 Gorilla 的区别在于查询历史数据需要读取磁盘文件。
Compaction
Compaction 主要操作包括合并 block、删除过期数据、重构 chunk 数据。其中合并多个 block 成为更大的 block,可以有效减少 block 个数,当查询覆盖的时间范围较长时,避免需要合并很多 block 的查询结果。
为提高删除效率,删除时序数据时,会记录删除的位置,只有 block 所有数据都需要删除时,才将 block 整个目录删除。因此 block 合并的大小也需要进行限制,避免保留了过多已删除空间(额外的空间占用)。比较好的方法是根据数据保留时长,按百分比(如 10%)计算 block 的最大时长。
Inverted Index
Inverted Index(倒排索引)基于其内容的子集提供数据项的快速查找。简而言之,我可以查看所有标签为 app=“nginx”的数据,而不必遍历每一个 timeseries,并检查是否包含该标签。
为此,每个时间序列 key 被分配一个唯一的 ID,通过它可以在恒定的时间内检索,在这种情况下,ID 就是正向索引。
举个栗子:如 ID 为 9,10,29 的 series 包含 label app=”nginx”,则 lable “nginx” 的倒排索引为 [9,10,29] 用于快速查询包含该 label 的 series。
性能
在文章 Writing a Time Series Database from Scratch 里,作者给出了 benchmark 测试结果为 Macbook Pro 上写入达到 2000 万每秒。这个数据比 Gorilla 论文中的目标 7 亿次写入每分钟(1000 千多万每秒)提供了更高的单机性能。