一条指标的一生 · 从埋点到那条曲线

用一个具体的指标走完全程——从代码里的一行埋点,经暴露、服务发现、抓取、存储、查询,到 Grafana 上那条曲线,以及告警怎么被触发。这张全景图就是整个专栏的路线索引。

作者 David更新于 第 3 篇(共 52 篇)

一条指标的一生 · 从埋点到那条曲线

理论讲完了(可观测性是什么、三大支柱),这一章用一个具体的指标走完全程。这条路上的每一站,后面都有专门的部分展开——所以这张图也是整个专栏的路线索引。

起点:代码里的一行埋点

一切始于你在业务代码里埋的一行。假设你想知道下单接口的请求数:

// 定义一个 counter,带两个标签
httpRequests := prometheus.NewCounterVec(
    prometheus.CounterOpts{Name: "http_requests_total", Help: "HTTP 请求总数"},
    []string{"method", "status"},   // 标签:未来提问的自由度
)
// 每处理完一个请求,加一
httpRequests.WithLabelValues("POST", "200").Inc()

注意 []string{"method", "status"} 这两个标签——它们就是上一章说的“未来提问的自由度”。 埋点时多带一个标签,日后就能多一个维度去切分(“只看 POST 的”、“只看 5xx 的”)。但每个标签也在成倍放大数据量——这就是基数问题的源头,第 8 部分会专门算这笔账。埋点这一刻的决定,决定了你日后能问什么问题、以及要付多少钱。

进程内,这个计数器只是内存里的一个数(准确说是一组数,每个标签组合一个)。它需要被暴露出去。

第 1 站:暴露与抓取(第 1 部分)

Prometheus 用的是 pull(拉) 模型——它主动来抓你,而不是你推给它:

  1. 暴露 —— 你的进程开一个 /metrics HTTP 端点,把当前所有指标以文本格式吐出来:
    # HELP http_requests_total HTTP 请求总数
    # TYPE http_requests_total counter
    http_requests_total{method="POST",status="200"} 1027
    http_requests_total{method="POST",status="500"} 3
  2. 服务发现 —— Prometheus 怎么知道去抓谁?靠 discovery/:从 Kubernetes API、Consul、DNS、静态配置等动态发现要抓的目标(pod 来来去去,不可能手工维护列表)。
  3. 抓取 —— scrape/ 里的抓取循环按固定间隔(如 15s)去 GET 每个目标的 /metrics,解析文本,得到一批采样点。

“pull 还是 push”是 Prometheus 最著名的设计取舍——第 1 部分会细讲。简单说,pull 让 Prometheus 掌握主动权:它知道该抓谁(靠服务发现)、抓不到就说明目标挂了(抓取本身就是健康检查)、且不会被失控的客户端打爆。代价是它得能网络可达每个目标——这在某些场景(如短命的批处理任务、NAT 后的客户端)行不通,于是有了 Pushgateway 这类补丁。

第 2 站:存进 TSDB(第 2 部分)

抓回来的采样点交给存储层(storage/ 的 Appender 接口),最终落进 TSDB(tsdb/)。这是整个 Prometheus 硬核程度数一数二的部分:

每个“指标名 + 标签组合”是一条独立的时间序列**(series),而一条 series 就是一串 (时间戳, 值)。TSDB 要解决两个难题:**

  • 怎么存得下 —— 每秒几百万个采样点,原始存法(8 字节时间戳 + 8 字节浮点)一天就是 TB 级。TSDB 用 Gorilla 压缩(delta-of-delta 存时间戳、XOR 存值),把平均每个采样点压到1~2 字节——压缩率超过 10 倍,这才让“指标全量长存”成为可能。
  • 怎么查得快 —— 你要问“所有 status="500" 的 series”,得在几百万条里瞬间找出来。TSDB 用倒排索引(标签 → series 列表),和搜索引擎一个思路。

数据先进内存里的 Head block(同时写 WAL 防丢),定期切成不可变的持久化 block 落盘,老 block 再被压实(compaction)合并。这个“内存写入 + 定期落盘 + 后台合并”的结构,和 LSM-tree 一脉相承。第 2 部分会打开 tsdb/docs/format/ 里的官方磁盘格式规范逐字节看。

第 3 站:查询(第 3 部分)

数据存下来,你得能问出答案。这是 PromQL(promql/)的活:

# 过去 5 分钟,每秒的 5xx 错误率
rate(http_requests_total{status=~"5.."}[5m])

PromQL 是一门为时序数据设计的语言:{status=~"5.."} 通过倒排索引选出 series,[5m] 取一段时间窗口,rate() 算增长速率。第 3 部分会读 promql/parser(怎么解析)和 promql/engine.go(怎么执行),并讲清 rate() 这个最常用也最容易用错的函数背后的坑。

第 4 站:分叉——告警与展示

查询的结果流向两个终点:

图表加载中…
  • 告警(第 4 部分) —— rules/ 里的规则引擎周期性地跑 PromQL,结果满足条件且持续够久就产生告警,交给 Alertmanager 去重、分组、静默,最后通知到人。
  • 展示(第 7 部分) —— Grafana 把 Prometheus 当数据源,把 PromQL 结果画成你屏幕上那条曲线。

至此,一条指标从代码里的一行走到了你的屏幕上。

另一条路:OpenTelemetry Collector(第 5、6 部分)

上面这条是 Prometheus 的经典路线。但现实里往往还有一条统一管道:

OpenTelemetry Collector 是一个厂商中立的中转站**:它接收(receiver)各种格式的指标 / 日志 / 追踪,做加工(processor:过滤、改标签、批量、采样),再导出(exporter)到各种后端。** 它解决的问题是:三大支柱、N 种客户端 SDK、M 个后端,如果两两对接就是 N×M 的噩梦。Collector 站在中间,把它变成 N+M——“管道”这一层被抽象出来独立演进。

这也是为什么本专栏要讲三个项目而不是一个:Prometheus 代表“指标怎么被存好查好”、Collector 代表“数据怎么被灵活地运输和加工”、Grafana 代表“数据怎么变成人能看懂的东西”——三者合起来才是完整的可观测性,缺一块都跑不通。

这张图就是路线图

回头看这条路,它正好对应本专栏的结构:

站点干什么部分
埋点 → 暴露 → 发现 → 抓取数据怎么被拿到第 1 部分
TSDB 存储怎么存得下、查得快第 2 部分
PromQL怎么问出答案第 3 部分
规则与告警什么时候叫醒你第 4 部分
OTel Collector数据怎么被统一运输第 5 部分
追踪与日志另两根支柱第 6 部分
Grafana怎么变成看得懂的第 7 部分
工程实践基数、成本、SLO、排障第 8 部分

小结与下一章

一条指标的一生:代码埋点(标签 = 提问自由度 + 成本)→ /metrics 暴露 → 服务发现(discovery/,去抓谁)→ 抓取(scrape/,pull 模型,抓取即健康检查)→ TSDB(tsdb/,Gorilla 压缩到 1~2 字节/点 + 倒排索引,Head+WAL→block→压实)→ PromQL(promql/)→ 分叉成 告警(rules/→Alertmanager)和 Grafana 展示。另有 OTel Collector 这条统一管道(把 N×M 对接变成 N+M)。

这条路上的三个主角各司其职。下一章正式介绍它们,以及为什么是这三个。👉 三个主角与分工。

参考资料

  • [1] Prometheus 源码 scrape/、tsdb/、promql/、discovery/、rules/、storage/(本章各站点)。
  • [2] Prometheus 官方文档 docs/getting_started.md、docs/index.md。
  • [3] OpenTelemetry Collector 源码 docs/internal-architecture.md。

本页目录