prometheus代码走读

背景

prometheus 还是很牛逼的,作为云原生监控系统的事实标准,值得一读。

生态中对应的还有 AlertManager、pushgateway、operator,之前已经走读过 alertManager 的代码,详见 alertmanager等监控项目源码走读, 这里,走读一下 pushgateway 和 prometheus 的代码。

代码走读

pushgateway

  • main.go : 解析参数,启动 server。 http server 用的 promethues 包中的,这在 alertManager 中是一样的。
  • handler : 实现各种接口处理, 用闭包的方式,避免了一些全局变量的使用,例如 logger。
    • 支持 protobuf 的 encoding 方式,model 在 prometheus 的 client 中定义的 MetricFamily
    • 使用 parser.TextToMetricFamilies 解析 prometheus 的值
    • status.go 中使用了 tempate,并注入了 func,这种方式在一些简单的场景下很好用,毕竟不是所有项目都适合上一套前端框架。
  • storage : 存储的实现,用的 disk 做存储,存储格式直接用的 gob,在直接存储对象上,非常方便。
  • asset : 将 ui 打包到二进制中。

这个项目的功能和结构虽然都非常简单,但却有很多值得参考的地方,尤其是 使用 gob 直接存对象的方式、直接在 http 接口中使用 protobuf 做解析、将 asset 直接打包到 二进制中、简单场景直接使用 template……

prometheus

  • cmd : 入口文件,处理 config bind、validate、init,启动服务等。 promtool 提供了 cli 和 prometheus 交互的方式。
  • discovery : 抓取的 targets 管理
    • targetGroup 代表一组 target endpoint,而在 scrape.target 中的 Target 则是一个具体的 endpoint。这里可能比较难理解 target 的 url 从哪里来,在 scrape.target.Target.URL() 中可以看到,host、metrics_path 都在 label 中,是特定的 label。
    • manager.go : manager 管理接收到的新的 endpoints (map[poolKey]map[string]*targetgroup.Group),在 startProvider 中对接管道。 provider 是实际的 metrics 的提供者,他们关心 endpoints 的变化。discovery 是发现变化的,例如 file、k8s、zk、http 等。
    • registry.go : 使用注册方式,由 config 生成实际的 discovery,然后注册到中心的 hub 中。 类似于 builder 的方式,生成的 discovery 通过 Run() 方法启动。 这种做法很通用,在 grpc 的各种包里也有同样的操作。
    • kubernetes.go
      • 使用 cache informer 可以减轻对 k8s api server 的压力,这里的代码参考有价值
      • 使用 workqueue,把接收到的消息异步处理,避免 event 处理的时序错误。
    • xds.go : 说是 xds,实际上是 kuma,因为 kuma 是基于 envoy 的,而 xds 则是 envoy 的 discoverys。 这块儿可以进一步看下 envoy 的东西。
    • 在类似于 http、dns、docker 等方式中,因为没有监听的能力,因此抽象了一个 refresh 的 discovery wrapper。
  • model : 在业务中使用的模型,包括处理方法。 核心模型定义在 prompb 中。
    • rulefmt : 告警规则的模型
    • textparse : 有三种格式,text 格式、proto buffer、openmetrics。结合 go_client 可以拿到 metrics_family。 text 格式的代码是一个不错的词法解析器的参考,另外的参考可以看 go-zero 对 proto 文件的解析。 (当然 promql 也是)
    • histogram : 一个 histogram 的实现。
  • prompb : 核心模型定义
  • notifier
    • 对 alertmanager 的管理,也有 discovery 的能力,具体在 discovery.legacymanager 中。
    • 逻辑很简单,Send() 方法吧所有 alerts 存到 queue 中,然后触发 sendall() 。
    • 接受来自 rule 包的 alerts。
  • plugins : 目前是注册 discovery 的。
  • rules
    • 告警规则管理,连接 promql 和 alertmanager。
    • 分组 (Group),每个组有一系列具体的监控规则 (rule),使用 interval 做周期控制,使用 promql 做判断。
    • recording.go : 具体的判断 rule (expr)
    • alerting.go : alert 的具体状态
  • scrape
    • 抓取 metrics 的具体实现。分 scrape pool 进行抓取,具体的控制过程在 scrapeLoop.scrapeAndReport 中。
    • 用了 buffer pool 做 bytes 复用。毕竟发起的请求数量很多,可以减少 gc 压力。 具体的 pool 实现在 util.pool 中,可以参考下,这个 pool 做了分 bucket。
    • 用了 label cache 来保证同一个指标的 label 可以复用而不用重新初始化。
    • 抓取到数据后的具体的处理逻辑在 scrape.scrapeLoop.append 中,主要是解析指标,并且给 appender 发送数据, appender 是从 scrape.Manager 中传进来的,具体实现在 storage.fanout 中。
  • util : 一些常用的方法函数, treecache 用 zk 作为具体实现比较有意思。
  • web : 提供了 rest api 和 ui。
    • alertmanager 和 pushgateway 都用了把静态资源打包的做法 (http.FS),但 prometheus 却没有这么做,奇怪。 (更新,还是有的,只是和之前的代码写法有些不一样,用的是 go 的 //go:embed 注释)
    • 对互联网后端开发而言,很多时候把写 api 作为工作的主体了,但实际上,web 的可能是最简单的部分……

最重要的部分放在最后~~

  • promql : parse 和 query ,具体没怎么看细节。
  • storage : tsdb 的接口层,可以对接 remote ,也可以直接使用本地 tsdb。
  • tsdb
    • db.go : 启动 tsdb 的入口
    • head.go : 插入数据的入口,series 的内存结构等。 是理解数据写入过程的最重要的文件。实际上,head 数据可以看做是 block 在内存中的展开状态,head 中的 chunk 写到一定阶段后就会由 compact 变成 block 存到磁盘中。
    • wal.go : 任何的数据操作,都会先写到 log 中,用于保证数据的安全,调用方为 head.Commit。使用 wal 和 checkpoint 是内存数据库非常通用的方法。 checkpoint 的逻辑 和 日志监听的逻辑 在 wlog 中。
    • record : 写入 wal 的具体事件的定义。
    • compact.go : 处理把数据写入 文件系统 成为一个 block 的过程,包括两类: 把内存中的 head 写入 block;把多个 block 合并,实际调用 index.Writer 和 chunks.Writer 等做实际写入操作。
    • block.go : 数据块的定义和操作,管理 读取、写入 block。一个数据块就是 一段时间内的所有数据的集合,包括 采集的指标值 和 指标本身的信息。 是数据最终的存在形态,数据通路是 db => head => chunk => block 。
    • chunks 和 chunkenc : chunk 的定义和操作。 chunkenc 中包含了几种 chunk 的序列化方法。
    • fileutil : 一些 file 的操作封装
      • mmap : 文件内存映射。是读取 chunk 的方式,保证了低内存占用。
    • index
      • 是理解 持久化查询 最好的方式,分为 符号表 (用于压缩空间)、序列表(用于关联 chunks)、label表和posting表 (用于查询及关联 label 和 name) 以及 TOC (用于定位上述几张表)
    • 有几个重点:
      • 理解 series 的概念: 特定的 label 集合。 (series => labels.Labels)
      • 理解 series 在内存中的状态 (stripeSeries => memSeries => headChunk => mmapchunk)
      • 理解 block 的生成流程: series.headchunk => mmapchunk => compact/index/meta => block
      • 理解一个数据写入的流程: labels+val => series + val => chunk

设计上的想法:

  • 对于稍微复杂点的项目,一定会走上分层的道路,分层之后有逐渐会形成以 接口 为传递方式的调用 (而不是实例,否则要写 n 多类似的方法来兼容不同的type)。 接口化之后的好处是 单层内代码更简洁了;坏处是 想要跨层走一次流程会很懵,因为不知道具体的实现方是谁 (一般分层后每层都可能实现相同的方法)。

一些非常好的参考资料

一些思考

你想要什么?

  • 数据库具体究竟是怎么工作的?
    • wal 的具体实现
    • block 的具体实现
  • 为什么我们现在脱离了数据库就没法写代码了一样?
  • 一个这种级别的项目会花多少精力?
    • prometheus 的发起人开始也是自己就发布了版本,而且还做了好多其他项目,这些牛逼的人为什么牛逼?
  • 项目成功的要素有哪些?
    • 能力、生态
  • 如何让读源码的收益最大化?
    • 通过: 阅读代码
      • 知道各自的职能是什么
      • 知道各自是怎么实现的
      • 知道相互是怎么连接的
    • 得到: 数据结构 + 过程演变
    • 最好能: 临摹代码
    • 如何把这件事做好?
      • 先可以大量实践一下,找到一些好的形态
    • 如何像一台机器一样阅读源码?
      • 主线流程跑一次
      • 用 debug
  • LSM tree 和 其他数据库存储模型

TODO


We may encounter many defeats, but we must not be defeated.
Maya Angelou