三大信号如果各自为战,可观测性就退化为”三个监控”。信号关联(Signal Correlation)是可观测性的灵魂——它让指标发现异常后直接跳转到追踪定位瓶颈,让追踪找到慢 Span 后直接查看关联日志,让日志发现错误后直接查看性能热点。
没有关联,排障需要你在 Grafana、Jaeger、Kibana 之间手动跳转和搜索;有关联,排障变成一条流畅的点击链路——从发现到定位到理解,几分钟内完成。
一、关联的本质
1.1 关联 ID 是桥梁
信号关联的核心是关联 ID——在多个信号中共享的身份标识符:
| 关联 ID | 源信号 | 目标信号 | 说明 |
|---|---|---|---|
trace_id | 指标(Exemplar) | 追踪 | 从指标跳转到慢请求追踪 |
trace_id | 日志 | 追踪 | 从日志跳转到请求追踪 |
span_id | 日志 | 追踪 Span | 从日志跳转到具体 Span |
profile_id | 追踪 Span | 性能分析 | 从 Span 跳转到火焰图 |
1.2 关联体验
没有关联的排障:
- 看到指标异常 → 手动复制时间范围
- 打开 Jaeger → 手动搜索服务名和时间范围
- 找到慢追踪 → 手动复制 TraceID
- 打开 Kibana → 手动搜索 TraceID
- 找到错误日志 → 手动分析
有关联的排障:
- 看到指标异常 → 点击 Exemplar → 自动跳转到追踪
- 在追踪中点击 Span → 自动查看关联日志
- 在日志中看到性能问题 → 自动查看火焰图
1.3 关联的层次模型
信号关联不是简单的”跳转链接”,而是一个分层体系:
| 层次 | 依赖 | 示例 |
|---|---|---|
| 身份关联 | TraceID 传播 | 日志中的 trace_id 匹配追踪中的 traceID |
| 上下文关联 | 标签一致性 | 指标中的 service=order 匹配追踪中的 service.name=order |
| 语义关联 | 命名约定统一 | 指标 http_server_request_duration_seconds 对应追踪 HTTP GET /api/orders |
二、Exemplar:指标直通追踪
2.1 什么是 Exemplar?
Exemplar 是 Prometheus 2.26+ 引入的特性,允许在 Histogram 桶中附加一个关联引用(通常是 TraceID):
# 普通 Histogram 桶http_request_duration_bucket{le="0.5"} 80
# 带 Exemplar 的 Histogram 桶http_request_duration_bucket{le="0.5"} 80 # {trace_id="abc123"} 0.45Exemplar 告诉你:“这个桶中有一个耗时 0.45s 的请求,它的 TraceID 是 abc123”。
2.2 Exemplar 的存储与查询机制
Exemplar 的实现比看起来更精巧。它不是给每个样本都附加 TraceID,而是每个 Histogram 桶只保留最近一个 Exemplar:
这个设计是有意为之的:如果每个样本都保留 Exemplar,存储量会翻倍以上。只保留最新的 Exemplar 是一个合理的折中——它让你总是能跳转到最近的慢请求追踪。
Exemplar 的采样策略因 SDK 而异。OTel Go SDK 默认在 Histogram 记录时附加当前 SpanContext;OTel Java SDK 则支持配置 Exemplar 采样策略(如只保留超过 P99 阈值的 Exemplar)。选择哪种策略取决于你的存储成本和关联需求。
2.3 Exemplar 的配置细节
Exemplar 在 Prometheus 侧也需要启用存储支持:
# Prometheus 启用 Exemplar 存储storage: tsdb: path: /prometheus # Exemplar 最大存储量,默认 0(不存储) # 建议设置为 100000(10 万条),约占 100MB 内存 max_exemplars: 100000
# 也可以通过命令行参数# --storage.tsdb.max-exemplars=100000| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
max_exemplars | 0(禁用) | 100000 | Exemplar 环形缓冲区大小 |
| 内存开销 | — | ~1KB/条 | 100000 条约 100MB |
| 淘汰策略 | — | FIFO | 超出 max_exemplars 后淘汰最旧的 |
2.4 OTel SDK 配置 Exemplar
// Go: OTel SDK 自动在 Histogram 中附加 Exemplar// 无需额外配置,OTel SDK 默认将当前 SpanContext 作为 Exemplarvar requestDuration metric.Float64Histogram
func handleRequest(w http.ResponseWriter, r *http.Request) { ctx, span := tracer.Start(r.Context(), "handleRequest") defer span.End()
start := time.Now() // 处理请求... duration := time.Since(start).Seconds()
// Record 会自动附加当前 SpanContext 作为 Exemplar requestDuration.Record(ctx, duration, metric.WithAttributes( semconv.HTTPMethodKey.String(r.Method), semconv.HTTPRouteKey.String("/api/v1/orders"), ), )}// Java: OTel SDK 配置 Exemplarotel: exemplar: # 只保留超过阈值的 Exemplar(减少存储开销) filter: type: trace_based # 采样率:只保留被采样的追踪的 Exemplar metrics: exporter: otlp histogram: # 使用显式桶边界,确保 Exemplar 落入正确的桶 explicit-buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]# Python: OTel SDK 自动附加 Exemplarfrom opentelemetry import metrics, tracefrom opentelemetry.sdk.metrics import MeterProvider
# 默认行为:Histogram 记录时自动附加当前 SpanContextmeter = metrics.get_meter("order-service")request_duration = meter.create_histogram( name="http.server.request.duration", unit="s", description="HTTP 请求延迟分布",)
def handle_request(request): tracer = trace.get_tracer("order-service") with tracer.start_as_current_span("handle_request") as span: # 处理请求... request_duration.record(0.45, {"method": "GET", "route": "/api/orders"}) # Exemplar 自动附加当前 span 的 trace_id2.5 Grafana 中的 Exemplar 体验
在 Grafana 面板中,Exemplar 以小圆点显示在时间序列上:
- 在 P99 延迟面板中,点击 Exemplar 小圆点
- 自动跳转到 Tempo,查询对应的 TraceID
- 在追踪视图中定位慢 Span
- 从 Span 跳转到关联日志
Exemplar 小圆点的颜色和大小也有含义——越大的点表示越慢的请求,颜色越红表示越接近 SLO 违规。这让你可以快速识别”最值得调查”的慢请求。
三、TraceID 关联:日志与追踪
3.1 注入 TraceID 到日志
// Go: 自动注入 TraceID 到 slogtype OTelHandler struct { inner slog.Handler}
func (h OTelHandler) Handle(ctx context.Context, r slog.Record) error { span := trace.SpanFromContext(ctx) if span.SpanContext().IsValid() { r.AddAttrs( slog.String("trace_id", span.SpanContext().TraceID().String()), slog.String("span_id", span.SpanContext().SpanID().String()), ) } return h.inner.Handle(ctx, r)}3.2 TraceID 注入的多语言实现
不同语言的日志框架对 TraceID 注入的支持方式不同。以下是四种主流语言的实现模式:
// Java: 使用 MDC (Mapped Diagnostic Context) 注入 TraceID// OTel Java Agent 自动将 trace_id/span_id 注入 MDC// logback-spring.xml 配置<configuration> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <!-- 自动从 MDC 提取 trace_id --> <includeMdcKeyName>trace_id</includeMdcKeyName> <includeMdcKeyName>span_id</includeMdcKeyName> <includeMdcKeyName>trace_flags</includeMdcKeyName> </encoder> </appender></configuration># Python: 使用 structlog + OTel 注入import structlogfrom opentelemetry import trace
def add_trace_id(logger, method_name, event_dict): """自定义 processor:注入 trace_id 和 span_id""" span = trace.get_current_span() if span.is_recording(): ctx = span.get_span_context() event_dict["trace_id"] = format(ctx.trace_id, "032x") event_dict["span_id"] = format(ctx.span_id, "016x") return event_dict
structlog.configure( processors=[ add_trace_id, # 注入 TraceID structlog.processors.JSONRenderer(), # JSON 输出 ],)// Node.js: 使用 OTel API 注入 TraceIDimport { trace, context } from '@opentelemetry/api';import pino from 'pino';
const logger = pino({ formatters: { log(object) { const span = trace.getSpan(context.active()); if (span) { const { traceId, spanId } = span.spanContext(); object.trace_id = traceId; object.span_id = spanId; } return object; }, },});| 语言 | 日志框架 | 注入方式 | 是否需要手动代码 |
|---|---|---|---|
| Go | slog/zap | 自定义 Handler | 是(~20 行) |
| Java | Logback/Log4j | MDC + OTel Agent | 否(Agent 自动注入) |
| Python | structlog | 自定义 Processor | 是(~10 行) |
| Node.js | Pino/Winston | 自定义 Formatter | 是(~10 行) |
| Rust | tracing | OTel Layer | 是(~15 行) |
Java 是唯一可以零代码注入 TraceID 的语言——OTel Java Agent 通过字节码增强自动将 trace_id 注入 MDC。其他语言都需要少量手动代码,但代码量通常不超过 20 行。建议将注入逻辑封装为项目内部的日志库,避免每个服务重复实现。
3.3 Grafana 中的日志-追踪关联
在 Grafana Explore 中,日志和追踪可以无缝关联:
- 在 Loki 查询日志:
{service="order-service"} | json | level="ERROR" - 日志行中显示 TraceID 链接
- 点击 TraceID → 自动跳转到 Tempo 查询追踪
- 在追踪视图中,点击 Span → 自动过滤该 Span 的日志
3.4 配置数据源关联
# Grafana 数据源关联配置apiVersion: 1datasources: - name: Tempo type: tempo uid: tempo jsonData: tracesToMetrics: datasourceUid: prometheus tags: - key: service value: service tracesToLogs: datasourceUid: loki filterByTraceID: true filterBySpanID: true tags: - key: service value: service serviceMap: datasourceUid: prometheus
- name: Loki type: loki uid: loki jsonData: derivedFields: - name: TraceID matcherRegex: '"trace_id":"(\w+)"' url: '$${__value.raw}' datasourceUid: tempo
- name: Prometheus type: prometheus uid: prometheus jsonData: exemplarTraceIdDestinations: - name: trace_id datasourceUid: tempo urlDisplayLabel: 'View Trace'3.5 数据源关联的完整拓扑
上面只展示了最基础的关联配置。在一个完整的可观测性平台中,数据源之间的关联形成一个网状拓扑:
| 关联方向 | 配置位置 | 关键字段 |
|---|---|---|
| Prometheus → Tempo | Prometheus exemplarTraceIdDestinations | datasourceUid: tempo |
| Tempo → Loki | Tempo tracesToLogs | filterByTraceID: true |
| Loki → Tempo | Loki derivedFields | matcherRegex + datasourceUid |
| Tempo → Prometheus | Tempo tracesToMetrics | datasourceUid: prometheus |
| Tempo → Pyroscope | Tempo tracesToProfiles | profileType: cpu |
四、ProfileID 关联:追踪与性能分析
4.1 从追踪跳转到火焰图
OTel 正在标准化 ProfileID 关联——在 Span 中附加 profile_id 属性,让追踪可以跳转到对应时间点的火焰图:
{ "span_id": "span1", "attributes": { "profile_id": "prof_abc123", "profile.type": "cpu" }}4.2 ProfileID 关联的配置
在 Grafana 中配置追踪到性能分析的关联:
# Tempo 数据源配置:追踪 → 性能分析- name: Tempo type: tempo uid: tempo jsonData: tracesToProfiles: datasourceUid: pyroscope # Profile 类型:cpu、memory、goroutine 等 profileType: process_cpu:cpu:nanoseconds:cpu:nanoseconds # 自定义标签映射 customQueryParameters: service: service.name # 时间范围偏移(Profile 采集间隔可能不同) spanEndTimeShift: '-30s' spanStartTimeShift: '30s'# Pyroscope 数据源配置- name: Pyroscope type: grafana-pyroscope-datasource uid: pyroscope url: http://pyroscope:40404.3 ProfileID 关联的技术原理
ProfileID 关联比 TraceID 关联更复杂,因为性能分析数据是周期性采集的(通常每 10-60 秒采集一次),而不是请求驱动的。这意味着:
| 对比维度 | TraceID 关联 | ProfileID 关联 |
|---|---|---|
| 数据产生 | 请求驱动(每个请求一条追踪) | 周期驱动(每 10-60s 一次采样) |
| 关联精度 | 精确到请求 | 近似到时间窗口 |
| 关联方式 | 精确匹配 TraceID | 时间范围 + 标签匹配 + ProfileID |
| 存储开销 | 追踪数据本身 | Profile 数据独立存储 |
因此,ProfileID 关联实际上有两种实现方式:
- 精确关联:在 Span 中附加
profile_id,指向该 Span 执行期间采集的 Profile。需要 OTel SDK 和 Pyroscope Agent 协同工作。 - 近似关联:根据 Span 的时间范围和标签(service name),在 Pyroscope 中查询对应时间窗口的 Profile。不需要 ProfileID,但精度较低。
精确关联(ProfileID)是 OTel Profiles 规范的一部分,目前处于 Beta 阶段。Grafana Tempo 2.4+ 和 Pyroscope 1.3+ 已支持此功能。如果你使用的是旧版本,可以先用近似关联过渡。
4.4 Grafana 中的关联体验
- 在追踪视图中,Span 旁边显示火焰图图标
- 点击图标 → 自动跳转到 Pyroscope
- 显示该 Span 执行期间的火焰图
- 可以看到 Span 耗时的函数级分解
五、端到端关联排障实战
5.1 场景:P99 延迟飙升
5.2 完整排障演练:订单服务延迟飙升
走一遍完整的端到端排障流程,从告警触发到根因定位:
背景:订单服务的 P99 延迟在 10 分钟内从 200ms 飙升到 2s,SLO 燃烧率告警触发。
第一步:指标面板确认异常
在 Grafana 的 SLO 面板中,看到可用性 SLO 的错误预算正在快速燃烧。切换到延迟面板,确认 P99 飙升但 P50 正常——典型的尾部延迟问题。
第二步:Exemplar 跳转到追踪
点击 P99 曲线上的红色 Exemplar 小圆点,Grafana 自动跳转到 Tempo,展示 TraceID 为 7f3b2a1c4d5e6f7890abcdef12345678 的追踪。
第三步:追踪定位瓶颈 Span
追踪瀑布图显示:
HTTP GET /api/v1/orders— 总耗时 1.95sorder-service.processOrder— 1.90sdb.query(SELECT * FROM orders)— 1.82s ← 瓶颈!cache.get(order:12345)— 0.003s
第四步:Span 关联日志
点击 db.query Span,Grafana 自动过滤该 Span 的日志:
{ "timestamp": "2026-06-27T02:15:32.456Z", "level": "WARN", "service": "order-service", "trace_id": "7f3b2a1c4d5e6f7890abcdef12345678", "span_id": "a1b2c3d4e5f67890", "message": "connection pool exhausted, waiting for available connection", "wait_time_ms": 1820, "pool_size": 50, "active_connections": 50, "pending_requests": 30}第五步:ProfileID 关联火焰图
点击 Span 旁的火焰图图标,跳转到 Pyroscope。火焰图显示 DB.waitConnection() 占了 90% 的 CPU 时间——这不是 CPU 热点,而是等待热点:线程在等待连接池分配连接。
第六步:确认根因并修复
根因:数据库连接池 max=50,但并发请求数已增长到 80。修复方案:将连接池 max 从 50 增加到 100。
// 修复前db.SetMaxOpenConns(50)
// 修复后db.SetMaxOpenConns(100)db.SetMaxIdleConns(25) // 同时增加空闲连接数db.SetConnMaxLifetime(5 * time.Minute) // 连接最大存活时间第七步:验证修复效果
部署修复后,P99 延迟在 2 分钟内恢复到 200ms 以下,SLO 燃烧率恢复正常。
5.3 关联配置清单
| 关联 | 配置项 | 验证方式 |
|---|---|---|
| 指标 → 追踪 | Prometheus Exemplar → Tempo | 点击 Exemplar 跳转 |
| 追踪 → 日志 | Tempo → Loki (TraceID) | 追踪中查看关联日志 |
| 日志 → 追踪 | Loki derivedFields → Tempo | 日志中点击 TraceID |
| 追踪 → 指标 | Tempo → Prometheus (RED) | 追踪中查看服务指标 |
| 追踪 → 性能 | Tempo → Pyroscope (ProfileID) | Span 中查看火焰图 |
六、关联的反模式
6.1 常见反模式
| 反模式 | 症状 | 解决方案 |
|---|---|---|
| 日志无 TraceID | 无法从日志跳转到追踪 | 自动注入 TraceID |
| Exemplar 未启用 | 指标无法跳转到追踪 | OTel SDK 默认启用 |
| 数据源未关联 | Grafana 中无法跳转 | 配置 derivedFields |
| 追踪链路断裂 | 跨服务追踪不完整 | 检查 W3C TraceContext 传播 |
| 时间不对齐 | 关联跳转后看不到数据 | 确保时钟同步 |
| 标签不一致 | 关联跳转后过滤条件不匹配 | 统一标签命名约定 |
| Exemplar 存储未启用 | Prometheus 不存储 Exemplar | 配置 max_exemplars |
6.2 反模式深入分析
反模式 1:日志有 TraceID 但格式不统一
这是最常见的”看起来有关联但实际不好用”的问题。不同服务的日志格式不一致:
// 服务 A:下划线格式{"trace_id": "abc123", "span_id": "def456"}
// 服务 B:驼峰格式{"traceId": "abc123", "spanId": "def456"}
// 服务 C:嵌套格式{"context": {"trace": {"id": "abc123"}, "span": {"id": "def456"}}}Grafana 的 derivedFields 只能匹配一种正则表达式。如果格式不统一,部分服务的日志就无法跳转。
解决方案:在 OTel Collector 中统一日志格式:
# OTel Collector: 统一日志中的 TraceID 格式processors: transform/log_format: error_mode: ignore log_statements: - context: log statements: # 将所有格式统一为 trace_id(下划线) - set(attributes["trace_id"], attributes["traceId"]) where attributes["traceId"] != nil - set(attributes["span_id"], attributes["spanId"]) where attributes["spanId"] != nil反模式 2:Exemplar 只在部分桶中出现
Exemplar 只在 Histogram 桶中出现,Counter 和 Gauge 不支持 Exemplar。如果你用 Gauge 记录延迟(而不是 Histogram),就无法使用 Exemplar 跳转。
// 错误:用 Gauge 记录延迟,无法使用 ExemplarlatencyGauge.Set(ctx, duration)
// 正确:用 Histogram 记录延迟,自动附加 ExemplarlatencyHistogram.Record(ctx, duration)反模式 3:追踪采样导致关联断裂
如果追踪采样率是 10%,那么 90% 的请求没有追踪数据。Exemplar 跳转到 Tempo 后可能找不到对应的 TraceID——因为那条追踪被采样丢弃了。
解决方案:使用尾部采样(Tail-Based Sampling),确保错误和慢请求的追踪总是被保留:
# OTel Collector: 尾部采样配置processors: tail_sampling: decision_wait: 10s num_traces: 100000 policies: # 错误追踪总是保留 - name: errors type: status_code status_code: status_codes: - ERROR # 慢请求追踪总是保留 - name: slow-requests type: latency latency: threshold_ms: 1000 # 其他追踪 10% 采样 - name: default type: probabilistic probabilistic: sampling_percentage: 10信号关联不是自动的——它需要你在架构层面设计。最关键的一步是在所有日志中注入 TraceID。如果日志里没有 TraceID,信号关联就无从谈起。此外,确保日志格式统一、追踪采样策略与 Exemplar 兼容,否则关联体验会大打折扣。
七、关联的验证与测试
7.1 自动化关联测试
手动验证关联链路很繁琐——你需要制造请求、查看指标、点击 Exemplar、检查跳转。更好的方式是编写自动化测试:
// Go: 自动化关联测试func TestTraceIDCorrelation(t *testing.T) { // 1. 发送一个带追踪的请求 ctx, span := tracer.Start(context.Background(), "test-request") traceID := span.SpanContext().TraceID().String()
// 2. 记录指标(带 Exemplar) requestDuration.Record(ctx, 0.5)
// 3. 记录日志(带 TraceID) slog.InfoContext(ctx, "test log", "key", "value")
// 4. 验证 Tempo 中有该 TraceID trace, err := tempoClient.GetTrace(traceID) require.NoError(t, err) require.NotNil(t, trace)
// 5. 验证 Loki 中有该 TraceID 的日志 logs, err := lokiClient.Query(`{service="test"} |= "` + traceID + `"`) require.NoError(t, err) require.NotEmpty(t, logs)
span.End()}7.2 关联健康检查面板
在 Grafana 中创建一个关联健康检查面板,持续监控关联链路的可用性:
| 检查项 | PromQL/LogQL | 健康标准 |
|---|---|---|
| Exemplar 存储使用率 | prometheus_tsdb_exemplar_storage_series | > 0 |
| 追踪采样率 | traces_sampling_rate | > 0(与配置一致) |
| 日志 TraceID 覆盖率 | count(logs_with_trace_id) / count(all_logs) | > 95% |
| 关联跳转成功率 | 自定义指标 | > 99% |
八、动手实践
8.1 配置 Grafana 数据源关联
# 更新 Grafana 数据源配置docker compose restart grafana
# 打开 Grafanaopen http://localhost:3000/explore8.2 验证关联链路
# 1. 在 Prometheus 面板中点击 Exemplar# 2. 验证跳转到 Tempo 追踪# 3. 在追踪中点击 Span# 4. 验证查看关联日志# 5. 在日志中点击 TraceID# 6. 验证跳转回追踪8.3 制造关联数据
# 发送一个带追踪的请求,验证完整的关联链路curl -X POST http://localhost:8080/api/v1/orders \ -H "Content-Type: application/json" \ -H "traceparent: 00-7f3b2a1c4d5e6f7890abcdef12345678-a1b2c3d4e5f67890-01" \ -d '{"item": "test-order"}'
# 在 Tempo 中查询该 TraceIDcurl http://localhost:3200/api/traces/7f3b2a1c4d5e6f7890abcdef12345678
# 在 Loki 中查询该 TraceID 的日志curl "http://localhost:3100/loki/api/v1/query_range?query={service=\"order-service\"}+%7C%3D+%227f3b2a1c4d5e6f7890abcdef12345678%22"8.4 验证清单
| 检查项 | 验证方式 | 预期结果 |
|---|---|---|
| Exemplar 可点击 | 指标面板中 | 显示小圆点 |
| 指标→追踪 | 点击 Exemplar | 跳转到 Tempo |
| 追踪→日志 | 点击 Span | 显示关联日志 |
| 日志→追踪 | 点击 TraceID | 跳转到 Tempo |
| 追踪→指标 | 服务详情 | 显示 RED 指标 |
| 追踪→性能 | Span 火焰图图标 | 跳转到 Pyroscope |
九、本章小结
上一章建立了可观测性存储后端的认知框架。 用了整章的篇幅拆解可观测性的灵魂——信号关联。
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| 关联 ID | TraceID、SpanID、ProfileID 是连接四大信号的桥梁。 | 关联 ID |
| Exemplar | 在 Histogram 桶中附加 TraceID,让指标直通追踪。每个桶只保留最新 Exemplar,是存储和关联的合理折中。 | Exemplar |
| 日志-追踪关联 | 在日志中注入 TraceID,在 Grafana 中配置 derivedFields。多语言实现方式不同,但代码量通常不超过 20 行。 | 日志-追踪关联 |
| 追踪-性能关联 | 在 Span 中附加 ProfileID,让追踪跳转到火焰图。精确关联需要 OTel Profiles 规范支持。 | 追踪-性能关联 |
| 端到端体验 | 从指标发现异常,到追踪定位瓶颈,到日志确认根因,到性能分析深入——一条流畅的点击链路。 | 端到端体验 |
| 反模式 | 日志无 TraceID、格式不统一、Exemplar 存储未启用、追踪采样导致关联断裂——这些是关联失败的最常见原因。 | 反模式 |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






