可观测性的难处不在接入,在设计

2026-10-03 Aryee 4

接入 OpenTelemetry 有多简单,后续翻车就有多快。

Java Agent 挂上,Spring Boot 自动配置一开,Trace、Metrics、Logs 三路信号就自动往 Collector 里灌。从「零可观测」到「有数据」,一个下午就够了。然后问题开始了:数据量比预期大十倍,存储账单月增,关键 Trace 被采样扔掉了,异步调用链在中途断裂——查问题时看到的是一条断成三截的 Span。

我自己的那套 CMS 跑在 WebFlux + R2DBC 上,接 OTel 的时候上述问题一个没少碰。这篇文章不聊怎么加依赖、怎么写配置——那些在官方文档里都有。只聊接入之后真正卡住的五个设计决策。

决策一:全量接入还是先砍掉九成数据

OTel Java Agent 默认采集每一个请求的完整链路。一个 200 QPS 的服务,每个请求跨四个下游、每层产生 15~20 个 Span,一天的 Trace 量轻松过亿。

全量采集意味着一亿个 Span 的存储、传输和索引。而其中 99% 是正常的——真正需要排查的,是那些报错的、慢的、异常的。

头部采样是最先想到的解法:在链路起点掷骰子,只保留 1% 的 Trace。简单、成本低。但它有一个根本缺陷:掷骰子的时候,你还不知道这条 Trace 的结局。它最后会不会报错、响应时间多长、有没有触发告警——都不知道。一个 500 错误、200ms 超时的请求,在采样器眼里和正常请求没有区别。

尾部采样把决策推迟到链路结束:所有 Span 先收齐,再按规则筛选。报错的全留、超过阈值的留、正常的只留 1%。存储量能降一个数量级,同时保留所有有价值的链路。代价是 Collector 要在内存里等整条 Trace 完成——跨多个服务的长链路意味着大量未完成的 Trace 驻留内存。KubeCon 2026 上 VictoriaMetrics 分享的实测数据是尾部采样能降 90% 成本。

判据:生产环境必须做采样。流量低(< 50 QPS)且排查需求不强的,头部采样固定比率够用;流量大或排查需求强的,上尾部采样。全量采集只适合开发环境——生产环境全量跑三个月,存储账单会替你做出决定。

决策二:Collector 放哪

数据从应用出来,到最终存储之间,至少经过一层 OTel Collector。Collector 有两种部署模式:

Agent 模式:每个应用实例旁边跑一个 Collector,应用把数据发到 localhost。延迟最低、不经过网络,但每个 Agent 各自配置、各自连后端,服务一多,后端的连接数跟着膨胀。

Gateway 模式:集中部署一组 Collector,所有应用的数据发到 Gateway。统一管理配置、认证、采样策略,集中做批处理和数据脱敏。代价是引入一跳网络延迟,Gateway 本身变成需要高可用的组件。

大规模场景通常两层叠加——应用发给本地 Agent,Agent 转发给集中 Gateway,Gateway 统一导出到 Jaeger、Prometheus 等后端。

判据:五十个服务以下,一个 Gateway 加直连后端就够了,不用引入 Agent 层。Agent 模式在容器环境里增加了一层需要管理的 sidecar,复杂度收益不对等。但无论哪种模式,Collector 必须配 memory_limiter 和健康检查——队列堆积时 OOM 是最常见的 Collector 死法,峰值时段丢的数据恰好是最需要的那段。

决策三:异步调用链为什么总是断

Trace 在下半段突然变成一条新链路——上半段的 Span 还在,下半段的服务另起了一个全新的 Trace ID。这种现象在同步调用链里几乎不会发生,但在异步场景里极其常见。

根本原因:OTel 的 Context 默认绑定在当前线程的 ThreadLocal 上。线程一切换,Context 就丢了。

在 Java 里,这个问题有三个高发区:

线程池。ExecutorService.submit() 把任务交给另一个线程,ThreadLocal 不会跟过去。解法是把 Executor 包一层,在提交时捕获当前 Context、在执行时恢复。OTel 的 opentelemetry-instrumentation-annotations 提供了 @WithSpan 等注解,但只覆盖方法级跨线程,不覆盖 Executor 级别的传播——后者需要自己包装或使用 contextTaskWrapping 工具方法。

响应式框架。WebFlux / Reactor 里一个请求可能在多个 Reactor 线程之间跳,根本没有「当前线程」这个概念。OTel 提供了 opentelemetry-reactor-3.1 模块,通过 Reactor 的 Context 机制传播 OTel Context。但如果业务代码里手动用了 subscribeOn() 或 publishOn() 切换调度器,传播链可能被打断——需要确认 Hook 在调度器切换之前注册。

跨进程传播。HTTP 调用走 Header 传递 Trace Context,OTel 默认使用 W3C traceparent 格式。如果上下游有一方还在用旧的 B3 格式,需要同时启用两种传播器。更隐蔽的问题是:某些网关或代理会自定义 Header 名,导致传播格式不匹配,下游收到的是一串无法解析的值,只能另起新 Trace。

验证方法:在下游服务里检查 Span 的 parentSpanId 是否指向上游的 spanId。如果下游变成了新 Trace 的 root Span,传播就断了。这个检查比任何告警都直接——它直接告诉你 Context 有没有传过去。

决策四:存储成本失控怎么收

接入 OTel 后第一个月的账单通常会让团队吃惊。一个中等规模的服务,全量 Trace 一天的数据量在数十 GB 级别。

控制成本最有效的杠杆不在采样率,在 Span 属性瘦身。自动埋点经常把 HTTP Header、完整 SQL 语句、甚至 Request Body 塞进 Span Attribute,单个 Span 膨胀到几 KB。在 Collector 层用 attributes 处理器砍掉不需要的大字段,数据量通常能降 60%~70%。

Logs 和 Trace 关联是另一个杠杆。把 Log 带上 trace_id 和 span_id,查问题时从一条 Trace 跳到相关日志,不需要全量索引日志。但前提是日志存储支持这种关联查询——如果日志系统不支持按 trace_id 过滤,关联就只是理论上的便利。

Metrics 基数是第三个坑。Prometheus 远程写入时,标签组合的笛卡尔积决定了时间序列的数量。一个 http_request_duration 指标,如果带上 method、path、status、instance 四个标签,而 path 包含用户 ID,时间序列数会爆炸。解法是在 Collector 层用 metricstransform 处理器对高基数标签做降维或丢弃。

量级参考:

场景 日 Trace 量 月存储成本(自建)
全量、无瘦身 ~30 GB/天 ¥5,000~8,000
尾部采样 10% + 属性瘦身 ~3 GB/天 ¥500~1,000
头部采样 1% ~300 MB/天 ¥100~300

三个量级差两个数量级,选型不同,成本结构完全不同。

决策五:数据存下来了,怎么查

可观测性数据存下来只是完成了一半。查询架构决定了这些数据能不能在排查问题时真正被用到。

一个常见的失误是把所有信号塞进同一个存储。Trace 需要的是按 ID 精确查找、按条件过滤(延迟 > 1s、状态码 = 500);Metrics 需要的是按时间窗口聚合(过去 7 天每 5 分钟的 P99);Logs 需要的是全文检索加上下文展开。三种查询模式差异很大,一个后端很难同时做好。

分治架构是目前比较务实的做法:

  • Trace → Jaeger(Cassandra/ES 后端)或 Grafana Tempo(对象存储后端,成本更低)
  • Metrics → Prometheus / VictoriaMetrics
  • Logs → Loki(和 Tempo 共享对象存储)或 Elasticsearch

在 Grafana 层做关联:从一条 Trace 的 Span 跳到对应时间段的 Metrics 和 Logs。Grafana 原生支持 Tempo + Prometheus + Loki 的跨数据源关联,用 trace_id 和 span_id 做桥梁。

判据:如果团队只接了 Trace 还没接 Metrics 和 Logs 的标准化采集,Jaeger 单独跑就够了。一旦三信号都接了 OTel,尽早做分治——拖得越久,数据迁移成本越高。小团队用 Grafana 三件套(Tempo + Prometheus + Loki)成本可控,全跑在对象存储上,不需要维护 Elasticsearch 集群。

五个决策的共同点

回头看这五个决策,它们没有一个是关于「用哪个工具」的。Collector 放哪、采样怎么做、Context 怎么传、成本怎么控、查询怎么搭——全是设计问题。

OpenTelemetry 解决了数据采集的标准化。它没有解决、也不可能替你解决的,是采集之后的数据治理。这部分工作决定了可观测性是真正能在凌晨三点帮你定位问题,还是变成一个每月花几千块、但没人查得动的数据仓库。

接入只是起点。设计才是正事。

你手上那套系统,卡在哪一步?

留个手机号和方便的时间,我打过来先听你说现状与约束,再给可执行的判断:这套系统是该继续修、该动哪里,还是干脆重做一版设计更省。

约一次沟通不接纯 UI 外包,只做系统层面的问题。