凌晨两点,你在工位上盯着监控大屏,心跳急促——用户投诉下单失败,可前端、订单服务、支付服务、库存服务全都显示“运行正常”。每个服务的日志都干干净净,但订单就是丢了。你翻遍四个服务的日志,熬到天亮才从某个角落的报错堆里拼出真相:原来是支付回调超时,订单服务没等到结果就回滚了。这种经历,每个微服务开发者都刻骨铭心。 让我用一座“大型快递分拣中心”来比喻微服务世界。你的每一个用户请求,就像一件从电商仓库发出的包裹。前端是揽件网点,订单服务是第一个分拣站,支付服务是路上的押运车,库存服务是远处的仓库。包裹(请求)要经过多个站点才能送达。问题来了:当包裹丢了,你怎么知道它丢在哪条传送带上?谁在哪个环节掉了链子? 这时候就需要“运单追踪系统”——快递行业的分布式追踪。而 Jaeger 和 Zipkin,就是微服务世界里两套最著名的追踪系统。它们给每个包裹(请求)贴上一张“运单号”(Trace ID),在每个站点(服务)安装“扫码枪”(埋点),记录包裹何时进站、何时出站、停留了多久。本文就用这套“快递追踪”的比喻,带你搞懂 Jaeger 和 Zipkin 的架构原理、接入方式和功能差异,帮你在 Go 和 Java 微服务项目中快速落地。
学会分布式追踪,你就能把“请求在微服务间的每一跳”变成一张清晰的时间线图,排查线上故障从“翻日志大海捞针”升级为“扫一眼链路图精准定位”。这是微服务开发者从“会写代码”走向“能扛生产”的必修技能。
可视化演示
展示分布式追踪核心概念:Jaeger架构数据流(App→Agent→Collector→Storage→UI)、Span结构与Trace树形关系、以及三种采样策略(Constant/Probabilistic/Ratelimiting)的对比。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+captions.arch+'
追踪基础概念
想象你是一家快递公司的运营主管。每天有成千上万个包裹进出分拣中心,客户打电话投诉“我的包裹怎么还没到”,你手忙脚乱地翻纸质单据,却拼不出包裹完整走过的路线。直到有一天,公司引入了一套运单追踪系统:每个包裹从揽件起就贴上一张独一无二的运单号,每个分拣站扫码枪“嘀”一声记录进出时间。这套系统,就是分布式追踪的雏形。
在微服务世界里,一个用户请求从进入前端服务开始,到最终返回结果,中间可能会调用订单服务、支付服务、库存服务等多个节点。我们把这整条调用路径叫做一条Trace(追踪链路),它就像包裹的“运单号”,是贯穿全程的唯一身份标识。而每一次具体的跨服务调用,叫做一个Span(跨度),它记录了一次调用的开始时间、结束时间、调用了哪个服务、耗了多少毫秒——就像分拣站扫码时记下的那行“进站时间-出站时间”记录。
要让跨服务的 Trace 串联起来,还需要上下文传播(Context Propagation)。运单号是怎么跟着包裹走的?包裹上贴了一张单子。对应到分布式系统里,服务 A 调用服务 B 时,会在 HTTP 请求头里带上 Trace ID 和 Span ID 信息,服务 B 收到请求后解析这些头信息,就知道自己属于哪条链路、父 Span 是谁。这个传递过程叫上下文传播。
回到快递公司的场景:有了这套运单追踪系统后,客户再投诉,你只需要输入运单号,屏幕上立刻显示包裹经过的每一个站点、每一段耗时。哪个站点延误一目了然。同理,当线上服务出现“订单丢单”问题,你在 Jaeger 或 Zipkin 界面输入 Trace ID,整条调用链的每一跳耗时和状态都摆在眼前——这就是追踪系统的核心价值:让微服务调用链路从“黑盒”变成“透明玻璃盒”。
- Trace 的定义一条完整调用链由多个 Span 组成,它们共享同一个 Trace ID,就像一件包裹的所有扫码记录共享同一张运单号。
- Span 对(Trace ID, Span ID)每个 Span 用 Span ID 唯一标识自己,并记录 Parent Span ID 指向父调用,建立树形结构。
- 上下文传播机制调用下游服务时,通过 HTTP Header 传递 \text{traceparent} 字段,携带 Trace ID 与 Parent Span ID。
两大工具简介
分拣中心要上运单系统,市面上有两套方案:一套是老牌物流公司自研的经典系统,稳定可靠、文档齐全、业内大量公司在用;另一套是互联网新贵开发的系统,功能更丰富、界面更现代、还自带了一些智能分析能力。你会怎么选?这就要分别认识我们文章的主角——Jaeger 和 Zipkin。
Zipkin 是分布式追踪领域的“老牌经典”,由 Twitter 在 2012 年开源,灵感来源于 Google 的 Dapper 论文。它奠定了分布式追踪的基本数据模型:Trace 和 Span 的概念就是 Dapper 论文提出的,Zipkin 是这套模型的忠实实现者。在 Java 生态中,Spring Cloud Sleuth 默认对接 Zipkin,很多遗留系统已经在用它。Jaeger 则是 2017 年由 Uber 开源的新锐,灵感同样来自 Dapper,但它在架构设计上更现代化,采用了更灵活的后端组件拆分,支持 OpenTelemetry 标准,并已成为 CNCF 的毕业项目。
两者的“血缘关系”很有意思:它们都源于 Google Dapper 这篇论文,就像两个师出同门的师兄弟,核心概念一脉相承,但在具体实现和生态上各有侧重。Zipkin 更偏“轻量务实”,Jaeger 更偏“功能全面”。至于 CNCF(云原生计算基金会),Jaeger 是 CNCF 毕业项目,Zipkin 则处于 CNCF 沙箱阶段,这意味着 Jaeger 在云原生社区有更活跃的维护和更完整的治理体系。
| 特性 | Jaeger | Zipkin |
|------|--------|--------|
| 出生年份 | 2017 | 2012 |
| 出身 | Uber | Twitter |
| CNCF 阶段 | 毕业项目 | 沙箱项目 |
| 存储默认 | Elasticsearch/Cassandra | Cassandra/ES |
| UI 体验 | 功能丰富,带依赖分析 | 简洁轻量 |
选型的核心考量是什么呢?如果你们团队是 Java/Spring Cloud 技术栈,Zipkin 和 Spring Cloud Sleuth 的集成方案成熟稳重;如果你们是 Go 微服务、云原生基础设施(Kubernetes)较重,或需要 OpenTelemetry 的标准支持,Jaeger 显然是更顺应趋势的选择。就像快递公司选系统:老牌系统稳,但新系统可能更适合你的自动化分拣线。
架构存储对比
快递分拣中心的运单系统,背后有“扫码枪”(客户端探针)、“数据汇总中心”(Collector)和“运单数据库”(Storage)。Jaeger 和 Zipkin 的架构都遵循这个基本模式,但组件划分和存储选型各有千秋。
先看数据流路径。两者都采用“客户端埋点 → 上报后端 → 存储 → UI 查询”的基础链路。Zipkin 的典型架构是:服务通过 Brave/Finchley 等库产生追踪数据,直接 HTTP 或 Kafka 上报到 Zipkin Server,Zipkin Server 内置了 Collector、Storage 和 Query 服务,组件相对内聚,部署简单。Jaeger 则更“模块化”:它拆分成 jaeger-agent(本地代理)、jaeger-collector(收集器)、jaeger-query(查询服务)和 jaeger-ingester(摄入器)等多个组件,每个组件可以独立扩展,适合大规模部署。
存储选型是两者最显著的差异。Zipkin 的默认存储是 Cassandra,也支持 Elasticsearch 和 MySQL,但官方对 MySQL 的定位是“仅用于开发环境”。Jaeger 的默认存储是 Elasticsearch,也支持 Cassandra 和 Badger(单机模式)。从这个角度看,如果你的公司已经在用 Elasticsearch 做日志检索,选 Jaeger 可以复用现有的 ES 集群——就像分拣中心已经有了一套数据库系统,新运单系统直接对接就能省一大笔成本。
数据模型上,Zipkin 的模型更简洁:Span 包含 traceId、parentId、id、name、timestamp、duration 等字段。Jaeger 在兼容 Zipkin 模型的基础上,扩展了 Process(进程信息)和 Tags 的语义化支持,能更精细地描述“哪个服务的哪个实例”产生的追踪数据。Jaeger 还内置了依赖分析功能,能自动生成服务调用依赖图,这一项是很多团队选 Jaeger 的重要原因。
回到快递分拣中心:Zipkin 像是一台“一体化运单查询机”——买回来插上电就能用,适合中小型分拣站;Jaeger 像是一套“模块化智能分拣流水线”——需要分别搭建扫码枪系统、数据汇总层和查询大屏,但每个模块坏了可以单独修、流量大了可以单独扩,适合每天处理百万包裹的大型枢纽。架构选择没有绝对的对错,只有适配你的业务规模与否。
- Zipkin 单体模型Zipkin Server 是一个可执行 Jar 包,内置收集、存储、查询三大模块,一条命令即可启动。
- Jaeger 组件化模型Jaeger 将功能拆分为 Agent(本地代理)、Collector(收集)、Query(查询)等多个独立组件。
- 存储选型差异Jaeger 默认使用 Elasticsearch,Zipkin 默认使用 Cassandra,存储选型影响运维成本。
接入方式指南
选好了运单系统,接下来是给每个分拣站安装扫码枪。这是落地环节最核心的一步——埋点接入。不同开发语言有不同的“扫码枪型号”:Go 语言用 Jaeger 官方客户端,Java 微服务用 Spring Cloud Sleuth 或 Brave。我们逐个来看。
Go 语言接入 Jaeger是最直观的路径。Jaeger 官方提供了 jaeger-client-go 库,它实现了 OpenTracing API。你只需要在 main 函数里初始化一个 Tracer(追踪器),然后通过 opentracing.StartSpanFromContext 在需要追踪的函数中创建 Span。Go 的 net/http 中间件模式让接入变得非常优雅——写一个 HTTP 中间件,自动为每个请求创建根 Span,并把 Trace ID 注入到请求头中传给下游。整个过程就像给快递分拣站的进站口装了一个固定式扫码枪,所有经过的包裹自动扫码,不用改造分拣线本身。
Java 微服务接入则更“省力”。在 Spring Cloud 生态中,你只需要在 pom.xml 里引入 spring-cloud-starter-sleuth 和 spring-cloud-sleuth-zipkin 依赖,Spring Boot 会自动为所有 HTTP 请求、数据库操作、消息队列发送创建 Span。这是因为 Spring 的拦截器机制就像在分拣站的每个传送带节点都预装了扫码枪,你只告诉它“运单系统地址在哪”(zipkin base-url),剩下的活它全包了。这种方式叫“无侵入式探针”,改造成本几乎为零。
如果你的系统不是 Spring 生态,或者需要追踪自定义的业务逻辑(比如某个核心算法函数),就需要手动埋点(Manual Instrumentation)了。手动埋点的核心是两条 API:startSpan 创建一个新的 Span,span.finish() 结束一个 Span。在 Go 里用 span := tracer.StartSpan("processOrder"),在 Java 里用 Span span = tracer.nextSpan().name("processOrder").start()。手动埋点就像给特殊包裹贴上“加急”标签——扫码枪还是那些扫码枪,但你能针对特定包裹做标记追踪。
给分拣站的扫码枪装好后,你还需要培训员工“什么包裹要扫、什么时候扫、扫码后看什么”。对应到微服务,就是接下来要讲的“采样配置”。扫码枪装好了,但每件包裹都扫码会产生海量数据——我们需要聪明的策略决定“哪些包裹必须扫、哪些可以抽查”。
- 发送端初始化 Tracer在服务启动时创建 Tracer 对象并设为全局单例,所有埋点操作都基于它。
- 接收端解析请求头服务收到请求后,从 HTTP Header 中提取 traceparent 字段,恢复出 Trace 上下文。
- 创建子 Span 并绑定父子关系在处理业务逻辑处创建子 Span,通过 ChildOf 关系挂载到父 Span 上。
- 结束 Span 并上报业务逻辑结束后调用 finish,将 Span 数据异步发送给 Collector。
采样配置策略
分拣中心每天涌进百万件包裹,如果每件包裹的每一个中转节点都完整记录,数据库很快就爆了。聪明的运单系统会设置一个“采样率”:流量高峰时只记录一部分包裹的完整路径,低峰时则全量记录。这就是分布式追踪里的采样(Sampling)。
全量采样(Always Sample)是最简单的方案——所有请求都生成并上报追踪数据。它像快递公司要求快递员“每一件包裹都拍照上传”。优点是数据完整,排查任何一条链路都有据可查;缺点是存储成本高、性能损耗大。在低并发系统或对数据完整性要求极高的核心链路(如支付),可以全量采样。
固定比率采样(Probabilistic Sampling)是最常用的策略——比如设置 0.1,即只有 10% 的请求会被记录。像快递分拣中心规定“每 10 件包裹抽 1 件做路径核验”。Jaeger 和 Zipkin 都支持这个策略,配置方式也很简单:Jaeger 初始化 Tracer 时 sampler.Type = "probabilistic", sampler.Param = 0.1;Zipkin 的 Brave 也支持 Sampler.create(0.1f) 这样的采样率设置。这个方案的优点是成本可控、实现简单,适合大多数业务场景。
动态采样(Rate Limiting / Adaptive Sampling)则是更高级的方案。Jaeger 提供了自适应采样(Adaptive Sampling)机制:它会根据每个服务的流量动态调整采样率——低流量服务自动全量采样,高流量服务自动降低采样率。这就像分拣中心的高峰期智能调度:流量小的时候每件包裹都详细记录,流量大的时候只抽查一部分。Zipkin 也支持速率限制采样(Rate Limiting Sampler),但不如 Jaeger 的自适应采样智能。
回到快递公司的比喻,采样策略的本质是“花多少钱换多少安心”。全量采样就是花钱买 100% 的放心;固定比率是花 10% 的钱买 90% 的覆盖面;动态采样则是“保险公司精算师”——根据风险灵活调整保额,花最少的钱覆盖最需要保障的部分。你在设置采样率时,心里要有一杆秤:哪些链路丢了不可接受(如支付),哪些链路丢了无所谓(如查询类接口)。
- 固定比率采样公式以概率 p 决定是否采样,p=1 表示全量,p=0.01 表示 1% 采样。
- 速率限制采样公式每秒最多采样 N 条链路,超出部分的追踪数据被丢弃。
- 动态采样核心思路根据服务 QPS 动态调整采样率 r,QPS 越高 r 越低。
链路查询技巧
系统装好了、埋点也埋了、采样配好了,现在到了最有价值的环节——查链路。快递公司客服用运单号查询包裹路径,你在 Jaeger/Zipkin 界面上用各种条件检索链路。学会查询,排障效率才能翻倍。
按服务名检索(Service Name Search)是最基础的查询方式。在 Jaeger UI 中,你可以选择某个服务名(如 order-service),查看该服务的所有链路列表,按耗时排序。这就像快递公司客服输入“北京分拣中心”,查看该站点所有处理过的高风险包裹。当你怀疑某个服务成为瓶颈时,直接查它就对了。
按标签过滤(Tag Filter)是精细化的查询手段。埋点时可以给 Span 打 Tag,比如 http.status_code=500、error=true、order_id=12345。在 Zipkin 的查询框里输入 error=true,就能筛出所有出错的链路。这是快递公司用“是否破损”“是否超时”等标签筛选问题包裹——比一个个翻单据高效得多。
慢链路定位(Slow Trace Analysis)是性能优化的利器。Jaeger 和 Zipkin 的 UI 都支持按耗时排序,找出 p99 以上的慢链路。在 Jaeger 的“Trace“页签里,瀑布图会直观展示每个 Span 的耗时,哪一段是红色(耗时最长)哪一段就是性能瓶颈。Zipkin 的”依赖关系图(Dependencies)”则能展示服务间的调用拓扑,一眼看出哪个服务被调用的次数最多、耗时占比最大。
回到快递分拣中心:当客户投诉包裹延误,你不再需要翻遍各站点的手写记录。输入运单号,屏幕弹出这条链路的完整时间线——“在北京分拣中心停留了 8 小时,比平均多 6 小时”。你立刻知道问题出在北京站点。分布式追踪的链路查询,就是给微服务世界的每个请求配了一位“专属客服”,随时可以调出它的完整旅程记录。
- 服务名过滤通过 service.name 字段定位到具体服务,查看该服务的所有 Span。
- 标签过滤对 Span 的 Tags 条件过滤,如 \text{http.status\_code} = 500 只保留出错调用。
- 耗时排序定位瓶颈按 Duration 排序后,最大 Duration 的 Span 即性能瓶颈所在。
- 依赖图分析聚合服务间 Span 的父子关系,生成调用拓扑图,发现链路热点。
AI 应用场景
场景一:微服务链路排障与 AI 辅助根因分析
阶段:可观测性数据分析与智能诊断
解决的问题:微服务架构中,一个请求跨 5-8 个服务,故障难以定位。传统方式是运维人员逐个服务查日志,一个请求涉及多个服务日志,平均排障时间(MTTR)长达数小时。引入 Jaeger/Zipkin 后,调用链数据被结构化采集,再结合 AI 异常检测模型,可自动识别链路上的异常 Span。
具体案例:某电商平台大促期间,支付成功率突然下降。通过 Zipkin 采集全部支付链路的 Span 数据,AI 模型实时分析链路的耗时分布,发现“支付服务→银行网关”段的耗时比基线高出 3 倍且伴随错误码。AI 自动为每条异常链路标注根因标签,运维人员在 Jaeger UI 中一键查看异常链路瀑布图,锁定银行网关超时。整个排障从传统的 2 小时缩短到 15 分钟。
场景二:AI 驱动的全链路调用链异常检测
阶段:无监督学习与链路特征聚类
解决的问题:人工巡检链路耗时只能发现明显的慢调用,对隐性的链路异常(如间歇性超时、缓存穿透、连接池耗尽前的延迟上升)难以察觉。通过将 Jaeger/Trace 数据转为时序特征向量,用 AI 聚类算法发现偏离正常模式的链路形态。
具体案例:某 Go 微服务团队使用 Jaeger 采集全量链路数据后,基于 Span 的耗时、状态码、服务依赖关系构建特征向量,使用孤立森林(Isolation Forest)算法检测异常链路。系统在某次发版后发现“订单服务→库存服务”的调用链出现锯齿状延迟波动,AI 自动标记为异常簇并推送告警,开发团队据此定位到新建连接池配置参数不当的问题。这套 AI 检测系统准确率达 92%,比人工配置阈值告警提前 40 分钟发现风险。
Python 演示
import jsonimport randomimport timefrom collections import defaultdict class Span: def __init__(self, trace_id, span_id, parent_id, service, operation, start_ms, duration_ms): self.trace_id = trace_id self.span_id = span_id self.parent_id = parent_id self.service = service self.operation = operation self.start_ms = start_ms self.duration_ms = duration_ms def to_dict(self): return { "traceId": self.trace_id, "spanId": self.span_id, "parentId": self.parent_id, "service": self.service, "operation": self.operation, "startMs": self.start_ms, "durationMs": self.duration_ms, } class Tracer: def __init__(self, service_name, sample_rate=1.0): self.service_name = service_name self.sample_rate = sample_rate self.span_list = [] def is_sample(self): return random.random() <= self.sample_rate def start_span(self, trace_id, span_id, parent_id, operation, duration_ms): span = Span( trace_id=trace_id, span_id=span_id, parent_id=parent_id, service=self.service_name, operation=operation, start_ms=int(time.time() * 1000), duration_ms=duration_ms, ) if self.is_sample(): self.span_list.append(span) return span def build_trace_data(): tracer = Tracer("order-service", sample_rate=0.5) trace_id = "abc123" root = tracer.start_span(trace_id, "span-1", None, "POST /orders", 120) span_2 = tracer.start_span(trace_id, "span-2", root.span_id, "call-pay", 80) span_3 = tracer.start_span(trace_id, "span-3", span_2.span_id, "deduct-stock", 35) return tracer def query_trace(span_list, service=None, error_tag=None): result = [] for span in span_list: if service is not None and span.service != service: continue if error_tag is not None and error_tag not in span.operation: continue result.append(span.to_dict()) return result def build_dependency_graph(span_list): graph = defaultdict(int) for span in span_list: if span.parent_id is not None: key = (span.parent_id, span.span_id) else: key = ("ROOT", span.span_id) graph[key] += 1 return dict(graph) def main(): tracer = build_trace_data() print("== 原始追踪数据 ==") for span in tracer.span_list: print(json.dumps(span.to_dict(), ensure_ascii=False)) print("\n== 按服务查询 ==") for item in query_trace(tracer.span_list, service="order-service"): print(item["operation"], item["durationMs"], "ms") print("\n== 依赖计数 ==") for key, count in build_dependency_graph(tracer.span_list).items(): print("edge", key, "count=", count) if __name__ == "__main__": main()
span_list = []用列表存储所有采集到的 Span,对应 Zipkin 的内存存储模型。if self.is_sample()采样判断:只有随机数小于采样率的 Span 才会被记录,对应概率采样策略。self.span_list.append(span)采样通过的 Span 存入列表,模拟上报到 Collector 前的缓存。span_2 = tracer.start_span(trace_id, "span-2", root.span_id, ...)通过父 Span ID 建立父子关系,对应子 Span 的 ChildOf 关联。query_trace(tracer.span_list, service="order-service")按服务名过滤查询链路,是 Jaeger UI 最基础的检索功能。build_dependency_graph(span_list)根据父子关系聚合出依赖图,对应 Zipkin 的 Dependencies 界面。
常见误区
误区我一直以为上了分布式追踪系统,日志系统就可以淘汰了。既然 Jaeger 能查到每个请求的完整调用链,还要 ELK 干嘛?直接全用追踪不就行了?
正解这是对可观测性体系最大的误解。分布式追踪记录的是“请求的路径和耗时”,它回答的是“这次请求经过了哪些服务、哪一步慢”。但日志回答的是“某个服务内部的具体错误堆栈是什么、上下文变量值是多少”。追踪就像快递运单轨迹,告诉你包裹到哪一站卡住了;日志是包裹被打开后的货物清单,告诉你里面的东西坏了没有、坏在哪。生产实践中二者互补:先用追踪定位到瓶颈服务,再查那个服务的日志定位具体代码行。
为何常见:新手刚接触追踪工具时,被它的“全链路可视化”震撼,容易产生“一工具包打天下”的错觉。加上 Jaeger 界面确实华丽,很多人误以为它就是微服务可观测性的全部。
误区追踪数据这么重要,我当然要把采样率设成 100% 全量采样。不然到时候排查问题时发现某个请求没被采样,不就抓瞎了吗?多花点存储费没关系。
正解全量采样在大多数场景下是不可持续的。设想快递公司每天处理 100 万件包裹,如果每件包裹的每个中转站都拍 10 张高清照片,库房再大也装不下。微服务也一样,一个高并发系统的请求量每秒数万,全量采样意味着巨大的存储开销和性能损耗——数据上报本身就会占用网络带宽和应用 CPU。业界实践普遍是核心支付链路 100% 采样,普通查询链路 1%-10% 采样。实在担心遗漏,可以用 Jaeger 的“尾部采样”(Tail Sampling)机制:链路结束后按条件决定是否保留完整数据。
为何常见:初学者从“单机调试”的思维出发,总觉得数据丢失是灾难。他们没有经历过大规模生产环境的存储成本和性能损耗,低估了追踪数据的数据量级。
误区既然都能做分布式追踪,那我只要选一个学就够了。反正功能差不多,Jaeger 是最新的,那我直接学 Jaeger 就行,Zipkin 不用看了。
正解两者确实功能重叠度高,但“替代”关系是片面认知。首先,如果你维护的是 Spring Cloud 技术栈遗留系统,Spring Cloud Sleuth 对 Zipkin 集成最成熟,强行迁移 Jaeger 改造成本不小。其次,Zipkin 作为 Dapper 论文最忠实、最轻量的实现,在数据模型上有很高的学习价值,理解了 Zipkin 的 Span 模型,再学任何追踪工具都是举一反三。最后,两者通过 OpenTelemetry 标准可以互通——很多团队用 Zipkin 的协议上报数据,Jaeger 也能接收 Zipkin 格式的数据。
为何常见:互联网技术文章喜欢用“XX 取代 YY”的标题党风格,让新手误以为新技术就是对旧技术的完全否定。实际上在工程实践中,新旧工具长期共存才是常态。
误区埋点不就是调一个 startSpan 方法吗?我在每个函数开头加一行 `span := tracer.StartSpan("funcName")`,然后函数结尾加一行 `span.Finish()`,这有什么难的?
正解手动埋点最大的坑不在埋点本身,而在上下文传播。如果 A 服务调用了 B 服务,但 B 服务没有从请求头里解析 Trace 上下文,那 B 服务产生的 Span 就是一条孤立链路,无法与 A 服务串起来,你的 UI 上看到的是一堆断裂的片段。另一个常见问题是忘记在异步协程/子线程中传递 SpanContext,导致并发场景下链路错乱。快递员光顾着给包裹贴单子,却忘了告诉下一站“这张单子对应哪件包裹”,那追踪就断了。
为何常见:教程里展示的都是单进程内的演示代码,没有展示跨服务、跨线程的上下文传递细节。初学者以为“加两行代码”就完成了埋点,忽略了追踪是一个全链路协作体系。
面试题
Q1. 在分布式追踪中,一条完整的调用链被称为什么?
- A.Transaction
- B.Span
- C.Trace← 正确答案
- D.Process
答案:C。一条完整调用链称为 Trace(追踪链路),它贯穿一个请求经过的所有服务节点,相当于快递的运单号。Span 是 Trace 中的一个跨服务调用片段;Transaction 是数据库概念;Process 是进程概念。注意区分:Trace 是整体,Span 是整体中的一段。
Q2. 下列哪个组件负责在服务启动时创建追踪器(Tracer)实例?
- A.Collector
- B.Storage
- C.Agent
- D.客户端埋点← 正确答案
答案:D。客户端埋点(即应用代码中的初始化逻辑)负责创建 Tracer 实例,并注入服务名、上报地址、采样策略等配置。Collector 负责收集数据,Storage 负责存储,Agent 是本地代理转发数据。类比:快递系统中,是“分拣站的操作员”(客户端埋点)负责激活扫码枪,而不是数据汇总中心。
Q3. Jaeger 和 Zipkin 的共同起源是哪篇论文?
- A.MapReduce
- B.Google Dapper← 正确答案
- C.BigTable
- D.Raft
答案:B。Jaeger 和 Zipkin 的设计灵感都来自 Google 在 2010 年发表的 Dapper 论文,这篇论文首次系统阐述了分布式追踪的 Trace 和 Span 数据模型。MapReduce 和 BigTable 是 Google 的其他分布式系统论文,与追踪无关;Raft 是共识算法论文。
Q4. 关于 Zipkin 和 Jaeger 的存储选型,下列说法正确的是?
- A.两者默认存储都是 Elasticsearch
- B.Zipkin 默认存储是 Elasticsearch,Jaeger 默认是 Cassandra
- C.两者默认存储都是 Cassandra
- D.Jaeger 默认存储是 Elasticsearch,Zipkin 默认是 Cassandra← 正确答案
答案:D。Jaeger 的默认存储是 Elasticsearch(也支持 Cassandra/Badger),Zipkin 的默认存储是 Cassandra(也支持 ES/MySQL)。这就像两家快递系统对接了不同的仓库软件。如果公司已有 ES 集群,选 Jaeger 可复用基础设施,这也是选型的重要考量之一。
Q5. 在 Go 语言中,要通过 HTTP 中间件创建根 Span,需要从哪个对象中提取 Span 上下文?
- A.响应体
- B.请求头← 正确答案
- C.文件系统
- D.数据库连接
答案:B。在 Go 的 HTTP 中间件中,追踪上下文通过 HTTP 请求头传播,通常使用 opentracing.GlobalTracer().StartSpanFromContext 从请求的上下文(context.Context)中提取。上游服务的 Trace ID 和 Parent Span ID 通过请求头传递,中间件解析并恢复 SpanContext,类似于快递包裹上的运单号跟随包裹到达每个站点。
Q6. 关于 Jaeger 的自适应采样(Adaptive Sampling),下列说法错误的是?
- A.低流量服务自动采用全量采样
- B.高流量服务自动降低采样率
- C.根据服务的历史 QPS 动态调整
- D.所有服务统一采用固定采样率← 正确答案
答案:D。自适应采样是 Jaeger 针对“不同服务流量差异大”设计的动态采样策略。它根据每个服务实时的 QPS 动态调整采样率:低流量服务全采,高流量服务降采,不需要人为设定固定值。选项 D 描述的“所有服务统一固定采样率”恰恰是自适应采样要解决的问题,因此 D 错误。这就像快递中心根据各站点实时货量调整抽检比例。
Q7. 下列哪些属于分布式追踪的核心概念?
- A.Trace← 正确答案
- B.Span← 正确答案
- C.Context Propagation← 正确答案
- D.Load Balancer
答案:ABC。Trace(追踪链路)、Span(跨度)、Context Propagation(上下文传播)是分布式追踪的三大核心概念。Trace 是完整调用链,Span 是其中一段调用,上下文传播负责 Trace 信息在服务间传递。Load Balancer(负载均衡)是流量调度组件,不属于分布式追踪的核心概念。
Q8. 下列关于 Zipkin 与 Jaeger 差异的描述,正确的有哪些?
- A.Zipkin 诞生时间早于 Jaeger← 正确答案
- B.Jaeger 是 CNCF 毕业项目,Zipkin 处于沙箱阶段← 正确答案
- C.Jaeger 采用一体化单体架构
- D.Zipkin 的默认存储是 Cassandra← 正确答案
答案:ABD。Zipkin 2012 年诞生,Jaeger 2017 年诞生,A 正确;Jaeger 是 CNCF 毕业项目,Zipkin 是沙箱项目,B 正确;Jaeger 是模块化组件架构(Agent+Collector+Query+Ingester),不是单体架构,C 错误;Zipkin 默认存储是 Cassandra,D 正确。这就像老牌快递和新锐快递的系统架构差异。
Q9. 在 Zipkin 的数据模型中,span 之间的父子关系是通过哪个字段关联的?
- A.traceId
- B.parentId← 正确答案
- C.serviceName
- D.duration
答案:B。Zipkin Span 模型中通过 parentId 字段指向父 Span 的 ID,没有 parentId 的 Span 是链路根节点。traceId 用于标识整条链路(所有 Span 共享),serviceName 用于标识服务名,duration 是耗时。类比快递物流:运单号共享(traceId),每站记录的前一站单号(parentId)把各站记录串成序列。
Q10. 关于分布式追踪中的采样策略,下列说法正确的有哪些?
- A.全量采样适用于核心支付链路← 正确答案
- B.固定比率采样适合高并发查询接口← 正确答案
- C.动态采样可以降低存储成本← 正确答案
- D.采样率越高系统性能损耗越小
答案:ABC。核心支付链路需要 100% 数据完整性,适合全量采样,A 正确;高并发查询接口用固定比率采样(如 1%)平衡成本与覆盖,B 正确;动态采样按流量调整采样率,能有效控制存储成本,C 正确;D 说反了,采样率越高,上报数据量越大,性能损耗越大。采样本质是在“数据完整”和“成本性能”之间做权衡——就像快递公司决定哪些包裹值得花成本去追踪。