共计 3310 个字符,预计需要花费 9 分钟才能阅读完成。
什么是 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 {
...
ref uint64
lset labels.Labels
...
headChunk *memChunk
}
type memChunk struct {
chunk chunkenc.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 即可;
参考:
1.https://ganeshvernekar.com/bl…
2.https://ganeshvernekar.com/bl…