西西软件园多重安全检测下载网站、值得信赖的软件下载站!
软件
软件
文章
搜索

首页 → 业内动态 → 业内资讯 → OpenTelemetry接入清单:5个必配项,第4项没配链路就断

OpenTelemetry接入清单:5个必配项,第4项没配链路就断

相关软件相关文章发表评论 来源:本站整理时间:2026/11/3 20:35:48字体大小:A-A+

  • 类型:编程工具大小:56.0M语言:中文 评分:10.0
  • 标签:
立即下载

去年年底我们把一个单体订单系统拆成了 9 个微服务,拆完之后服务之间靠 HTTP 调用串起来。上线第二天就出事:一个用户下单要等 40 秒,报警响起来的时候,我打开监控面板,看见的全是绿色的 CPU 和内存曲线,每一个服务单独看都"没问题"。最后是挨个服务翻日志、拼时间戳,花了整整一个下午才拼出一条完整调用链而那还是运气好,服务数量少。

那之后我花了两个月把 OpenTelemetry 的链路追踪接进了全部 9 个服务。回头看,接入本身并不复杂,代码量加起来不到 200 行;真正踩坑的是那些"看起来配了、其实没生效"的地方。这篇就把这套接入里必须配对的 5 个东西按踩坑顺序写下来,尤其第 4 项,我漏了它整整两周,链路断在第三跳,后端同事查了几天日志都找不到原因。

如果你也在做微服务拆分,或者正在给已有的服务补可观测性,准备先拿一台机器把 Collector 和存储后端跑起来,可以先看看这几家的配置和技术说明:reliablesite(服务器与主机,可定制工作负载);如果要按区域多节点部署采集器,zlidc(全球云服务器,多城市节点) 的多地线路也值得一并比较。

机柜旁部署链路追踪采集器的运维现场

先说清楚:链路追踪到底在记录什么

先对齐一下概念,不然后面容易看糊涂。链路追踪做的事很简单:给一次请求生成一个全局唯一的 ID(traceId),然后把这次请求经过的每一段都记成一个 span,span 之间有父子关系,最后所有 span 汇总成一条完整的"瀑布图"。你看到的界面上那条从左到右的横条,就是这条链路。

关键点在于 span 之间靠两个东西关联:一是进程内通过 Context 对象(Java 里是 ThreadLocal,Go 里是 context.Context)往下传;二是跨进程必须靠 HTTP 请求头传出去,标准头名是 traceparent 和 tracestate。这两套机制同时存在,缺一不可而我只配了第一套,这就是后面要说的那个大坑。

OpenTelemetry 本身不存数据。它只做三件事:产生 span、把 span 通过 OTLP 协议发给 Collector、Collector 再转发给后端(Jaeger、Tempo、或者云厂商的 APM)。所以接 OpenTelemetry 等于同时接了三层东西,很多"接了但没数据"的问题,其实出在后两层,跟你的业务代码没关系。

一条请求在多个微服务间被逐段点亮的调用链示意图

必配项 1:后端服务要真的创建 span

最基础也最容易忽略的一层。如果你只是引入了 OpenTelemetry 的 Java agent,把依赖加进 pom.xml,然后发现界面上什么都没有,八成是业务代码里压根没有埋点框架会自动为 Spring MVC、Nginx、gRPC 这些常见框架生成 span,但你自己写的业务方法调用,得手动开一个 span。

我用的写法比较朴素,就是把耗时统计包了一层:

Span span = tracer.spanBuilder("order.submit").startSpan();
try (Scope scope = span.makeCurrent()) {
  // 原来的业务逻辑
} catch (Exception e) {
  span.recordException(e);
  throw e;
} finally {
  span.end();
}

这里有个小坑:span 一定要在 finally 里 end()。我早期版本里忘了这行,异常路径下 span 永远不结束,Collector 那边就看到一堆"孤儿 span",后端存储压力直接翻倍。后来我在代码评审里加了一条检查:所有 spanBuilder 调用必须紧跟 try-with-resources 的 Scope。

追踪链路断成两截的断链示意图

必配项 2:OTLP 导出地址要指向 Collector 而不是后端

很多人第一次接会填错这个地址。正确的链路是:应用 → Collector → 后端。应用通过 OTLP gRPC 或者 OTLP HTTP 把 span 发给 Collector,Collector 再按自己的 pipeline 配置转发给 Tempo 或者 Jaeger。

如果你把应用的 OTLP endpoint 直接写成后端地址,短期看是"能跑通"的,但问题会在你给 Collector 加采样、限流、脱敏的时候一起冒出来。我们当时的 Collector pipeline 里配了批处理和重试,直接打到后端就意味着这些能力全都用不上,几万个 span 打过去直接把后端打挂过一次。

我们的 Collector 配置大致是这样一个结构:

receivers:
  otlp:
    protocols:
      grpc:
      http:
exporters:
  otlp/tempo:
    endpoint: tempo:4317
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, memory_limiter]
      exporters: [otlp/tempo]

memory_limiter 我强烈建议开着。默认的 memory_limiter 阈值是 JVM 堆内存的某个比例,如果 Collector 所在机器内存吃紧,它会主动拒绝接收而不是撑爆自己;没开这个处理器的时候,我们那台 4G 内存的小机器被 Collector 直接 OOM 过一次,追踪数据跟着一起断。

必配项 3:采样率要在应用和 Collector 两边想清楚

全量采集听起来最美好,但生产环境基本不可行。一个日活百万的接口,全量采集意味着每天几亿个 span,存储成本和 Collector 压力都会失控。

我们用的策略是两层采样:应用侧按 traceId 哈希做头采样,比例 1%;Collector 侧再做一层尾部采样,把慢请求(超过 800 毫秒)无条件保留。这样日常流量只留 1%,但真正有问题的慢请求一条不丢。

踩坑点在于:应用侧采样必须基于 traceId 的哈希,而不是随机数。随机采样的话,同一个请求经过三个服务时,三个服务各自独立决定采不采,结果就是链路被切成三段碎片,在界面上看是三条互不相干的短链路。用哈希能保证同一条 trace 的所有服务做出相同决定。

运维工程师对比多个服务耗时面板排查慢接口

必配项 4:跨服务传递 traceparent,这一条漏了链路必断

这是我说的那个漏了两周的坑。上面三项都配好之后,界面上能看到链路,但每条链路只有一跳:网关 → 订单服务,下游的库存服务、支付服务完全不在图里。当时我们查日志、查代码、查配置,整整两天没进展。

根因是上下文传播没做。Java agent 默认会自动给出站的 HTTP 请求注入 traceparent 头,但如果你是自己封装的 HTTP 客户端、或者走了某个内部 RPC 框架的自定义适配层,agent 就插不上手了。表现出来的现象特别有迷惑性:同一个服务的入站 span 有 traceId,出站的 span 却是一个全新的 traceId,两边对不上。

排查这个问题有个很好用的土办法:在每个服务的拦截器里把出站请求头全部打出来,肉眼比一下 traceId 前 32 位是不是一致。我们的最后定位过程就是靠这个,发现库存服务调库存仓库时,走的是自己封装的 HttpClient,头根本没带过去。

补法也简单:要么让 agent 能识别这个客户端(换成标准的 OkHttp 或者 RestTemplate 就自动好了),要么手动注入在拦截器里取当前 Context 的 span,把它的 traceparent 手工写进请求头。手动注入的代码大概是这样:

Span current = Span.current();
String traceparent = "00-" + current.getSpanContext().getTraceId() + "-" + current.getSpanContext().getSpanId() + "-01";
request.addHeader("traceparent", traceparent);

顺带提醒一句:traceparent 是敏感信息,跨公网传输时要注意别把内部 traceId 直接暴露给不受信任的调用方,很多安全扫描项就是专门查这个头的。

必配项 5:把 traceId 写进日志,两件事才能串起来

最后这一条不配的话,链路追踪和日志系统就是两套割裂的东西。当你在瀑布图里看到某个 span 慢了 3 秒,想去看那时候到底发生了什么日志,还得靠时间戳去猜。

做法是在日志输出里带上 traceId 和 spanId,这样在日志系统里按 traceId 一搜,整条链路相关的日志就都串出来了,排查效率完全不一样。我们的 logback 配置里加了 MDC 输出:

<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %X{trace_id:-} %X{span_id:-} %-5level %logger{36} - %msg%n</pattern>

还有个实践上的建议:给 traceId 加一个便捷入口。日志系统里一般都能把 traceId 做成可点击的链接,直接跳到链路追踪的后端界面。我们内部就在日志平台加了个小插件,鼠标悬停就能看这条链路的完整瀑布图,用起来之后基本没人再回手写 traceId 去搜了。

微服务可观测性运维现场与服务拓扑火焰图

费用和资源账,顺手算给你看

有人会问,接了追踪是不是要多花很多钱。实际算下来大头不是存储,是采集器本身。9 个服务,每个服务一台 2 核 2G 的小机器跑 agent 绰绰有余,Collector 单独一台 4 核 8G 就够撑住我们日均千万级 span 的量(1% 采样之后)。存储这块,用对象存储存热数据、冷数据定期归档,比长期挂在对象存储上更省。

我们踩过的一个坑是一开始把采样率设成 100%,纯粹为了"看全一点",结果一周之后存储账单涨了三倍多,只能回头把历史数据删掉重新降采样。采样率这个东西一旦上线就很难改,因为改了之后新旧数据混在一起看图表会非常迷惑,建议一开始就定好,并且写进部署文档里。

写在最后

链路追踪这套东西,原理一点都不复杂,就是给请求发一个 ID,把每一段的耗时记下来串成一条线。但真正花时间的部分全在"怎么让它真的连起来"上上下文传播、采样一致性、时区时钟对齐、agent 覆盖不到的客户端,这些坑每一个都能让你在监控面板上盯着满屏绿灯发愣。

如果你正打算给服务加追踪能力,我建议就按上面 5 条顺序来,先把 1、2、3 跑通,能看到单跳的链路,再去解决 4 的跨服务传递。第 4 条我花了最久,但也正因为花得久,值得单独写一篇链路断在中间的时候,怎么用最土的办法把它定位到具体是哪个客户端没带上下文。

工具链的名字不重要,重要的是先把链路连起来。连起来之后,你会发现很多以前只能靠猜的问题,开始有答案了。

    相关评论

    阅读本文后您有什么感想? 已有人给出评价!

    • 8 喜欢喜欢
    • 3 顶顶
    • 1 难过难过
    • 5 囧囧
    • 3 围观围观
    • 2 无聊无聊

    发表评论

    昵称:
    表情: 高兴 可 汗 我不要 害羞 好 下下下 送花 屎 亲亲