Prometheus TSDB分析
概述
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千多万每秒)提供了更高的单机性能。
参考
Writing a Time Series Database from Scratch
Prometheus官方介绍
时间序列数据的存储和计算 - 开源时序数据库解析(四)