什么是Head block?
v2.19之前,最近2hour的指标数据存储在memory。
v2.19引入Head block,最近的指标数据存储在memory,当head block满时,将数据存储到disk并通过mmap引用它。
Head block由若干个chunk组成,head chunk是memChunk,接收时序写入。
写入时序数据时,当写入head chunk和wal后,就返回写入成功。
文章图片
什么是mmap?
普通文件的读写:
- 文件先读入kernal space;
- 文件内容被copy值user space;
- 用户操作文件内容;
文章图片
mmap方式下文件的读写:
- 文件被map到kernal space后,用户空间就可以读写;
- 相比普通文件读写,减少了一次系统调用和一次文件copy;
- 在多进程以只读方式共享同一个文件的场景下,将节省大量的内存;
文章图片
Head block的生命周期 1)初始状态
时序数据被写入head chunk和wal后,返回写入成功。
文章图片
2)head chunk被写满
headChunk对每个series,保存最近的120个点的数据;
const samplesPerChunk = 120
若scrape interval=15s的话,headChunk会存储30min的指标数据;
head chunk被写满后,生成新的head chunk接受指标写入,如下图所示:
文章图片
同时,原head chunk被flush到disk,并mmap引用它:
文章图片
3)mmap的chunks满
mmap的chunks达到chunkRange(2hour)的3/2时,如下图所示:
文章图片
mmap中chunkRange(2hour)的数据将被持久化到block,同时生成checkpoint & 清理wal日志。
文章图片
Head block的源码分析
文章图片
每个memSeries结构,包含一个headChunk,其保存1个series在mem中的数据:
// prometheus/tsdb/head.go
// memSeries is the in-memory representation of a series.
type memSeries struct {
...
refuint64
lsetlabels.Labels
...
headChunk*memChunk
}type memChunk struct {
chunkchunkenc.Chunk
minTime, maxTime int64
}
向memSeries添加指标数据:
// prometheus/tsdb/head.go
// append adds the sample (t, v) to the series.
func (s *memSeries) append(t int64, v float64, appendID uint64, chunkDiskMapper *chunks.ChunkDiskMapper) (sampleInOrder, chunkCreated bool) {
// 1个chunk最多120个sample
const samplesPerChunk = 120
numSamples := c.chunk.NumSamples()
// If we reach 25% of a chunk's desired sample count, set a definitive time
// at which to start the next chunk.
// 到1/4时,重新计算nextAt(120点以后的时间)
if numSamples == samplesPerChunk/4 {
s.nextAt = computeChunkEndTime(c.minTime, c.maxTime, s.nextAt)
}
// 到达时间,创建新的headChunk
if t >= s.nextAt {
c = s.cutNewHeadChunk(t, chunkDiskMapper)
chunkCreated = true
}
// 向headChunk插入t/v数据
s.app.Append(t, v)
......
}
当达到nextAt后,写入老的headChunk数据,并新建headChunk:
// prometheus/tsdb/head.go
func (s *memSeries) cutNewHeadChunk(mint int64, chunkDiskMapper *chunks.ChunkDiskMapper) *memChunk {
// 写入mmap
s.mmapCurrentHeadChunk(chunkDiskMapper)// 新建headChunk
s.headChunk = &memChunk{
chunk:chunkenc.NewXORChunk(),
minTime: mint,
maxTime: math.MinInt64,
}
s.nextAt = rangeForTimestamp(mint, s.chunkRange)
app, err := s.headChunk.chunk.Appender()
s.app = app
return s.headChunk
}
将headChunk写入mmap:
// prometheus/tsdb/head.go
func (s *memSeries) mmapCurrentHeadChunk(chunkDiskMapper *chunks.ChunkDiskMapper) {
chunkRef, err := chunkDiskMapper.WriteChunk(s.ref, s.headChunk.minTime, s.headChunk.maxTime, s.headChunk.chunk)
s.mmappedChunks = append(s.mmappedChunks, &mmappedChunk{
ref:chunkRef,
numSamples: uint16(s.headChunk.chunk.NumSamples()),
minTime:s.headChunk.minTime,
maxTime:s.headChunk.maxTime,
})
}// prometheus/tsdb/chunks/head_chunks.go
// WriteChunk writes the chunk to the disk.
func (cdm *ChunkDiskMapper) WriteChunk(seriesRef uint64, mint, maxt int64, chk chunkenc.Chunk) (chkRef uint64, err error) {
....
// 写入header信息
if err := cdm.writeAndAppendToCRC32(cdm.byteBuf[:bytesWritten]);
err != nil {
return 0, err
}
// 写入chunk数据
if err := cdm.writeAndAppendToCRC32(chk.Bytes());
err != nil {
return 0, err
}
if err := cdm.writeCRC32();
err != nil {
return 0, err
}
// writeBufferSize=4M
// 超过4M,则直接flush到disk
if len(chk.Bytes())+MaxHeadChunkMetaSize >= writeBufferSize {
if err := cdm.flushBuffer();
err != nil {
return 0, err
}
}return chkRef, nil
}
Head block的好处 prometheus在2.19.0的release note中提到:
文章图片
可以看出,Head block带来的好处:
- 减少了用户态内存的占用:
- 之前最近2hour的chunks存在memory中;
- 引入head block后,chunks通过mmap引用,不占用用户态内存;
- 提升了prometheus实例重启的数据恢复速度:
- 若没有head block,恢复时需要replay所有的wal到memory;
- 有了head block后,恢复时仅需读入mmap chunks,然后replay没有mmap的部分wal即可;
2.https://ganeshvernekar.com/bl...
推荐阅读
- prometheus源码分析(rules模块)
- prometheus源码分析(scrape模块)
- prometheus源码分析(t/v数据的压缩、写入和读取)
- prometheus源码分析(index倒排索引)
- k8s|k8s hpa计算
- 我的大屏监控布局资料
- PromQL之label_replace/label_join