Prometheus TSDB (Part 3): Memory Mapping of Head Chunks from Disk

本文译自Ganesh Vernekar 的 prometheus-tsdb-mmapping-head-chunks-from-disk


文章目录

  • Prometheus TSDB (Part 3): Memory Mapping of Head Chunks from Disk
  • Introduction
  • Writing these chunks
  • Format on disk
  • The File
  • Chunks
  • Reading these chunks
  • Replaying on startup
  • Enhancements that this brings in
  • Memory savings
  • Faster startup
  • Garbage collection
  • Code reference

Introduction

在TSDB系列博客的 第一部分,我提到过一次“当chunk满的时候”,它会被刷到磁盘并memory-mapped,这有助于减少Head Block的内存占用,同时也有助于加快在第二部分中提到的WAL重播的速度。在本篇博客中,我们来更深入的讨论这部分的设计。

Writing these chunks

回顾第一部分,当chunk满的时候,我们会切出一个新的chunk,老的chunks将成为不可变的chunk,并且只能读取数据(黄色块为老的chunk,红色块为新切出的chunk)。

prometheus operator 支持remote write_迭代


老的chunk我们不再存储在内存中,而是刷到磁盘上并存储一个引用,用于后续访问它。

prometheus operator 支持remote write_数据库_02


这个被刷到磁盘的chunk就是memory-mapped chunk。此处“不可变”是一个非常重要的特性,因为如果要重写这些已压缩的chunk,对所有Sample而言效率太低。

Format on disk


The File

这些chunks存储在chunks_head目录中,其文件序列名和WAL中的类似。如下:

data
├── chunks_head
|   ├── 000001
|   └── 000002
└── wal
    ├── checkpoint.000003
    |   ├── 000000
    |   └── 000001
    ├── 000004
    └── 000005

这些文件的最大大小为128MiB,我们来详细看一下每个文件的结构,单个文件中包含一个8Byte的头。

┌──────────────────────────────┐
│  magic(0x0130BC91) <4 byte>  │
├──────────────────────────────┤
│    version(1) <1 byte>       │
├──────────────────────────────┤
│    padding(0) <3 byte>       │
├──────────────────────────────┤
│ ┌──────────────────────────┐ │
│ │         Chunk 1          │ │
│ ├──────────────────────────┤ │
│ │          ...             │ │
│ ├──────────────────────────┤ │
│ │         Chunk N          │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘

Magic Number是用于标识这类文件类型为memory-mapped chunks文件,Chunk Format告诉我们用什么方式来解码这个文件,padding用于后续的扩展。

Chunks

单个Chunk格式如下所示

┌─────────────────────┬───────────────────────┬───────────────────────┬───────────────────┬───────────────┬──────────────┬────────────────┐
| series ref <8 byte> | mint <8 byte, uint64> | maxt <8 byte, uint64> | encoding <1 byte> | len <uvarint> | data <bytes> │ CRC32 <4 byte> │
└─────────────────────┴───────────────────────┴───────────────────────┴───────────────────┴───────────────┴──────────────┴────────────────┘

series ref我们在第二部分讨论过,是对Seires的引用,可以用来关联访问内存中的Seires。mintmaxt是chunk中可以找到的Sample数据的最小和最大时间戳。encodnig是用于压缩chunk的算法。len是从此处开始的字节数,而data是实际压缩的chunk的字节数据。

CRC32是上述chunk内容的校验和,用于验证数据的完整性。

Reading these chunks

对于所有的chunk,Head Block中都会存储它的mintmaxt以及引用。

引用占8个字节,前四个字节指明chunk所在的文件序列名,后四个字节指明该chunk在这个文件的offset(也就是seires ref所在字节)。如果chunk在00093文件,并且series ref在offset1234处,则该chunk的引用为(93 << 32 | 1234)

我们将mintmaxt存储在Head中,以便我们在筛选时无需查询磁盘。当我们必须要访问chunk时,我们使用引用来访问编码和chunk数据。

在代码中,该文件看起来像一个字节切片(每个切片对应一个文件),OS将内存中的切片映射到磁盘上,在索引处获得chunk数据。从磁盘进行memory-mapping是OS的特性,它仅将磁盘的一部分取到内存中,而非整个文件。

Replaying on startup

在第二部分中,我们讨论了WAL的重播,我们通过重播每个独立的Sample来重新构建压缩后的chunk。现在我们在磁盘上已经有了完整的压缩后的chunk,因此我们无需重新创建这些chunk,只需要重新创建那些没有满的chunk。现在有了这些来此磁盘的memory-mapped chunk,重播会发生如下动作。

在开始的时候,我们首先在迭代chunk_head目录下的所有chunks,并且构造一个map,结构是series ref -> [chunk引用的列表]

然后我们就按照第二部分的描述来进行WAL重播,少量调整如下:

  • 当我们遇到Seires时,在创建完Seires后,我们会在上述map中查找引用,如果存在memory-mapped chunk,那会进行关联;
  • 当遇到Sample时,如果该Sample相关联的Seires存在memory-mapped chunk,并且该Sample的时间戳在这些chunk的range内,则直接跳过该Sample。如果不在范围内,则将这个样本加入Head中;

Enhancements that this brings in

当我们可以通过内存型的chunk和WAL来存储数据是,这种增加复杂性的memory-mapped会给我们带来些什么呢?该特性我们在2020年添加,所以我们看看它带来了什么。

Memory savings

如果我们必须将chunk存储在内存中,它可能需要占用120到200字节(甚至更多,取决于样本的可压缩性)。而现在被替换为24字节(chunk引用、mintmaxt)。

虽然这听起来好像减少了80%-90%的内存,但实际情况有所不同,Head需要存储更多的东西,比如内存索引、所有符号(标签值)等,以及TSDB的其它部分。

真是的情况是,我们可以看到内存占用减少了15%-50%,这取决于采样速度和创建新Seires的速度。另一件需要注意的事情是,如果运行的一些查询需要大量使用这些chunk,那它们仍然需要加载到内存运行,所以这并不是峰值内存使用量的绝对减少。

Faster startup

WAL的重播是启动时最慢的部分,从磁盘解码WAL记录以及通过单个Sample来重建压缩chunk的过程是最慢的两个部分。而memory-mapped的迭代则相对较快。

我们无法避免对Record进行解码,因为我们需要进行检查。如你在上述描述中看到的,我们跳过了那些在memory-mapped chunk范围内的样本,这里我们避免了重新创建那些完整的压缩后的chunk,这被认为可以减少15%-30%的启动时间。

Garbage collection

内存中的垃圾回收发生在Head清空的期间,它只是丢弃了在清空时间T之前的chunk的引用,但是文件仍然存在于磁盘上。和WAL的segment一样,我们还需要定期删除旧的memory-mapped文件。


Code reference

tsdb/chunks/head_chunks.go包含写chunks到磁盘、通过引用访问chunk、清空、处理文件、迭代chunks的所有实现。

tsdb/head.go将其作为黑盒使用,用于memory-mapped。