APM 与分布式追踪:OpenTelemetry 链路实践
网站从单体演进到"前端 + 网关 + 多个微服务 + 数据库 + 第三方 API"后,一次请求会穿越几十个组件。此时单个服务的监控指标只能告诉你"某个服务慢了",却说不清"慢在哪一环"。分布式追踪(Distributed Tracing)用一条贯穿所有服务的Trace还原请求的完整路径,而 **OpenTelemetry(OTel)**是这一领域的标准事实——一套厂商中立、可插拔的遥测框架。
核心概念:Trace、Span 与上下文
按 OpenTelemetry 官方文档,一条 Trace 由多个 Span 组成,每个 Span 代表"一段工作单元",包含名称、父 Span ID、起止时间戳、Span 上下文、属性(Attributes)、事件(Events)、链接(Links)与状态(Status)。同一 Trace 的所有 Span 共享同一个 trace_id,parent_id 字段则表达层级关系——这正是链路还原的关键。
三个需要理解的细节:
- Span 上下文(Span Context):每个 Span 上不可变的对象,含 Trace ID、Span ID、Trace Flags 与 Trace State,是随分布式上下文一起序列化并传播的部分。
- Span 属性:推荐用语义属性(semantic conventions)的约定命名,让元数据跨系统标准化。能影响采样的属性尽量在创建 Span 时就加上。
- Span 事件 vs 属性:时间戳有意义就放事件(如"页面变为可交互"这个时刻),没有就放属性。
用 SDK 手动创建一个 Span 很简单,以 Python 为例:
from opentelemetry import trace
tracer = trace.get_tracer("shop.checkout")
with tracer.start_as_current_span("charge_payment") as span:
span.set_attribute("order_id", "20260808-001")
span.set_attribute("payment.provider", "stripe")
span.add_event("retry_after_timeout", {"attempt": 2})
result = charge()
span.set_status(trace.StatusCode.OK if result else trace.StatusCode.ERROR)
几个要点:order_id 这类属性在创建 Span 时就设置,方便采样器按订单维度决定是否采样;add_event 用于记录"重试"这类有时间意义的时刻;根 Span 一般由框架或入口中间件自动创建,业务代码通常只需要在关键调用点加内层 Span,不必处处手动埋点。
上下文传播与 Span Kind
**上下文传播(Context Propagation)**是分布式追踪能成立的基石:没有它,各服务生成的 Span 无法关联成一条 Trace。实践中就是通过 HTTP 头(如 traceparent)把 Span 上下文从上游传给下游。OTel 还定义了 Span Kind:Client(发出同步远程调用)、Server(接收远程调用)、Internal(进程内操作)、Producer/Consumer(异步队列的生产与消费),这些类型帮助后端正确组装链路。
采样策略:成本与完整性的平衡
全量采样在高流量下成本不可承受,采样是关键设计。常见做法分两类:
- 头部采样(Head Sampling):在请求入口就决定是否采样,简单高效,但无法感知"这个请求是否异常"。
- 尾部采样(Tail Sampling):Span 结束后由 Collector 根据结果(如错误、慢请求)决定,能保证"所有错误链路都被保留",但需要缓冲与更复杂的部署。
务实建议:错误与慢请求 100% 保留,正常请求按 5%-10% 采样,再配合指标监控兜底——因为采样链路看不全的问题,恰恰要由指标层来发现。
Collector 的尾部采样可以用配置文件声明,按规则保留"错误"与"慢"两类链路:
processors:
tail_sampling:
policies:
- name: keep-errors
type: status_code
status_code: {status_codes: [ERROR]}
- name: keep-slow
type: latency
latency: {threshold_ms: 5000}
batch:
两种采样方式的取舍:
| 维度 | 头部采样 | 尾部采样 |
|---|---|---|
| 决策时机 | 请求入口 | Span 结束后 |
| 能否感知结果 | 不能 | 能 |
| 额外成本 | 几乎为零 | 需缓冲与复杂度 |
| 典型场景 | 高流量、成本敏感 | 需保证错误链路完整 |
三大信号联动:指标、日志与链路
追踪不是孤立的,它要和指标、日志联动才有完整的排查闭环:指标负责"发现有问题"(错误率、延迟上升),日志负责"看细节"(具体错误堆栈),链路负责"定位环节"(慢在哪一个服务、哪一次调用)。OTel 的上下文传播让这三种信号天然关联——日志里带上 trace_id,就能从一条日志直接跳到对应链路;指标里带上服务维度,就能从图表下钻到具体调用。排查慢请求的标准流程是:先在指标面板确认异常时段,再用链路找到瓶颈服务,最后用该服务的日志定位根因。
一次慢请求的定位
某商城活动页在大促期间 P95 延迟从 800ms 涨到 3.2 秒。指标面板确认异常时段后,工程师打开一条采样到的慢链路,发现时间几乎都花在一个叫 inventory.check_stock 的内部 Span 上,而不是团队猜测的支付网关。顺着这条链路,他在日志里用 trace_id 找到那次调用的具体 SQL,定位到一条没有走索引的库存查询——加索引后 P95 回到 900ms。
这个案例里三个信号各司其职:指标发现异常,链路定位环节,日志还原细节。如果没有链路,团队很可能在错误的组件上排查几个小时。
参考:OpenTelemetry Traces 概念文档 https://opentelemetry.io/docs/concepts/signals/traces/;尾部采样配置 https://opentelemetry.io/docs/collector/configuration/#tail-sampling-processor
16IDC 观察
对大多数独立站,完整接入 OTel 可能过重,但至少值得做两件事:一是给关键服务接入追踪 SDK,让"慢请求到底慢在哪"可回答;二是用 OTel Collector 作为统一入口,把指标、日志、链路三种信号一起转出,避免被某个厂商锁定。日志与链路的关联可参考 日志聚合与查询,前端侧的真实用户链路可结合 RUM 监控,错误追踪的轻量替代见 Sentry 错误监控。基础设施层可配合 Prometheus + Grafana。更多内容请查看 监控报警分类。