凌晨12点,手机震了三次,心里一紧。登录后台一看,支付回调接口的P99延迟飙到了5秒,正常水位是200毫秒。用户付款成功但页面迟迟不跳转,客服群里已经炸了。我做了这么多年后端,最怕这种"多个服务都正常,但链路上不知道哪个环节慢半拍"的问题。幸好这次有Jaeger。输入网关日志里捞出的trace_id,一键查询,30秒定位到外部渠道商接口异常。整个排查过程没有去翻一台台服务器日志,也没有靠猜。这个"救命工具"就是分布式追踪系统Jaeger,本文我会把它的核心模型、采样策略、部署选型、性能影响和实战经验一次讲透。
1. 一次支付回调超时的排查实录
1.1 从报警到定位:完整还原排查链路
先交代一下背景。我们用的技术栈是Spring Cloud,核心交易链路涉及网关、订单服务、支付服务、外部渠道商回调。出问题那晚,报警规则是"支付回调接口P99超过3秒持续5分钟",阈值一破,值班手机就响了。
第一步,我没有直接登录服务器,而是先去Nginx访问日志里捞慢请求。理由很简单:流量入口的访问日志最全,而且里面有网关在入口处注入的trace_id,形如8a3d5f2e...这一长串十六进制。有了它,就相当于拿到了这条请求在系统中的"身份证号"。
第二步,打开Jaeger UI,搜索框粘贴这个trace_id,回车。一条完成的调用链在时间线上舒展开来,从网关到订单服务再到支付服务,一直到外部渠道商HTTP调用。整条链路总耗时4.7秒,内部服务自身的逻辑只花了280毫秒,剩下的3.9秒全部压在了那个外部HTTP调用上。
第三步,点开那个外部调用的Span,看Tags。方法、URL、状态码、响应体大小全都记录在案。进一步确认是渠道商接口的响应变慢,而不是我们超时时间设得太短导致误报。
第四步,打开Jaeger的依赖分析页(Service Dependency Graph),看到这个外部依赖的调用量和错误率在最近15分钟明显攀升,坐实了"上游服务问题",直接发工单联系渠道商处理。
整个定位过程不到5分钟。对比以前没有链路追踪时的"复盘会"——开发说网关没问题,网关说订单服务正常,订单服务说支付服务没报错,支付服务说对方返回慢——你说气不气。
1.2 为什么Nginx网关层要生成trace_id
很多同学问过:为什么不直接在业务服务里生成trace_id,非得在网关生成?核心原因是"入口生成、全链共享"。网关是流量的必经之路,所有请求都从这里进来。在这层生成一个全局唯一的ID,通过HTTP Header传给下游,后面每一个服务都把这个ID原样透传下去,最终就能保证一次用户请求产生的所有Span都属于同一条Trace。
Jaeger原生使用uber-trace-id这个Header,格式是{trace-id}:{span-id}:{parent-span-id}:{flags}。现在业界标准是W3C的traceparent,形如00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01,Jaeger早已兼容。
提示:如果请求进来时没有携带任何trace上下文,Jaeger SDK会自动生成一个新的TraceId,自己成为Root Span。这就是为什么你能在UI里看到一些"孤立"的Trace——它可能是外部直接调用,没走网关导致的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心数据模型:Span、Trace与上下文传递
2.1 Span的字段结构
分布式追踪里,一个Trace由若干个Span组成,Span是链路中的最小工作单元。可以把它理解成电视剧里的一集:Trace是整部剧,Span是每一集,而References(引用关系)决定了集与集之间的播放顺序。
一个Span,核心字段如下表:
| 字段 | 说明 | 举例 |
|---|---|---|
| operationName | 操作名称 | POST /api/pay/callback |
| startTime | 开始时间,微秒精度 | 1699999999999000 |
| duration | 持续时长 | 4700ms |
| tags | 标签,键值对,可检索 | http.method=POST, http.status.code=200 |
| logs | 日志事件,带时间戳 | 栈信息、业务标记、异常事件 |
| references | 引用关系 | ChildOf / FollowsFrom |
| traceId | 所属Trace | 8a3d5f2e... |
| spanId | 自身ID | 16位十六进制 |
| serviceName | 所属服务 | pay-service |
Tags的设计逻辑值得多说一句。Tags是"可索引的维度",UI里支持按tag名=值做Search,比如查http.status_code=500就能筛选出所有返回500的Span。但注意,tags不要塞太多大对象,只放那些用于查询和定位的键值,比如订单ID、用户ID、HTTP状态码。想记录业务细节,用logs更合适。
2.2 父子关系与FollowsFrom
Span之间最常用的引用关系是ChildOf,语义是"子Span的完整生命周期取决于父Span"。比如支付回调接口(父Span)内部发起了数据库查询(子Span),数据库查询的结束时间必然早于父Span。
另一种引用关系是FollowsFrom,很多初学者不知道。它的语义是"父Span触发子Span,但不等待它结束"。典型场景是异步消息:支付成功后,把短信通知的消息丢进MQ,父Spa
