关于prometheus:promethues源码剖析head-block

15次阅读

共计 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…

正文完
 0