TSDB 概述:
Head: 数据库的内存部分
Block: 磁盘上持久块,是不变的
WAL: 预写日志系统
M-map: 磁盘及内存映射
粉红色框是传入的样品,样品先进入Head中存留一会,然后到磁盘、内存映射中(蓝色框)。然后当内存映射块时间长到某点,就会作为持久块存在硬盘上,进一步一个个合并。
超出保留时间就会被删除。
Head的生命周期
(这里讨论的都是基于一个time series,同样适用于其他time series)
当样品存入时,chunk变活跃,红色块是我们唯一可以主动编写数据的单位。
将样品存入时,我们还会将其记录在WAL中,在机器崩溃时可以从中恢复数据。
默认chunkRange为120,如果2小时内,chunkRange都是120的满状态情况,就会新建一个chunk,在这篇博客里,我们默认抓取间隔为15s。所以从空chunk到满chunk需要30min.
黄色block是刚刚填充满的满chunk,红色块则是创建的新块。
在prometheus v2.19.0中,我们不会把所有块存在内存中。当新chunk建立,full chunk被放入磁盘,如果只存内存索引时就将full chunk存入磁盘的memory-mapped中。然后在我们需要时,用索引将chunk动态加载到内存中。
随着新样品不断进入,新的chunk被分割。
被放入磁盘及内存映射中
过了一段时间head如图示,我们认为红色chunk几乎满了,那么我们在head里存在3h的数据。(6个chunk,每个30min满)chunkRange3/2
当数据存储至chunkRange3/2时,(2h时)就会压缩成一个恒久chunk。此时WAL被截断,创建一个checkPoint。
以上周而复始及为head的功能。
如果TSDB必须重新启动(优雅退出、突然),他将使用内存映射的chunk和WAL重放数据和事件,重新构建内存索引等。
WAL基础知识
当编写、修改、删除数据库的数据之前,事件首先被记录在WAL中,然后才进行相关操作。
在Prometheus TSDB中写入WAL
records类型
prometheus中存在三种wal类型:
- Series 当一个新的series到达时记录一次,先将series写入Head,再写wal
- Samples 先写wal,再写入Head,
- Tombstones 用于记录删除特定series,soft delete
WAL截断
在上一篇中介绍过,当Head Block阶段时也同时截断wal,在实际实现时,由于写请求可以是随机的,因此要确定WAL段中样本的时间范围而不遍历所有记录比较困难,因此简化为直接删除2/3。
这里有个问题,series只存一次,在wal也一样,直接截断wal可能会丢失series,甚至依然是还在Head中的series,这里引入check point来处理改问题。
CheckPointing
假设截断的时间点是T,要实现保留series,需要遍历所有要删除的wal文件:
- 删除所有不在Head中的series
- 删除所有在时间T之前的samples
- 删除所有在时间T之前的tombstones
- 保留剩余的series、samples、tombstones到checkpoint.X文件
data
└── wal
├── checkpoint.000002
| ├── 000000
| └── 000001
├── 000003
从WAL恢复时,直接从最新的一个checkpoint.X文件开始,扫描X+1对应的WAL文件名;
WAL底层写入磁盘使用32KB page。
当一个chunk满了(120个sample或者2h)以后,会使用m-map(2)将其flush到disk中,其数据不在占用内存,索引依然保留在内存中。
写 chunks
从第1部分重新开始,当一个chunk已满时,我们剪切了一个新chunk,而较旧的chunk变得不可变,只能从中读取(下面的黄色块)。
而不是将其存储在内存中,我们将其刷新到磁盘并存储引用以供以后访问。
此刷新的chunk是磁盘中的内存映射chunk。不变性是最重要的因素,否则对于每个sample而言,重写压缩chunk的效率都太低。
File
这些chunks保留在其自己的目录中,称为chunks_head,其文件序列类似于WAL(除了以1开头)。例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDhNZkQZ-1636355505443)(/download/attachments/1195970022/image-1636352695839.png?version=1&modificationDate=1636352693244&api=v2)]
文件由8B的文件头和chunks数据组成:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ac2Cikgz-1636355505445)(/download/attachments/1195970022/image-1636352672901.png?version=1&modificationDate=1636352670327&api=v2)]
Chunks
┌─────────────────────┬───────────────────────┬───────────────────────┬───────────────────┬───────────────┬──────────────┬────────────────┐
| series ref <8 byte> | mint <8 byte, uint64> | maxt <8 byte, uint64> | encoding <1 byte> | len <uvarint> | data <bytes> │ CRC32 <4 byte> │
└─────────────────────┴───────────────────────┴───────────────────────┴───────────────────┴───────────────┴──────────────┴────────────────┘
series ref :是内存中某个的series索引的id,这里是为了方便后续宕机恢复时重建索引数据
mint,maxt:chunk时间戳
encoding :压缩编码方式
len :数据长度
data :压缩编码的数据
如何访问
内存中依然有每个chunks的地址(也叫ref,64位,前32位是文件号,后32位是文件内字节偏移)和mint、maxt。
在代码中看,每个chunks文件都是一个byte slice,当访问该byte slice时,mmap自动映射到磁盘中的文件。
series ref id ->[(ref,mint,maxt),(addr2,mint2,maxt2)…]-> mmap chunks
重启实例时重新生成HeadBlock
因为所有满了的chunk都被mmap到了磁盘目录chunks_head下面,所以在重建HeadBlock时,直接可以使用这些数据重建满chunks,然后再根据wal重建还没有满的chunks。
重建在磁盘中的满的mmap chunks只需要重建index(如上节所示map)即可;
重建wal中的未满的chunks就需要重建index和数据。
Mmap优点
- 节省约15%~50%内存使用
- 节省启动时间15%~30%,主要是不需要重建所有wal了
GC
tsdb定期截断HeadBlock为一个2h(default)的block,在截断时,也是内存gc的时刻,对于截断时间T之前的index都可以gc。
Block
磁盘上的Block可以认为是一个小型的db,自带了index和chunks data,其中chunks可以是多个文件。
每个Block用一个Universally Unique Lexicographically Sortable Identifier (ULID)唯一识别。
每个Block是不可变的数据文件,只能标记删除,不同block之间没有引用关系。
默认一个新生成的block是2h的数据,随着时间推移,多个2h block可以compate(merge)为一个更大的block。
block组成
meta.json (file): block元数据,json文件。
chunks (directory): chunks数据文件
index (file): 索引文件
tombstones (file): 删除标记文件
meta.json
meta记录当前block的ulid,chunks数量,samples数量,时间跨度,comapction等级等信息。
{
"ulid": "01EM6Q6A1YPX4G9TEB20J22B2R",
"minTime": 1602237600000,
"maxTime": 1602244800000,
"stats": {
"numSamples": 553673232,
"numSeries": 1346066,
"numChunks": 4440437
},
"compaction": {
"level": 1,
"sources": [
"01EM65SHSX4VARXBBHBF0M0FDS",
"01EM6GAJSYWSQQRDY782EA5ZPN"
]
},
"version": 1
}
chunks
chunks是一个目录,内部存放着多个有序排列的chunks文件,每个文件默认512MB,保存着所有chunks data。
此时的chunk格式发生了变化,具体如下:
┌───────────────┬───────────────────┬──────────────┬────────────────┐
│ len <uvarint> │ encoding <1 byte> │ data <bytes> │ CRC32 <4 byte> │
└───────────────┴───────────────────┴──────────────┴────────────────┘
相比HeadBlock mmap chunk格式,缺少了series ref、mint 和 maxt,这三个信息现在存入了index文件
index
index是一个倒排索引,index的实现比较常规,主要由以下几部分:
一个符号表,每个series中存一个特定符号的addr能节省内存
每个series有一个固定的series ID可以寻址,且每个series内包含在该block内对应的所有chunks地址信息和时间范围信息
基于单个字符的倒排索引,比如有两个series{a=“b1”, x=“y1”} 和 {a=“b2”, x=“y2”},字符a和x将各自都有一个索引指向这两个series,index(a)->[ref(series id1), ref(series id2)]
因为每个字符的索引slice可能很大,所以在实现上,做了一个二级index,字符a -> index(a) -> [ref(series id1), ref(series id2)]
Postings Offset Table :第一级index,存储每个label对(name,value)对应的Postings N
Postings N: 第二级index,存放一个label对应的所有series id
Series:各个Series信息,包含series ID,按id有序排列,每个series内包含在该block内对应的所有chunks地址信息和时间范围信息可以查找数据
Label Index 和 Label Offset Tabel 废弃了
query分为三种:
- LabelNames() : 查询所有label names(直接循环遍历Posting Offset Table,获取所有的label name即可。)
- LabelValues(name) : 查询指定label name对应的所有values(直接循环遍历Posting Offset Table,获取所有的label name与name一致的label value即可。)
- Select([]matcher) : 返回符合matcher的series
Select([]matcher)
matcher用于选择出符合指定label name和label value的series,有以下4种:
- labelName="" ,相等匹配
- labelName!="" ,不相等匹配
- labelName=~"",正则正匹配
- labelName!~"" ,正则负匹配
匹配步骤:
- 获得单个matcher匹配的所有series
- 取交集
Postings Offset Table存放了所有label name和label value的组合,并且指向Postings N,Postings N包含了所有的series ID。
相等匹配:遍历Postings Offset Table,找到name和value都相等的项就可以获得对应的Postings,然后获得series
正则正匹配:需要遍历所有Postings Offset Table,找到所有符合的name和value项,然后也可以获得series
同理,负向匹配一样,但负匹配可能会匹配到大量数据,所以,不允许单个负匹配查询,必须结合一个相等或者正则正匹配才能使用负匹配。
然后,获取到所有单个matcher的Postings后,进行交集,获取最终的Postings,然后遍历Postings对应的series id,根据series id中的chunk addr和mint、 maxt取得数据返回。
最后,如果查询时间跨越多个block,那么应该单独查每个block,然后将结果merge。
Querying Head block
Head block在内存中建立了map[labelName]map[labelValue]postingsList结构的索引,可以直接查上述过程。
整理 (https://ganeshvernekar.com/blog/prometheus-tsdb-the-head-block/) 学习内容