想象你是一家快递公司的客服。用户打电话质问:“我的包裹已经 3 天没动了!到底卡在哪里?” 你打开内部系统,只能看到包裹从“杭州转运中心”出发的记录,之后就一片空白。你只好挨个打电话问各个中转站:“喂,你们见过这个包裹吗?” 大半天过去,才查明白——包裹在第三个中转站的分拣传送带上卡了 2 天。 现代软件的世界里,一个用户请求进入微服务架构后,也要经历类似的“旅程”:它先经过 API 网关,再去订单服务查单、库存服务扣减,最后到支付服务完成扣款。任何一个环节慢了、错了,用户感知到的就只有一句“加载中……”。Jaeger 就是给每一个请求办理“快递运单号”的系统:它给每条请求发一个唯一的 Trace ID(运单号),每经过一个服务就留下一段记录 Span(站点记录),最终拼成一张完整的“物流轨迹图”。 本文会从快递运输的视角,带你拆解 Jaeger 的八个关键环节:为什么要追踪、Trace 与 Span 模型、如何埋点采集、跨服务如何透传、采样策略怎么配、底层架构有哪些组件、如何用瀑布图定位慢调用、以及怎样在生产环境落地。
掌握分布式追踪是构建 AI 系统可观测性平台的核心技能。当你的 AI 应用由多个微服务协作时,Jaeger 能帮你从“玄学排障”升级为“科学定位”,这是 SRE 与 AI 工程化工程师的必备能力。
目录
可视化演示
动态演示Jaeger分布式追踪的核心机制,通过输入/处理/存储/输出四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Jaeger分布式追踪:Jaeg Jaeger架构与核心概念解析 OpenTelemetry数据 核心机制可视化 Jaeger分布式追踪整体架构与关键组件
追踪问题背景:快递黑盒的困境
老式快递公司怎么查一个滞留的包裹?客服只能翻一本又一本各站自记的台账,格式不同、站点独立,查一次要打十几个电话。微服务世界也一样:订单、库存、支付每个服务都有各自的日志,请求一跨服务就“失联”,没人说得清它到底卡在哪个环节。
请求每穿越一个服务,就多一个潜在故障点。客户端感受到的总耗时,是每个环节耗时的叠加。要打破黑盒,必须先建立“链路耗时拆解”的数学模型:把一次请求的总耗时拆成一段一段可测量的时间。
一次请求在 k 个服务间的总耗时可以写成:
有了这个拆解,客服至少知道“包裹全程 60 小时 = 站 A 8 小时 + 站 B 30 小时 + 站 C 22 小时”,问题立刻聚焦到 B。Jaeger 就是为每个请求装上这套“逐站计时系统”。
- 求和符号 Σ从 i=1 到 i=k 对全部 k 个服务的耗时做累加,表示“把所有环节的时间加起来”。
- 单站耗时 t_i第 i 个服务单独处理一次请求所花费的时间,单位毫秒,是链路追踪要测量的最小对象。
- 总耗时 T_total当前请求从发起到返回的完整时间,必须等于所有分段时间之和。
核心概念模型:运单号与站点记录
快递公司给每个包裹分配唯一运单号,包裹每到一个中转站,扫码枪就生成一条“站点记录”:何时进站、分拣多久、何时出站。把运单号下所有站点记录按时间串起来,就是完整的物流轨迹图。Jaeger 的数据模型几乎照搬了这套体系。
Trace 对应一次完整请求(一张运单的全部信息),Span 对应一次服务调用(一个站点记录)。服务 A 调用服务 B 时,B 的 Span 就挂在 A 的 Span 下面成为子 Span,层层嵌套成一棵树。Trace ID 是运单号,Span ID 是记录编号,全局唯一。
父 Span 的计时天然覆盖子 Span,它们存在严格的包含关系:
站点记录必须包含父记录编号才能串成轨迹;Span 的 parent_id 一旦丢失,这棵树就断了枝干,这正是下一节要解决的传输问题。
- 父 Span 总耗时父服务从收到请求到返回响应的时长,必须覆盖所有下游调用的时间。
- 自身耗时父服务自己执行业务逻辑的时间,不含等待下游返回的时间。
- 子 Span 求和把该 Span 的所有直接子 Span 耗时相加,父总耗时等于两者之和。
埋点采集实现:自动扫码枪
老式快递靠人工登记,慢还容易漏。现代快递在每个中转站安装自动扫码枪,包裹路过“嘀”一声自动记录。OpenTelemetry 就是分布式追踪领域的“统一扫码标准”:它定义了数据格式和 API,让所有语言、所有框架用同一种方式上报。
只要在服务里接入 OpenTelemetry SDK,常用的 HTTP 客户端、数据库驱动会被自动插桩,请求进出时自动生成 Span,无需手写埋点代码;业务关键路径还可以加自定义标签,比如订单号、用户 ID,方便后续检索。
扫码枪越多,产生的记录越多,部署前必须估算数据量:
流量高峰期,扫码枪产生的记录会淹没仓库,逼着快递公司考虑“抽检”——这就自然引出了采样策略。
- 请求吞吐 Q系统入口每秒处理的请求数,是数据量的基数。
- 平均 Span 数 n̄一条请求平均穿越多少个服务、产生多少条站点记录。
- 单 Span 大小 s每条 Span 序列化后的字节数,通常包含操作名、标签和耗时。
跨服务透传:交接单不能丢
包裹从杭州到上海,运单号必须写在交接单上随车带走。如果交接单被雨淋湿看不清,下一站就不知道这箱货属于哪个运单,轨迹链当场断裂。微服务之间的“交接单”就是 HTTP 请求头。
每发起一次下游调用,SDK 会把当前 Trace ID 和父 Span ID 写入 OpenTelemetry 标准头 traceparent,下游服务再从中提取上下文继续传给更下游;走消息队列时,也要把上下文塞进消息属性,消费端才能接上链。
透传的唯一要求是同一 Trace ID 逐跳不变:
任何一个中间服务忘了透传,Jaeger 里就会出现两条“半截轨迹”,就像运单号在半路丢失,客服只能看到两个孤立的物流片段。
- 第 j 跳的 Trace ID请求在第 j 个服务中携带的全局追踪标识。
- 逐跳相等传给下一个服务时,Trace ID 必须保持不变。
- 对所有跳成立任意相邻两跳都必须满足等式,要求所有服务“无脑透传”。
采样策略:不可能给每个包裹装 GPS
双十一仓库每秒进出数万个包裹,给每一个都装 GPS、做全量精细化记录,成本不可承受。快递公司的做法是按比例抽检——抽查 10% 的包裹上高级追踪设备,其余走普通流程。Jaeger 的采样策略同理。
最常用的是“头部采样”:网关收到请求时按概率决定“这条要不要记录”,一旦决定,整条链路要么全记录、要么全丢弃,避免出现半条链。采样率 r 决定了最终落库的数据规模。
采样后的期望记录量很简单:
采样率越低,存储越省,但小流量错误链路越容易被漏掉。像快递抽查一样,要平衡“抽查比例”和“问题检出率”,生产环境通常从 0.1 起步再逐步调优。
- 总请求数 N一段时间内进入系统的全部请求数量。
- 采样率 r0 到 1 之间的概率,决定每条请求被记录的可能性。
- 期望采样量被记录下来的请求数的数学期望,等于总数乘以概率。
架构组件存储:快递公司的 IT 体系
一个现代化快递公司的数据链路是:每个网点的扫码枪(Agent)→ 区域集散中心汇总(Collector)→ 中央数据库 → 客服查询台(Query)。Jaeger 的架构与之一一对应,职责清晰。
Jaeger Agent 以边车方式贴近业务进程,负责接收本机 Span 并批量上报;Collector 负责校验、清洗、执行采样,写入存储后端(常为 Elasticsearch 或 Cassandra);Jaeger Query 提供查询 API,Jaeger UI 把结果渲染成轨迹图。存储后端直接决定“能查多久的历史”。
部署前需要先估算存储容量:
就像快递公司要估算中央仓库租多大,部署 Jaeger 前必须用这个公式算存储,否则系统跑两周磁盘就会被写满。
- 采样后每秒写入请求吞吐乘以采样率,得到真正落库的请求数。
- 单条请求数据量平均 Span 数乘以单 Span 大小。
- 保留周期每秒数据量 × 86400 秒/天 × 保留天数,就是总容量需求。
可视化排障:输入运单号看轨迹
用户投诉包裹慢时,客服在官网输入运单号,就能看到每一站的时间线:哪站停留 2 天一目了然。Jaeger UI 的核心视图“瀑布图”就是这个时间线的技术版,也是日常排障用得最多的页面。
瀑布图横轴是时间,每个 Span 是一条横杠,父子关系按缩进显示,条越长说明该服务越慢。瓶颈定位的量化指标是耗时占比:
除了瀑布图,Jaeger 还提供依赖拓扑图,展示服务间的调用关系和流量大小。找到占比最大的那条 Span,就像客服一眼看到“第三个中转站停留了 2 天”,接下来只需展开它查看具体日志。
- 最长耗时所有服务耗时中的最大值,通常是瓶颈所在。
- 总耗时整条链路所有服务耗时之和。
- 瓶颈占比 P最慢服务耗时占总耗时的比例,用来确认“枪打出头鸟”。
部署集成实践:从试点到全网推广
快递公司上马新追踪系统不会一口气铺满全国,而是先选一个枢纽试点,跑通流程后再复制到所有网点。Jaeger 落地也遵循“先试点、再推广”的节奏。
落地路径通常是:先在网关或核心服务接入 OpenTelemetry agent/SDK,配置上报地址;再到 Jaeger UI 验证链路完整性;最后逐步扩大接入范围。核心指标是覆盖度:
生产建议:采样率从 0.1 起步,先观察存储增长再调高;给 Collector 配好缓冲;监控上报延迟,防止链路追踪自身变成瓶颈。当所有网点都装上扫码枪、交接单不再丢失,客服(SRE)就能在几分钟内回答“包裹卡在哪”,这就是 Jaeger 带给微服务体系的确定感。
- 已接入服务数 m已经完成埋点接入并上报 Span 的服务数量。
- 服务总数 M整个微服务体系中服务的总数量。
- 覆盖度 C两者比值,衡量链路追踪的完整程度,目标趋近 1。
AI 应用场景
LLM 应用推理链路追踪
阶段:模型推理与可观测性
解决的问题:LLM 应用从 API 网关、Prompt 模板、RAG 向量检索到模型推理往往横跨多个服务。用户抱怨“回答太慢”时,传统方式只能挨个服务翻日志,无法区分是检索慢、生成慢还是网络慢。接入 Jaeger 后,一次推理请求生成一条 Trace,Prompt 处理、向量检索、模型生成各占一个 Span,瀑布图直接暴露耗时大头。
具体案例:某 AI 客服机器人接入 Jaeger 后发现 P95 延迟 12s,瀑布图显示其中 8s 耗在向量检索——原因是向量索引未加载进内存。修复后 P95 降至 3s。
推荐系统特征链路追踪
阶段:特征服务与在线推理管线
解决的问题:推荐系统链路长,包含召回、粗排、精排、重排和多路特征服务。特征服务偶发超时会导致整体推荐降级,但传统日志只能看到“推荐超时”,看不到是哪个特征通道拖慢。用 Jaeger 给每个特征通道打一个子 Span,超时时能立刻定位到具体特征源。
具体案例:某短视频推荐系统通过 Jaeger 发现“用户画像特征”服务在高峰期的 P99 从 20ms 飙到 500ms,拓扑图显示其依赖的 Redis 集群出现热 key,扩容后恢复。
AI 训练任务调度跟踪
阶段:分布式训练任务生命周期
解决的问题:分布式训练任务涉及数据加载、参数服务器通信、梯度同步等多个阶段,训练变慢时难以判断瓶颈在 I/O 还是网络。将训练任务的每个阶段作为 Span 接入追踪系统,可以量化各阶段耗时占比,辅助调优。
具体案例:某大模型训练团队用 Jaeger 追踪后发现 40% 时间耗在数据加载的图片解码上,于是引入缓存和预取,训练吞吐提升约 25%。
Python 演示
import randomimport json class Span: """一次服务调用单元, 对应 Jaeger 中的一个 Span""" def __init__(self, trace_id, span_id, name, parent_id, duration_ms): self.trace_id = trace_id self.span_id = span_id self.name = name self.parent_id = parent_id self.duration_ms = duration_ms self.children = [] def add_child(self, name, duration_ms): child = Span( self.trace_id, self.span_id + "-" + name, name, self.span_id, duration_ms, ) self.children.append(child) return child def to_dict(self): return { "name": self.name, "span_id": self.span_id, "parent_id": self.parent_id, "duration_ms": self.duration_ms, "children": [c.to_dict() for c in self.children], } class Tracer: """极简追踪器: 带比例采样, 模拟 Jaeger 的 Trace 入口""" def __init__(self, sample_rate=0.3): self.sample_rate = sample_rate self._trace_count = 0 def start_trace(self, root_name, root_ms): self._trace_count += 1 if random.random() > self.sample_rate: return None return Span(self._trace_count, "trace-" + str(self._trace_count), root_name, None, root_ms) SERVICES = [("API网关", 40), ("订单服务", 90), ("库存服务", 55), ("支付服务", 110)] def simulate_request(tracer): """模拟一次用户请求依次穿过四个微服务""" total_ms = sum(ms for _, ms in SERVICES) root = tracer.start_trace("用户请求", total_ms) if root is None: return None node = root for name, ms in SERVICES: node = node.add_child(name, ms) return root.to_dict() def main(): random.seed(7) tracer = Tracer(sample_rate=0.3) total_traces = 0 sampled_traces = 0 for _ in range(2000): total_traces += 1 trace = simulate_request(tracer) if trace is not None: sampled_traces += 1 if sampled_traces <= 1: print(json.dumps(trace, ensure_ascii=False, indent=2)) print("总请求数: %d, 采样记录: %d, 实际采样率: %.2f" % (total_traces, sampled_traces, sampled_traces / total_traces)) if __name__ == "__main__": main()
if random.random() > self.sample_rate: return None采样判断:抽不中就直接放弃这条请求,相当于“没被抽到装 GPS 的包裹”。root = tracer.start_trace("用户请求", total_ms)在入口创建根 Span,相当于给包裹生成一张运单号。node = node.add_child(name, ms)调用下游服务时挂一个子 Span,形成父子嵌套的链式结构。"children": [c.to_dict() for c in self.children]递归把 Span 树转成 JSON,模拟 Jaeger UI 收到的数据格式。tracer = Tracer(sample_rate=0.3)配置 30% 采样率,控制台会验证实际采样比例接近这个值。
常见误区
误区所有链路都记录下来最稳妥,以后想查什么都有,采样万一漏掉关键请求怎么办?所以采样率一定要设成 1。
正解全量采样在高并发下每秒产生数 GB 数据,存储成本、网络开销和查询性能都会被拖垮,得不偿失。快递公司也不会给每个包裹装 GPS。正确做法是头部比率采样兜底,再对核心业务(如支付、登录)单独开高优先级全量采样。
为何常见:初学者只看到“记录越多越好”,没意识到可观测性系统本身也会消耗生产资源,属于典型的成本意识缺失。
误区只要给每个服务装上 Jaeger SDK,打开 UI 就能看到完整的跨服务调用链,什么都不用配置。
正解SDK 只负责“本站记录”,跨服务的“交接单透传”(traceparent 头注入与提取)必须正确。很多老项目中自定义 RPC、消息队列不会自动透传,需要手动把上下文塞进请求头或消息属性,否则 UI 里全是断成两截的孤岛链路。
为何常见:自动插桩太“智能”,让人误以为上下文传播也全自动了,忽略了下游调用这一最关键的环节。
误区Jaeger 已经能看到每个服务的耗时和调用关系,日志系统可以下线了。
正解链路追踪回答“请求走的哪条路、哪段慢”,日志回答“那段代码为什么慢”,指标回答“系统整体健康度如何”。三者是互补关系,可观测性三大支柱缺一不可。
为何常见:新手容易把“看得见调用链”等同于“看得见一切”,其实 Span 里只有元数据,详细的异常堆栈和业务参数仍在日志里。
误区Trace ID 只是个随机字符串,只要能区分不同请求就可以,用 UUID 或者自增数字都行。
正解Trace ID 需要全局唯一,且通常设计成 128 位,内嵌时间戳和随机部分,这样既保证海量请求下不冲突,又能支持按时间范围快速检索。自增数字在多节点下极易重复,会导致不同请求的链路互相串扰。
为何常见:单机思维的残留——本地唯一很简单,分布式环境下“全局唯一”才是真正的考验。
误区既然头部采样在网关就决定了,中间服务随便配不配置采样都无所谓。
正解头部采样确实在入口一次性决策,但下游服务如果自己又做了一层采样或过滤,就会出现“父 Span 在、子 Span 没了”的半截链。链路追踪的铁律是:一旦入口决定记录,所有下游必须无条件透传和上报,中间层不能再次采样。
为何常见:把“采样策略”和“上下文传播”两个概念搞混,以为采样点越多越省,忽略了整链一致性的要求。
面试题
Q1. 在 Jaeger 链路追踪中,一个完整请求的全局唯一标识是什么?
- A.服务名 ServiceName
- B.请求的唯一 TraceID← 正确答案
- C.单条日志的 LogID
- D.容器 Pod 的 IP 地址
答案:B。Trace ID 是整条链路的全局唯一标识,相当于快递运单号,负责把多个服务的记录串起来。Span ID 只是单个站点记录编号;服务名和日志 ID 都不能关联跨服务的多个调用。
Q2. 下列哪个 Jaeger 组件负责贴近业务进程接收 Span 并批量上报?
- A.Jaeger Query 组件
- B.Jaeger Agent 组件← 正确答案
- C.Elasticsearch 组件
- D.Jaeger UI 界面组件
答案:B。Jaeger Agent 相当于网点扫码枪,以边车方式贴近业务进程收集本机 Span;Query 是客服查询台,负责提供查询 API;Elasticsearch 是中央数据库;UI 是查询前台。
Q3. 某服务每秒处理 10000 个请求,每条请求平均产生 5 个 Span,每个 Span 约 1KB,采用 10% 采样后每秒需要存储多少数据?
- A.每秒 1 MB 左右
- B.每秒 5 MB 左右← 正确答案
- C.每秒 10 MB 左右
- D.每秒 50 MB 左右
答案:B。每秒落库请求数 = 10000 × 10% = 1000 条;每条 5 个 Span × 1KB = 5KB;1000 × 5KB = 5MB。对应公式 D = Q × r × n̄ × s,采样率对存储成本的影响是线性的。
Q4. OpenTelemetry 标准规定的跨服务上下文透传 HTTP 头名称是?
- A.X-Trace-Id 请求头
- B.traceparent 请求头← 正确答案
- C.X-Span-Id 请求头
- D.X-Request-Id 请求头
答案:B。OpenTelemetry 规范规定跨服务透传的标准头是 traceparent,格式为“版本-traceId-parentId-flags”,承载完整的上下文信息。其余请求头都是各家私有方案,无法互通。
Q5. 瀑布图中某父 Span 耗时 200ms,其子 Span 耗时之和为 150ms,该服务自身业务逻辑耗时为?
- A.自身耗时约 50ms← 正确答案
- B.自身耗时约 150ms
- C.自身耗时约 200ms
- D.自身耗时约 350ms
答案:A。根据父子耗时公式 T_parent = T_own + ΣT_child,200 = T_own + 150,所以自身耗时 50ms。T_own 指服务自己执行逻辑的时间,不包括等待下游返回的时间。
Q6. 采样率 r=0.01 时,对 N=1000 个请求采样,期望采样条数约为?
- A.大约 1 条左右
- B.大约 10 条左右← 正确答案
- C.大约 100 条左右
- D.大约 1000 条左右
答案:B。期望采样量 = N × r = 1000 × 0.01 = 10 条。采样是概率行为,实际值会在 10 附近波动,真实观测到的条数未必正好等于期望值。
Q7. 定位一条慢链路的最主要瓶颈,最直接的方法是?
- A.找到耗时占比最大的 Span← 正确答案
- B.随机挑选一个服务排查
- C.关闭采样改全量采集
- D.在业务代码中增加日志
答案:A。瀑布图中耗时占比最大的 Span 就是瓶颈所在,对应瓶颈占比公式 P = max(t_i) / Σt_i。随机排查靠运气,关闭采样或加日志都不是定位瓶颈的高效手段。
Q8. 下列哪些属于 Jaeger 支持的采样策略?
- A.固定全量采集策略← 正确答案
- B.比率概率采样策略← 正确答案
- C.动态速率采样策略← 正确答案
- D.网络流量采样策略
答案:ABC。Jaeger 支持恒定采样(全量或全不采)、比率采样(按概率抽)、动态速率采样(根据流量自适应调整)。它没有“网络流量采样”这种策略,采样决策不依据网络流量。
Q9. 可观测性体系的三大支柱包括哪些?
- A.日志数据收集系统← 正确答案
- B.指标数据收集系统← 正确答案
- C.链路追踪数据系统← 正确答案
- D.容器调度管理系统
答案:ABC。可观测性三支柱是 Logging(日志)、Metrics(指标)、Tracing(链路追踪),分别回答“发生了什么、整体是否健康、为什么慢”。容器调度是基础设施能力,不属于可观测性数据。
Q10. 关于头部采样(Head-based Sampling),下列说法正确的有?
- A.在请求入口就决定是否采样← 正确答案
- B.整条链路要么全留要么全弃← 正确答案
- C.请求完成后按结果精细筛选
- D.实现简单且对存储容量友好← 正确答案
答案:ABD。头部采样在入口一次性做决策,整条链路保持一致,实现简单、存储可控。“请求完成后按结果精筛”是尾部采样(Tail-based Sampling)的特征,它更灵活但实现复杂、需要先缓冲全量数据。