可观测性三件套落地:日志、指标、链路追踪
微服务最难的不是写代码,是出事之后定位不到问题。第 7 章我们讲了链路追踪的原理,这一章讲怎么真的把它落地到生产:Spring Boot 3 之后 Sleuth 已经归档,得换成 Micrometer Tracing;追踪信息怎么和日志串起来;指标怎么变成告警;SkyWalking 和 OpenTelemetry 两条路线到底怎么选。目标很明确——当线上报警响起时,你能在三分钟内说清"哪个服务、哪个接口、慢在哪一步",而不是翻十台机器的日志。
三件套分别回答什么问题
先把分工搞清楚,不然后面配置起来你会不知道每一项是为了什么。三件套不是"三个都上就更稳",而是三个不同维度的问题,缺一个就会有盲区。
| 支柱 | 回答的问题 | 典型数据形态 | 典型工具 |
|---|---|---|---|
| 日志 Logging | "这一次请求到底发生了什么?" | 离散的文本事件,量大、成本高 | ELK / Loki / ClickHouse;SLF4J + Logback |
| 指标 Metrics | "整体健康吗?现在比昨天差多少?" | 可聚合的数值时间序列,成本低 | Micrometer + Prometheus + Grafana |
| 追踪 Tracing | "这个慢请求,时间花在哪一步?" | 一次请求的调用树(trace + span) | Micrometer Tracing + Brave/OTel,SkyWalking |
论为什么只靠日志不够
① 日志没有全局视角:一次下单请求穿过网关、订单、库存、账户四个服务。你只有一堆各自为战的日志文件,"这次请求"在哪台机器上发生了什么都不确定,只能靠时间戳肉眼对齐。
② 指标知道"坏了"但不知道"为什么坏":告警告诉你 P99 从 200ms 涨到 3s,但它不会告诉你是数据库慢、还是某个下游超时。指标是体温计,不是 CT。
③ 排查路径应该是这样的:指标发现异常 → 追查找出是哪条链路的哪一步变慢 → 日志看那一步具体报了什么错。三件套的价值就是"从聚合到个体"的三级下钻,缺任何一级,下钻都会断。
链路追踪:Micrometer Tracing 与 traceId 的传播
Spring Cloud Sleuth 已于 2023 年归档(迁移到 Micrometer Tracing),所以 Spring Boot 3.x 的正确选择是 Micrometer Tracing。它本身是一层门面(facade),底层可以桥接到 Brave(Zipkin 生态,轻量)或 OpenTelemetry(CNCF 标准,生态更大)。
论traceId / spanId 是怎么"跟着请求走"的
① 一个 traceId 代表一整次请求,一个 spanId 代表链路中的一段。Gateway 收到请求时先生成一个 traceId 和一个根 spanId,然后把这些 ID 塞进 HTTP 请求头往下游发。
② 下游收到头之后不新建 traceId,只新建自己的 spanId,并把"父 spanId"记下来。于是所有服务上报的数据靠 traceId 就能拼成一棵树。
③ 默认用的是 W3C 标准头:traceparent(含 traceId、parent spanId、采样标记)。Brave/Zipkin 生态传统上用的是 X-B3-TraceId / X-B3-SpanId 系列。混用两套标准是排查不出来的常见原因之一。
④ 跨消息也要传播:走 MQ 时 traceId 要放进消息头(RocketMQ 的 user property、Kafka 的 header),消费端取出来继续接。否则"下单服务 → MQ → 积分服务"这一段就断了,追踪树会缺一块。
Micrometer Tracing + Brave + Zipkin 的最小可运行配置
业务代码里主动打点和取追踪 ID
Sleuth 到 Micrometer Tracing 迁移对照
如果你的项目原来是 Spring Boot 2.x + Sleuth,升级到 3.x 时这一节就是"替换清单"。核心变化:Sleuth 的 API 从 org.springframework.cloud.sleuth 整体换成了 Micrometer 的 io.micrometer.tracing,概念没变,类名全变。
| 能力 | Sleuth(Spring Boot 2.x,已归档) | Micrometer Tracing(Spring Boot 3.x) |
|---|---|---|
| 依赖 | spring-cloud-starter-sleuth | micrometer-tracing-bridge-brave(或 otel 桥) |
| 上报 | spring-cloud-sleuth-zipkin | zipkin-reporter-brave / OTLP exporter |
| 配置项 | spring.sleuth.sampler.probability | management.tracing.sampling.probability |
| 取 tracer | 注入 org.springframework.cloud.sleuth.Tracer | 注入 io.micrometer.tracing.Tracer |
| 创建 span | tracer.nextSpan().name("x").start() | 形式类似,但返回 io.micrometer.tracing.Span,需配 withSpan 建立作用域 |
| 日志字段 | %X{X-B3-TraceId} | %X{traceId} / %X{spanId} |
| 注解打点 | @NewSpan / @ContinueSpan | 改用 @Observed(Observation API) |
| 与指标的关系 | 追踪和指标是两套独立体系 | 统一到 Observation API:一次打点同时产出指标和追踪 |
只换依赖、忘了改 logging.pattern.level,结果每行日志里 traceId 全是空的或者显示成 %X{X-B3-TraceId} 字面量。排查了半天以为是追踪没生效,其实只是占位符名字对不上。迁移后第一件事:随便发一个请求,看日志里有没有真实的 16 进制 traceId。有,说明链路通了;没有,先查日志格式再查依赖。
日志聚合:让 traceId 把日志串起来
日志分散在十几台机器上是不可能排查的,必须集中。做法是统一的:应用把日志输出到 stdout,采集器(Filebeat / Promtail / Fluent Bit)从 stdout 收集并推到日志后端,你在一个界面上按 traceId 搜索,一次请求跨所有服务的日志就全出来了。选型上,ELK 功能全但资源重,Loki 只索引标签、成本低。
| 方案 | 索引方式 | 优势 | 代价 / 适合 |
|---|---|---|---|
| ELK (Elasticsearch + Logstash + Kibana) | 全文索引,每个字段都可搜 | 检索能力强,能做复杂聚合与可视化,生态成熟 | 存储和内存开销大,集群运维成本高。适合日志量大、有专职运维的团队。 |
| Loki (+ Promtail + Grafana) | 只索引标签,日志正文不索引 | 成本低、部署简单,和 Grafana 指标在同一界面 | 正文只能"过滤"不能"全文检索",复杂查询慢。适合中小团队、指标+日志一体化。 |
| ClickHouse 方案 | 列式存储 + SQL 查询 | 写入吞吐极高,SQL 灵活,成本可控 | 要自己搭采集与查询链路。适合日志量大又要便宜的场景。 |
Logback 结构化输出:让采集器能按 traceId 建索引
很多团队上了 Zipkin,界面上也能看到红色慢请求,但点进去只有 span 名字,没有业务信息——因为日志里没带 traceId,你在日志系统里搜不到那一次请求的任何业务输出。记住:追踪负责"定位到哪一步",日志负责"那一步发生了什么",两者的桥梁就是 traceId。所以配置顺序应该是:先把 traceId 打进日志格式,再配上追踪上报,最后才考虑采样率、告警这些优化。顺序反了,你会发现追踪"看着有了但没用"。
指标:Micrometer + Prometheus + 告警
指标是唯一成本低到可以"全量长期保留"的可观测性数据,也是告警的数据源。Spring Boot 3 的默认指标门面是 Micrometer,它把 JVM、HTTP、数据库连接池、线程池等的指标自动注册好,你只需要暴露 /actuator/prometheus 端点,让 Prometheus 定期来抓。
自定义三种最常用的指标:计数器、计时器、仪表盘值
Prometheus 抓取配置 + Alertmanager 告警规则
论为什么平均耗时是最没用的指标
① 平均值会掩盖长尾:100 个请求里 99 个 20ms、1 个 5 秒,平均值只有 70ms,看着很健康。但那个 5 秒的请求背后可能是一个用户投诉、一次超时重试风暴。
② 用户感受到的是 P99,不是平均值。稍微大一点的业务,1% 的请求可能就是每天几千次,足以形成舆情。
③ 所以告警要用百分位:histogram_quantile(0.99, ...),Grafana 面板上也同时看 P50/P95/P99 三条线。P99 涨而 P50 平,说明是局部问题(比如某个下游偶发超时);P50 也涨了,说明是整体容量问题。这一条判断能省你很多时间。
SkyWalking 与 OpenTelemetry:两条路线怎么选
Micrometer Tracing 是"应用侧埋点"的方案,还有一类是"探针自动埋点"的方案。SkyWalking 属于后者(Java Agent 无侵入),OpenTelemetry 是目前的事实标准(既支持 SDK 也支持 Agent)。这不是"谁更好"的问题,而是"你的约束是什么"的问题。
| 对比项 | Apache SkyWalking | OpenTelemetry (OTel) |
|---|---|---|
| 接入方式 | Java Agent 挂载(-javaagent),代码零改动 | SDK 手动埋点,或用 OTel Agent 自动注入 |
| 接入成本 | 极低。加启动参数即可,框架组件自动识别 | SDK 方式需要改代码;Agent 方式成本接近 SkyWalking |
| 数据模型 | 自有模型,与自家 UI/OAP 存储深度绑定 | 厂商中立标准,trace/metrics/logs 三合一规范 |
| 后端选择 | 官方 OAP + Elasticsearch/BanyanDB,一体化开箱即用 | 自由:Jaeger、Tempo、Prometheus、Datadog、阿里云 ARMS 等都能接 |
| 可定制 / 可移植 | 改造成本高,容易和 SkyWalking 生态绑定 | 标准协议(OTLP),换后端不动代码,避免供应商锁定 |
| 国内生态 | 中文文档与社区非常好,国内公司用得多 | 大厂逐渐统一到 OTel,云厂商基本都支持 OTLP 接入 |
| 适合谁 | 想快速见效、不想改代码、团队运维能力一般的团队 | 有明确可观测性规划、想避免锁定、多语言混合技术栈的团队 |
SkyWalking 的接入:只改启动命令,不碰代码
从 OTel Agent 演进到"标准协议 + 自由后端"的部署形态
论怎么在这两条路线里做决定
① 看团队规模与运维能力:三五个人的团队、没人专门维护可观测性平台,SkyWalking 更省事,装上就有 UI 和拓扑图。
② 看是否需要避免锁定:如果未来可能上云、可能用云厂商的 APM,那从一开始就走 OTel 标准协议,换后端时不用改业务代码。
③ 看是否多语言:Java + Go + Node 混合的团队,OTel 的统一规范优势非常明显;纯 Java 团队 SkyWalking 的体验更顺。
④ 其实可以共存:两边都是 OTLP/自有协议,很多公司用 OTel Agent 采集、通过 Collector 同时喂给 SkyWalking 和 Tempo。不用把这道题做成单选题,关键是先把"统一用哪种传播头(建议 W3C traceparent)"定下来。
坑一:采样率设成 1.0。这是新手最容易犯的错——为了"看得全"把生产采样率写成 1.0。结果每个请求都产生一个 span 上报,QPS 一万的服务每秒上报几万个 span,网络带宽、追踪后端存储、应用 CPU 全部被打爆,甚至会影响主业务。正确做法:生产用 1%~10% 采样,再配尾部采样把错误和慢请求全部保留——这样既省成本,又不丢关键链路。
坑二:traceId 没跨线程传递。主线程里有 traceId,一提交到线程池/异步任务里就变成空的。原因是 Micrometer/Brave 把追踪上下文放在 ThreadLocal 里,线程一换就丢了。解决:用 @Async 时确保执行器是 Spring 包装过的(会传递上下文),或者手动用 Tracer.withSpan() 在子线程里重建作用域,ContextExecutorService / TaskDecorator 也是常用手段。
坑三:只埋点不看。上完 Zipkin 和 Grafana 就结束了,没有任何告警规则、没有看板、没人值班时看。可观测性的价值在于"问题出现时有人被通知到",没有告警规则,再好的追踪也只是事后复盘用的工具。
① 日志、指标、追踪各答一个问题,"聚合 → 定位 → 细节"三级下钻缺一不可。
② Spring Boot 3 起用 Micrometer Tracing 取代 Sleuth,日志格式要同步改成 %X{traceId},否则追踪形同虚设。
③ traceId 是日志与追踪之间的桥;跨服务靠 HTTP 头(推荐 W3C traceparent),跨 MQ 靠消息头。
④ 生产采样率绝不要写 1.0,用低比例 + 尾部采样保错误与慢请求。
⑤ 指标要看 P99 不看平均值;指标必须配 Alertmanager 规则,否则等于没上。
⑥ SkyWalking 胜在无侵入快见效,OTel 胜在标准中立不锁定,可共存,但要统一传播头。
本章面试题
Q1. 一次请求在微服务里跨了 4 个服务,你怎么把它的日志串起来?
参考答案
靠 traceId。链路:入口(网关)生成 traceId,通过 HTTP 头(W3C 的 traceparent 或 Brave 的 X-B3-*)逐跳传递,每个服务不从头上新建 traceId 只新建 spanId。日志侧:把 traceId 打进日志格式(%X{traceId}),日志统一采集到 ELK/Loki。使用:从追踪界面拿到慢请求的 traceId,粘到日志系统里一次搜出全链路日志,按时间排即可还原现场。跨 MQ 时要额外把 traceId 放进消息头。
Q2. 为什么生产环境不能把采样率设成 1.0?那怎么保证不丢关键链路的追踪?
参考答案
不能设 1.0 的原因:全量采样意味着每个请求都产生并上传 span。高 QPS 下这会让追踪后端存储爆炸、占用大量网络带宽、甚至反噬应用自身的 CPU 和延迟,属于典型的"用可观测性把系统搞挂了"。解决办法:用尾部采样(tail sampling)——先收集完整链路,再在 Collector 侧按策略决定保留哪些:所有带 error 的、耗时超过阈值的 100% 保留,正常请求按 1%~10% 概率保留。这样成本可控,又不会丢掉真正需要排查的那部分数据。
Q3. Spring Boot 2 的 Sleuth 升级到 3.x 要改哪些东西?
参考答案
四类改动:① 依赖,spring-cloud-starter-sleuth → micrometer-tracing-bridge-brave(或 otel 桥),上报换 zipkin-reporter-brave;② 配置项,spring.sleuth.sampler.probability → management.tracing.sampling.probability;③ API,注入的 Tracer 从 org.springframework.cloud.sleuth.Tracer 换成 io.micrometer.tracing.Tracer,注解从 @NewSpan 改为走 Observation API 的 @Observed;④ 日志格式,占位符从 %X{X-B3-TraceId} 改成 %X{traceId}。最后一条最容易被漏掉。
Q4. 接口的 P50 是 30ms,P99 是 2s,说明了什么?你应该怎么排查?
参考答案
说明:绝大多数请求很快,但有 1% 的请求被拖到 2 秒,长尾明显。这通常不是容量问题(否则 P50 也会涨),而是某个偶发因素——某个下游服务偶发超时、数据库偶发慢查询、GC 停顿、线程池排队、连接池不够用。排查:① 用追踪筛出耗时大于 1 秒的 trace,看时间花在哪个 span;② 对应服务看 GC 日志和 JVM 指标(有没有长停顿);③ 看下游的 P99 是否同步变差;④ 检查连接池/线程池的排队指标。优化方向:给下游加超时和熔断,避免一个慢下游拖住整条链路。
Q5. SkyWalking 和 OpenTelemetry 你会怎么选?
参考答案
不是好坏题,是约束题。选 SkyWalking:纯 Java 技术栈、团队运维人力少、想快速见效、不想改任何代码——Java Agent 挂上就有 UI 和拓扑图,中文资料也好。选 OTel:多语言混合栈、未来可能接入云厂商 APM、希望避免供应商锁定——OTLP 是事实标准,换后端不用动业务代码。实践中常见做法是两者共存:用 OTel Agent 采集,通过 Collector 分发到多个后端。无论选哪个,都要先把追踪上下文的传播格式统一(建议 W3C traceparent),否则跨服务链路一定是断的。