promethues源码剖析:head block

什么是Head block?

v2.19之前,最近2hour的指标数据存储在memory。
v2.19引入Head block,最近的指标数据存储在memory,当head block满时,将数据存储到disk并通过mmap引用它。
Head block由若干个chunk组成,head chunk是memChunk,接收时序写入。

写入时序数据时,当写入head chunk和wal后,就返回写入成功。

image.png

什么是mmap?

普通文件的读写:

  • 文件先读入kernal space;
  • 文件内容被copy值user space;
  • 用户操作文件内容;

image.png

mmap方式下文件的读写:

  • 文件被map到kernal space后,用户空间就可以读写;
  • 相比普通文件读写,减少了一次系统调用和一次文件copy;
  • 在多进程以只读方式共享同一个文件的场景下,将节省大量的内存;

image.png

Head block的生命周期

1)初始状态

时序数据被写入head chunk和wal后,返回写入成功。

image.png

2)head chunk被写满

headChunk对每个series,保存最近的120个点的数据;

const samplesPerChunk = 120

若scrape interval=15s的话,headChunk会存储30min的指标数据;
head chunk被写满后,生成新的head chunk接受指标写入,如下图所示:

image.png

同时,原head chunk被flush到disk,并mmap引用它:

image.png

3)mmap的chunks满

mmap的chunks达到chunkRange(2hour)的3/2时,如下图所示:

image.png

mmap中chunkRange(2hour)的数据将被持久化到block,同时生成checkpoint & 清理wal日志。

image.png

Head block的源码分析

image.png

每个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中提到:
image.png

可以看出,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...

38 声望
19 粉丝
0 条评论
推荐阅读
promethus删除value=""的label
一.概述prometheus拉取exporter中的指标进行解析时,对于labels,若label value="",则会将该label去掉;也就是说,对于label value="",不会存储到tsdb,通过prom API也查询不到该label。二.源码分析1.原理scape...

a朋阅读 123

Prometheus的使用
在Prometheus的架构设计中,Prometheus Server 并不直接服务监控特定的目标,其主要任务负责数据的收集,存储并且对外提供数据查询支持。因此为了能够能够监控到某些东西,如主机的CPU使用率,我们需要使用到Expo...

代码的路2阅读 365

自定义数据采集export到prometheus使用 Flask实现
如图 想要取到 url get请求的值,使用prometheus blackbox 无法获取,所以考虑使用flask自定义exporter 获取

台湾省委书记阅读 931

封面图
国产 ETL工具 ETL产品 数据交换系统 ETL工具选型 需要考虑哪些因素
多数软件又为什么逐渐国产化? 长期以来,国内数据技术都由海外厂商主导。然而随着国际竞争环境日益激烈,争端所引发的技术卡脖子事件频发,面对特殊的大环境,全球化带来的科技创新共同体背后,国家IT产业实现自...

weigeonlyyou阅读 884

封面图
prometheus pushgateway使用及源码分析
一.Pushgateway是什么pushgatway是prometheus社区推出的一个推送指标的组件,主要应用在:短生命周期(short-lived)或者批任务(batch jobs)的资源/作业的指标;prometheus无法拉取到(网络原因)的target的指标;作...

a朋阅读 763

开源signoz实现可观测性的分析
Signoz是一个开源的APM(Application Performance Management),它是应用可观测性的一个实践,使用OpenTelemetry协议,将traces/metrics/log融合在一起。

a朋阅读 731

如何使用 Blackbox Exporter 监控 URL?
监控域名和 URL 是可观察性的一个重要方面,主要用于诊断可用性问题。接下来会详细介绍如何使用 Blackbox Exporter 和 Prometheus 在 Kubernetes 中实现 URL 监控。

东风微鸣云原生阅读 673

38 声望
19 粉丝
宣传栏