Prometheus TSDB (Part 2): WAL and Checkpoint
作者:互联网
Prometheus TSDB (Part 2): WAL and Checkpoint
本文译自Ganesh Vernekar 的 prometheus-tsdb-wal-and-checkpoint。
文章目录
- Prometheus TSDB (Part 2): WAL and Checkpoint
- Introduction
- WAL Basics
- Writing to WAL in Prometheus TSDB
- WAL truncation and Checkpoingting
- Replaying the WAL
- Low level details of writing to and reading from WAL
- Code reference
Introduction
在TSDB系列博客的第一部分,我提到了为了持久化的考虑,我们会将抓取的样本数据先写入WAL,并且当WAL被清理时,会创建一个checkpoint。在本篇博客,我们会简要的讨论WAL的基本概念,然后深入谈下WAL和checkpoint在Prometheus的TSDB中是如何设计的。
由于这是我关于Prometheus TSDB系列博客的一部分,所以建议你在阅读时先阅读 第一部分。
WAL Basics
WAL是一个顺序存储的日志,包含database中发生的事件。当数据库中发生 writing/modifying/deleting 数据的事件时,会先写入到WAL中,然后才会在database中执行必要的操作。
当机器或程序突然崩溃时,我们可以通过顺序的重播WAL中存储的事件来恢复数据。这对于内存型数据库而言非常有效,因为如果没有WAL,那当数据库崩溃时,所有内存中的数据都会丢失。
在关系型数据库中,这是一个广泛使用的用于实现持久化的特性。类似的,Prometheus也是通过这种机制来为Head Block提供持久化能力。同时,Prometheus在重启时能优雅的恢复内存中的数据状态,这也是通过WAL来实现的。
在Prometheus的体系中,WAL只是用来记录事件,或在启动时恢复内存中数据。它并没有用于其他读写相关的用途。
Writing to WAL in Prometheus TSDB
Types of records
TSDB的写请求中,包含了时序相关的所有Label values以及相关的Sample,分别对应两种需记录的数据类型,Series和Samples。
对Seires记录而言,它包含写请求中所有的时序的Label values,在创建Series时,会生成唯一的引用,该引用用于查找(关联)该Seires。相应的,Samples记录中会包含Series的唯一引用(用于关联Seires记录,指明该Sample的归属)以及该Seires的所有样本数据。
最后一种Record类型是Tombstones,用于删除请求。它包含待删除的Seires的唯一引用以及时间跨度。
这些Record的格式可以从 这里 找到,此处就不再展开描述。
Writing them
如果写请求中包含Sample数据,那每次写请求时都会往Samples记录中写入数据,相反的,Series记录是公用的,仅在第一接收到这个新的Seires时才会写入。
如果一个写请求包含一个新的Seires,那Series的写入一定会先于Sample,因为显而易见的是,如果Sample的写入在Seires之前,则Sample中的对Seires的唯一引用将会找不到任何Seires的记录。
Seires是在写入Head之后再写入WAL的,因为需要先获取到唯一的引用。而Samples则是在写入Head之前写入WAL的。
通过将所有不同的Time Seires都分组到相同的Record中,每个写请求只会写入一个Seires和Smaples Record。如果写请求中的所有Sample对应的Seires都已经存在于Head中,则只有Samples会被写入WAL。
当我们接收到删除请求时,我们并不会直接从内存中删除。我们会通过存储Tombstones(包含待删除的时序以及时间跨度)来表明这个删除操作。在实际执行这个删除请求前,会先将Timebstones写入WAL。
How it looks on disk
WAL是通过顺序的数字命名的文件来存储的,每个文件默认为128MiB,这些文件被称为“segment”。
data
└── wal
├── 000000
├── 000001
└── 000002
文件的大小是有限制的,以使得对旧文件的垃圾回收更简单。显而易见,文件序号是不断增加的。
WAL truncation and Checkpoingting
我们需要定时的删除老的WAL的segment,否则磁盘早晚会满,并且TSDB的启动周期也会变长,因为它需要重播非常多的事件到内存中。一般来讲,任何你不再需要的数据,你都会想要清空它。
WAL tuncation
当Head Block被清空时,WAL也会同时被清空(第一部分有简单提及)。对WAL中的文件的删除并不是随机发生的,它总是删除前N个文件,并且不会在递增的文件序列中产生间隙。
因为写请求是随机发生的,所以想在segment中高效的找到时间跨度中的所有样本并不容易,所以我们默认删除前2/3rd
的segment。
data
└── wal
├── 000000
├── 000001
├── 000002
├── 000003
├── 000004
└── 000005
在如上的例子中,000000
000001
000002
000003
会被删除。
补充一点:因为Series只会被写入一次,所以如果我们盲目的删除WAL segment,会导致我们丢失这些Seires,从而导致启动时无法恢复数据,同时,我们也可能会丢失前2/3rd
segment中所包含的并未在Head Block中清除的Sample数据。这也是为什么我们需要checkpointing的原因。
Checkpointing
在清空WAL之前,我们会为待删除的segment创建checkpoint,你可以想象checkpoint是一个过滤后的WAL。考虑当Head Block的清空会删除掉时间T
之前的数据,如上例所示,checkpointing会在000000
000001
000002
000003
中发生:
- 删除Head中不再存在的Series;
- 删除所有在时间
T
之前的Sample数据; - 删除所有在时间
T
之前的Tombstones; - 保留所有需要的Seires、Samples和Tombstones(顺序同之前保持一致);
当从Record中移除不需要的项时,删除操作也可以看做是一个重写操作(因为一个Record中可能包含多个Seires、Sample和Tombstone)。
通过这种方式,你不会丢失在Head中仍然存在的Seires、Sample和Tombstone。checkpoint的名字叫checkpoint.X
,X
代表创建这个checkpoint的segment的数字文件名(此处为000003
,下一章节中你会知道我们为什么这么做)。
在WAL清理并创建checkpoint之后,磁盘上的文件会看起来类似如下示例:
data
└── wal
├── checkpoint.000003
| ├── 000000
| └── 000001
├── 000004
└── 000005
如果在这之前有其它老的checkpoints,它们也会在此时被删除。
译者注:
checkpoint.00003
中仍包含000000
000001
两个segment,可能是其中存在需要保留的Seires
Replaying the WAL
我们一开始会从最后一个checkpoint开始遍历(编号最大的checkpoint即是最后一个)。对于checkpoint.X
,X
就是用于告诉我们需要从哪一个segment开始进行重播,这个segment就是X+1
。所以在上面的例子中,在对checkpoint.000003
进行重播后,我们会开始对segment 000004
进行重播。
你可能会考虑为什么我们在checkpoint中需要跟踪segment的段号,因为最终我们都会删除掉在checkpoint之前的segment。原因是,创建checkpoint和删除segment并非原子性的,在这两个动作之间任何事情的发生都可能阻止segment的删除,所以我们有时不得不重播额外的2/3rd
的segment,即便它们已经在Head中被删除,这使得重播的过程变得更缓慢。
对于单个Record,它们会发生如下动作:
- Series:以相同的引用(ID)创建该Seires,对于相同的Series可能存在多条记录,Prometheus会处理这种情况;
- Samples:Sample数据会被添加到Head中,Sample记录中的引用关系会指明该Sample被添加到哪个Seires,如果该引用指向空,则该Sample会被忽略;
- Tomstones:Tomstones会被添加的Head中,其保存的引用会指明其所属的Seires;
Low level details of writing to and reading from WAL
当写请求的量较大时,你希望能避免随机写入造成的 写入放大 ,同时,当你进行读操作时,你希望确保它没有损坏(突然关机或磁盘故障时很容易重现)。
Promethues包含WAL的一般的实现,其记录的是字节流,调用方必需负责对Record进行编码。为解决上述的问题,WAL包做了如下工作:
- 数据是一次一页的写入,每页的长度是32KiB,如果Record的大小超过32KiB,它会被拆解为更小的片段,每个片段都会有一个WAL头,以便知道该片段是头部、中部还是尾部;
- 通过Record尾部的checksum来校验其没有被损坏;
WAL包负责无缝的连接Record的片段,并且在迭代时检查checksum,以便后续进行重播。
WAL中的Record并没有被严格压缩(或者说根本没有压缩),所以WAL包提供了一个选项,可以通过Snappy来压缩记录(现在默认是启用的)。这个信息被保存在WAL头,所以你打算启用或禁用压缩,这些压缩和未压缩的记录可以保存在一起。
Code reference
tsdb/wal/wal.go
中有将Record进行字节化处理并提供低级磁盘交互的实现。该文件有写入字节流和对Record进行迭代的实现。
tsdb/record/record.go
包含各种Record的编解码的逻辑。
tsdb/wal/checkpoint.go
中包含checkpoint的逻辑。
tsdb/head
中包含:
- 创建Record,对Record进行编码并调用WAL的写入;
- 调用checkpoint并清空WAL;
- 重播WAL,对其进行解码并存储为内存状态;
标签:checkpoint,WAL,Seires,写入,Checkpoint,Record,Prometheus,Part,segment 来源: https://blog.csdn.net/m0_51450908/article/details/121982967