exemplar · 三支柱之间那座桥

从一条指标曲线,一键跳到那一刻的某条真实 trace——exemplar 是可观测性里最巧妙的设计之一。这一章讲它怎么工作、为什么它「用聚合的便宜换到了个案的入口」,以及三支柱关联的完整图景。

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

exemplar · 三支柱之间那座桥

第 0 部分说过:“支柱之间的桥和支柱本身一样重要”。这一章讲最巧妙的那座桥——exemplar。

它解决什么

一个每天都在发生的场景:

你在 Grafana 上看到 P99 延迟在 10:23 有个尖峰。然后呢?

  • 指标只告诉你“有个尖峰” —— 它是聚合的,个案早被抹掉了。
  • 你想去看那一刻的 trace —— 但怎么找? 去 Jaeger 里按时间范围搜?那一分钟里有几万条 trace,哪条是造成尖峰的那条?
  • 而且它很可能根本没被采样 —— 头部采样下,那条慢的 99% 被丢了。

这个“从聚合跳到个案”的鸿沟,是可观测性最恼人的断层。 你有指标、有追踪,但它们之间没有桥——你只能靠时间戳手工对,效率极低。

exemplar:在指标里挂几个样例

exemplar 的思路极简:在记录指标时,顺便挂上几条“代表性样本”的 trace_id。

# 一个带 exemplar 的直方图桶([OpenMetrics 格式](/manual/observability/exposition-format))
http_request_duration_seconds_bucket{le="2.5"} 1027 # {trace_id="abc123"} 2.3 1719849600
                                                     └── exemplar ──┘  值   时间戳

含义:“这个桶(≤2.5s)里有 1027 个请求,其中有一个具体的请求,它的 trace_id 是 abc123,耗时 2.3s,发生在那个时刻。”

于是 Grafana 上,那条 P99 曲线的尖峰旁边会出现一个小点——点它,直接跳到那条 trace 的火焰图。

鸿沟被一座桥填平了。

为什么这个设计如此巧妙

它同时绕开了两个死结:

① 绕开了基数

你不能把 trace_id 放进标签——那是无限基数,会当场炸掉。但 exemplar 不是标签:它是挂在采样点上的附属数据,不参与 series 的身份。

于是:series 数一点没涨,但你有了跳转入口。 这是个漂亮的规避——它承认“trace_id 不能索引”,但保留了“trace_id 可以被携带”。

② 绕开了采样

更妙的是:exemplar 是从“真实发生过的请求”里挑的,而且实现通常会优先挑那些异常的(慢的、错的)。

结合采样位看:客户端库可以因为“这个请求要被记成 exemplar”而强制采样它——于是那条你最想看的慢 trace,一定会被存下来。

这就直接解决了上一章头部采样的致命缺陷:1% 随机采样会漏掉慢请求,但 exemplar 机制保证了“每个桶里至少有一个真实样本被完整保留”——尤其是最慢那个桶里的。

“用聚合的便宜,换到了个案的入口”——你没有存下所有 trace(那太贵),但你为每个指标的每个桶留了一条“证据”。这可能是可观测性里性价比最高的一个机制。

三支柱关联的完整图景

exemplar 只是桥之一。把所有桥放一起看:

图表加载中…

四座桥,方向不同:

桥怎么实现用途
① 指标 → 追踪exemplar看到尖峰,跳到那条慢 trace
② 追踪 → 日志日志里带 trace_id看到慢的那一跳,查它当时的日志
③ 日志 → 追踪同上,反向看到一条错误日志,查它属于哪个请求
④ 追踪 → 指标spanmetrics connector从 trace 自动生成 RED 指标

桥 ② ③ 的关键是:你的日志里必须有 trace_id。 这需要:

桥 ④ 值得单说:spanmetrics connector 从 traces 自动生成 RED 指标——你不用在应用里再埋一遍指标。这是第 6 部分说“connector 让管道成为图”的最有价值的实例。

注意它的代价:生成的指标基数取决于 span 的属性——如果你的 span name 里带了 ID(GET /orders/12345),生成的指标会当场基数爆炸。又一次,基数是万恶之源。

实战:怎么开

exemplar 需要三方配合,少一环就没有:

  1. 应用侧 —— SDK/客户端库要支持记录 exemplar(Prometheus 的各语言客户端库大多支持,但默认可能关着)。
  2. 暴露格式 —— 必须是 OpenMetrics(经典文本格式表达不了 exemplar)。Prometheus 抓取时要协商到 OpenMetrics。
  3. Prometheus 侧 —— 要开 --enable-feature=exemplar-storage(exemplar 是单独存的,有自己的存储和 API)。
  4. Grafana 侧 —— 数据源里配好“exemplar → 跳到哪个 trace 数据源”的映射。

最常见的失败:第 2 步。 你开了 exemplar,但抓取协商到的是经典文本格式 → exemplar 被静默丢弃 → Grafana 上啥也没有,还不报错。

另外注意 exemplar 是“少量样本” —— 每个 series 只留最近的几个,不是全部。它的定位是“给你一个入口”,不是“完整记录”。

小结与下一章

exemplar 填平了“从聚合跳到个案”的鸿沟——看到 P99 尖峰后,你以前只能按时间戳在几万条 trace 里手工捞,而且那条慢的很可能根本没被采样。做法极简:记录指标时顺便挂几条代表性样本的 trace_id。它同时绕开两个死结:①绕开基数——exemplar 不是标签、不参与 series 身份,所以 series 数一点没涨(承认 trace_id 不能索引,但保留它可以被携带);②绕开采样——实现会优先挑异常的样本并强制采样它,于是“最慢那个桶里一定有一条完整 trace”,直接解决了头部采样漏掉慢请求的致命缺陷。“用聚合的便宜换个案的入口”——性价比最高的机制之一。 三支柱有四座桥:[指标→追踪(exemplar)、追踪↔日志(trace_id,前提是结构化日志 + context 没断)、追踪→指标(spanmetrics connector 自动生成 RED,但 span name 带 ID 会基数爆炸)](/manual/observability/red-method)。实战上 exemplar 需要应用/OpenMetrics 格式/Prometheus feature/Grafana 四方配合,最常见的失败是格式没协商到 OpenMetrics → 静默丢弃、不报错。

三支柱的最后一根:日志。下一章收尾第 7 部分。👉 日志支柱。

参考资料

  • [1] OpenMetrics 规范:Exemplars;Prometheus 文档:Exemplars、--enable-feature=exemplar-storage。
  • [2] OTel Collector contrib:spanmetricsconnector;OTel 文档:Exemplars。
  • [3] 关联基数、采样、三大支柱。

本页目录