想象你是一位快递公司的运单调度员。每天有成千上万个包裹从仓库发出,要经过分拣、干线运输、中转场、末端网点,才能到达用户手中。当一个用户打来电话抱怨“我的包裹卡了三天没动”,你要如何快速定位快递卡在了哪个环节?方法很简单——每个包裹都带一个唯一的运单号,每到一站就扫码记录。这,就是物流界的“分布式追踪”。 在微服务架构里,一个用户请求(包裹)会穿越网关、认证、订单、支付、库存等多个服务(中转站)。如果请求变慢或报错,与其在各台服务器日志里大海捞针,不如给这个请求也打一个“运单号”(Trace ID),让它在每个服务里留下“扫码记录”(Span)。把这些记录串起来,就得到一条完整的调用链(Trace)。Jaeger 和 Tempo 就是两套成熟的“运单查询平台”,让你像查快递一样排查线上问题。本文就用这个“快递物流”的比喻,带你从零看懂分布式追踪的每一块拼图。

岗位关联

分布式追踪是云原生可观测性三支柱中最能直接还原“一次请求全貌”的技术。掌握 Jaeger/Tempo 的架构与原理,是 SRE、全栈工程师与云原生架构师定位线上故障、优化调用链、建设追踪平台的必备核心能力。

目录
  1. 可视化演示
  2. 追踪基础概念
  3. 数据采集原理
  4. Jaeger架构
  5. Tempo架构
  6. 存储与检索
  7. 采样策略
  8. 性能优化
  9. 实践落地
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

以Span/Trace数据模型展示一次请求的完整调用链,演示采样策略的成本控制,以及traceparent头跨服务传播的过程,可切换三种模式观察。

本节配有交互动效(进入视口后自动加载,约 3KB)。

链路模型:Span与Trace Trace是完整请求,由多个Span组成调用树 每个Span含操作名、起止时间与父Span引用 TraceId 全局唯一,SpanId 链内唯一 同一TraceId串联前后端全部调用。

追踪基础概念

先回到快递的比喻。假设你在淘宝下单,快递公司给包裹贴了一张运单号。此后包裹每经过一个中转站,工作人员就扫码一次,录入“到达时间、离开时间、当前站点”。当运单号被查询时,你就看到一条完整的轨迹:杭州揽收 → 上海分拣 → 北京配送。这条轨迹,就是物流界的 Trace;而其中每一站的扫码记录,就是一个 Span。

在微服务架构中,前端请求到达网关后,会依次调用认证、订单、支付、库存等服务。分布式追踪系统为这条请求分配一个全局唯一的 Trace ID(运单号),每进入一个服务就创建一个 Span(扫码记录),记录服务名、操作名、开始时间、耗时,以及它的“上一站”是谁(父 Span ID)。将这些 Span 按照父子关系串起来,就得到一棵“追踪树”。

一棵追踪树可以用数学形式描述:它由一个 Span 集合和一组父子边构成。根节点对应入口请求,叶子节点对应最底层的数据库或缓存调用。

T=(S,E)\displaystyle T = (S, E)

1. S 集合:表示一条 Trace 中所有工作单元记录的集合,每个元素是一个 Span。

S={span0,span1,…,spann−1}\displaystyle S = \{\text{span}_0, \text{span}_1, \ldots, \text{span}_{n-1}\}

2. E 边集合:表示 Span 之间父子调用关系的集合,一条边说明“谁是调用方、谁是被调用方”。

E={(spani,spanj)∣spani 是 spanj 的父节点}\displaystyle E = \{(\text{span}_i, \text{span}_j) \mid \text{span}_i \text{ 是 } \text{span}_j \text{ 的父节点}\}

3. T 整体:由 S 和 E 共同构成一棵有向树,描述一次请求从入口到出口的完整路径。

T=(S,E)\displaystyle T = (S, E)

所以,当用户说“下单接口好慢”时,你只要在追踪系统里输入 Trace ID,就能像查快递一样,看到请求在哪个服务停留最久、哪一段调用失败。这就是分布式追踪提供的第一个核心价值:全链路可见。

数据采集原理

快递员不会凭记忆在终点站复述包裹走过哪些站,而是每到一站就扫码录入。对应到代码世界,这个“扫码”动作就是埋点。当今的主流标准是 OpenTelemetry:它提供 SDK,可以在 HTTP 请求进来时自动创建 Span,也会在调用下游服务时自动把追踪信息“粘”在出站请求上。

请求从服务 A 调到服务 B,仅仅在 A 里创建 Span 是不够的,B 必须知道“自己属于哪条链路、A 的 Span 是谁”。这就是上下文传播(Context Propagation)。物流界的做法是随包裹携带一张“交接单”,上面写着运单号、上一站编号、时间戳;微服务界的做法是在 HTTP Header 或消息队列的 Metadata 中携带同样的信息。

Ci=(traceID,spanID,parentSpanID)\displaystyle C_i = (\text{traceID}, \text{spanID}, \text{parentSpanID})

1. traceID:整条链路的全局唯一标识,类似快递运单号,贯穿所有服务。

traceID∈{0,1}128\displaystyle \text{traceID} \in \{0, 1\}^{128}

2. spanID:当前工作单元的唯一标识,相当于本站点扫码产生的流水号。

spanID∈{0,1}64\displaystyle \text{spanID} \in \{0, 1\}^{64}

3. parentSpanID:指向父 Span 的指针,让服务 B 能把自己挂到树上正确的位置。

parentSpanID=spanIDparent\displaystyle \text{parentSpanID} = \text{spanID}_{\text{parent}}

实际落地时,OpenTelemetry 的自动埋点已经覆盖了主流框架(Spring Boot、Gin、Express 等),它会在 RPC 调用的 Header 中写入 W3C 标准的 traceparent 字段。就像快递公司统一了“交接单”格式,各站点不用协商也知道怎么读。上下文传播是整棵追踪树能够“从根长到叶”的血管。

Jaeger架构

如果说分布式追踪是一套“快递运单系统”,那么 Jaeger 就是一套典型的“总部集中式”平台。它的架构可以拆成三个角色:快递员手里的扫码枪(Agent)、网点的分拣调度中心(Collector)、以及供客户查询运单轨迹的客服查询台(Query)。

Jaeger 最初由 Uber 开源,现为 CNCF 毕业项目。应用服务通过 SDK 把 Span 上报给本机 Agent(UDP 协议),Agent 做轻量校验后批量转发给 Collector;Collector 负责大规模接收、验证、索引并写入存储后端(支持 Cassandra、Elasticsearch、Badger 等);Query 服务提供 Web UI 和 API,让你按 Trace ID、服务名、标签检索追踪数据。这样的分层设计使各组件可以独立水平扩展——“扫码枪”不会成为“分拣中心”的瓶颈。

从请求产生到查询结果返回,整个链路会产生如下端到端延迟:

Ltotal=Lsdk+Lagent+Lcollector+Lquery\displaystyle L_{\text{total}} = L_{\text{sdk}} + L_{\text{agent}} + L_{\text{collector}} + L_{\text{query}}

1. L_sdk:应用 SDK 在本地生成、缓冲 Span 的耗时,通常只有几微秒。

Lsdk=tcreate+tbuffer\displaystyle L_{\text{sdk}} = t_{\text{create}} + t_{\text{buffer}}

2. L_agent:Agent 将批量数据通过 UDP/HTTP 转发给 Collector 的网络传输时间。

Lagent=tflush+tnetwork\displaystyle L_{\text{agent}} = t_{\text{flush}} + t_{\text{network}}

3. L_collector:Collector 做解包、校验、索引并写存储的处理时间。

Lcollector=tvalidate+tindex+twrite\displaystyle L_{\text{collector}} = t_{\text{validate}} + t_{\text{index}} + t_{\text{write}}

4. L_query:查询时从存储读取并渲染结果的耗时。

Lquery=tsearch+trender\displaystyle L_{\text{query}} = t_{\text{search}} + t_{\text{render}}

回到快递比喻:扫码枪只负责“扫”,网点分拣中心负责“理”,查询台负责“答”。这种分工让 Jaeger 可以在每层按流量独立扩容,是生产环境中最常用的追踪后端之一。

Tempo架构

与 Jaeger 的“中央分拣”思路不同,Grafana Tempo 更像一家“极简物流公司”:快递员只在前端登记一个运单号,然后把包裹整个扔进仓库,查询时才按运单号去翻货。它的设计哲学是“追踪数据就是大 blob,不做深度索引,只做按 ID 查找”。

Tempo 的核心组件只有四个:Distributor(分发器)、Ingester(预写器)、Querier(查询器)和对象存储后端(S3、GCS、MinIO 等)。请求到达后,Distributor 按 traceID 哈希分片,把数据交给 Ingester 缓冲在内存中,定期刷成 block 写入对象存储。查询时,Querier 先查内存索引或近期 block,再回溯对象存储中的归档数据。

Trace[traceID]=Lookup(idxrecent∪blobsarchive)\displaystyle \text{Trace}[\text{traceID}] = \text{Lookup}\big(\text{idx}_{\text{recent}} \cup \text{blobs}_{\text{archive}}\big)

1. idx_recent:由 Ingester 维护的近期 Trace 索引,覆盖最近数小时的数据。

idxrecent={traceID→blockID}\displaystyle \text{idx}_{\text{recent}} = \{\text{traceID} \to \text{blockID}\}

2. blobs_archive:对象存储中长期归档的 block 集合,适合海量冷数据。

blobsarchive={b1,b2,…,bm}\displaystyle \text{blobs}_{\text{archive}} = \{b_1, b_2, \ldots, b_m\}

3. Lookup(·):先查索引定位 blockID,再到对应 block 中取出完整 Trace 数据。

Tempo 的选择与 Grafana 生态深度绑定:追踪数据可以直接在 Grafana 中与日志、指标联动。就像你查一年前的快递,虽然要去大仓库翻,但仓库租金便宜,长期保存也不心疼。Tempo 的“按 ID 直查”模型非常适合海量低成本追踪归档。

存储与检索

快递公司每天产生几亿条运单记录,不可能每条都放在快递柜里,更不可能全部无条件永久保存。存储设计决定了追踪系统的成本、查询速度和数据保留能力。分布式追踪的存储策略同样需要在“收入成本”与“查询体验”之间做取舍。

规划存储容量时,有五个关键变量:每秒请求量(QPS)、每条 Trace 的平均 Span 数量、单个 Span 序列化后的平均字节数、数据保留天数、以及采样率。它们相乘,就是一天的存储量。

Storageday=QPS×spans‾×size‾×86400×r\displaystyle \text{Storage}_{\text{day}} = \text{QPS} \times \overline{\text{spans}} \times \overline{\text{size}} \times 86400 \times r

1. QPS:入口服务的每秒请求数,直接决定产生多少条 Trace。

QPS=NrequestsTseconds\displaystyle \text{QPS} = \frac{N_{\text{requests}}}{T_{\text{seconds}}}

2. spans̄:每条 Trace 平均包含的 Span 数量,取决于调用链深度(微服务链路越长数值越大)。

spans‾=1N∑i=1NspanCounti\displaystyle \overline{\text{spans}} = \frac{1}{N}\sum_{i=1}^{N} \text{spanCount}_i

3. sizē:单个 Span 序列化后的平均字节数,包含属性标签、时间戳等。

size‾=1M∑j=1Mbytesj\displaystyle \overline{\text{size}} = \frac{1}{M}\sum_{j=1}^{M} \text{bytes}_j

4. 86400 与 r:一天的秒数;r 为采样率(0~1),二者共同决定实际落库总量。

检索设计上,Jaeger 和 Tempo 都按 traceID 建主索引;若要按服务名/标签搜索,则需要额外的二级索引或依赖外部系统。理解存储公式后你就知道:无脑提高采样率,等于把所有包裹都永久存在市中心仓库,查询只会越来越慢、账单越来越贵。

采样策略

质检部门不可能对每个快递都开箱检查,但也不能什么都不查。分布式追踪面对同样的困境:如果每条请求都全量上报,存储和带宽成本会呈爆炸式增长;如果随机丢弃,又可能恰好丢掉最重要的错误链路。所以需要采样策略。

最简单的方案是头部采样:在请求入口(根 Span 处)按固定概率决定整条链路是否上报。优点是开销极小,缺点是决策“盲人摸象”——不知道请求最终成不成功就直接丢弃,可能漏掉错误链路。更聪明的做法是尾部采样:请求结束后,由收集端根据结果(错误标志、延迟超标、自定义标签)决定哪些 Trace 值得保留。

P(保留链路 i)=wi∑j=1Nwj\displaystyle P(\text{保留链路 } i) = \frac{w_i}{\sum_{j=1}^{N} w_j}

1. w_i:第 i 条 Trace 的权重,由错误、延迟、标签等特征加权计算。

wi=α⋅1[error]+β⋅max⁡(0,Li−Lthr)\displaystyle w_i = \alpha \cdot \mathbf{1}[\text{error}] + \beta \cdot \max(0, L_i - L_{\text{thr}})

2. Σ w_j:采样窗口内所有候选 Trace 的权重之和,用于归一化概率。

∑j=1Nwj=w1+w2+⋯+wN\displaystyle \sum_{j=1}^{N} w_j = w_1 + w_2 + \cdots + w_N

3. 比值:权重占比越高,该链路被保留的概率越大,从而实现“重点链路全保留、普通链路低概率保留”。

像质检员优先检查投诉多的包裹一样,尾部采样把宝贵的存储资源集中在“最值得复盘”的链路上。生产实践中,头部采样控制总量,尾部采样兜底异常,两者通常搭配使用。

性能优化

快递高峰期,总部要临时增开分拣口、甩挂运输、优先派送时效件。追踪系统在高流量下同样面临“吞吐扛不住、查询乌龟速”的挑战。性能优化的目标,就是在不丢失关键数据的前提下,让上报和查询都快起来。

批量和异步是关键。SDK 端不要每产生一个 Span 就立刻同步发送,而是先在内存缓冲、批量压缩后再上报,大幅减少网络包数量。查询侧则引入并行化:把大查询拆成多个子查询并行执行,最后聚合结果。实际测算优化效果时,延迟分为不可并行的串行部分和可并行的部分:

Dopt=Dserial+DparallelK\displaystyle D_{\text{opt}} = D_{\text{serial}} + \frac{D_{\text{parallel}}}{K}

1. D_serial:无法并行必须顺序执行的耗时,如单次网络 RTT、磁盘寻址。

Dserial=trtt+tseek\displaystyle D_{\text{serial}} = t_{\text{rtt}} + t_{\text{seek}}

2. D_parallel:可拆分成子任务并行处理部分的耗时,如扫描多个 block。

Dparallel=tscan+tdecode\displaystyle D_{\text{parallel}} = t_{\text{scan}} + t_{\text{decode}}

3. K:并行度,即同时参与查询的协程/线程/机器数量。

K=min⁡(Kmax⁡,CPUquota)\displaystyle K = \min(K_{\max}, \text{CPU}_{\text{quota}})

4. D_opt:优化后的总延迟;K 越大,并行部分被摊薄得越狠。

在实际部署中,Jaeger 和 Tempo 都支持分区存储:近期热点数据放在 SSD 上,历史归档数据滚动到对象存储。这就像快递公司把热销区包裹放在最近的网点,把过季库存送到郊区仓库——热得快、冷得省。

实践落地

有了追踪平台,运维团队就像装上了“全链路监控大屏”的快递老板:用户投诉派送慢,你能直接看到是揽收慢、干线堵还是派送员绕路。在真实业务中,分布式追踪的落地场景已经非常成熟。

最直接的场景是根因定位:告警系统发现订单接口 P99 延迟飙到 2 秒,工程师打开 Grafana/Tempo 搜出几十条慢 Trace,瀑布图一眼锁定了“库存服务”对某个 Redis 分片的读耗时异常——顺着 Trace 直接找到大 Key 热区。第二个场景是 SLO 统计:分布式追踪天然记录每次请求的真实耗时,可以据此计算服务等级目标的达成率。

SLOratio=∑i=1N1[Li≤Lthreshold]N×100%\displaystyle \text{SLO}_{\text{ratio}} = \frac{\sum_{i=1}^{N} \mathbf{1}[L_i \le L_{\text{threshold}}]}{N} \times 100\%

1. 指示函数 1[·]:当第 i 次请求的耗时小于等于阈值时计 1,否则计 0。

1[Li≤Lthreshold]={1,Li≤Lthreshold0,otherwise\displaystyle \mathbf{1}[L_i \le L_{\text{threshold}}] = \begin{cases} 1, & L_i \le L_{\text{threshold}} \\ 0, & \text{otherwise} \end{cases}

2. 求和:对所有请求的指示函数值求和,得到满足阈值的请求总数。

∑i=1N1[Li≤Lthreshold]\displaystyle \sum_{i=1}^{N} \mathbf{1}[L_i \le L_{\text{threshold}}]

3. N 与百分比:N 为总请求数,比值乘以 100% 转化为百分比。

第三个场景是容量规划:基于追踪数据统计出各服务调用频率、依赖深度、平均耗时,结合预估 QPS,用存储公式和延迟模型推算所需实例数与大促资源预算。追踪不是炫技,而是让“看得见、查得清、改得准”成为日常——像管理快递网络一样管理你的微服务。

AI 应用场景

微服务调用链故障定位

阶段:在线诊断与根因分析

解决的问题:传统排查需要运维工程师登录多台服务器逐行翻日志,靠经验猜测慢在哪个环节,耗时从分钟到小时不等。接入 Jaeger/Tempo 后,输入一个 Trace ID,秒级画出调用链瀑布图,直接定位到耗时异常或报错的服务节点。

具体案例:某电商平台晚高峰订单接口 P99 延迟从 200ms 飙升至 2s。SRE 通过 Tempo 检索该接口的慢 Trace,发现“库存服务”对某个 Redis 分片的读延迟异常,进一步定位到大 Key 热区。修复后 P99 回落至 180ms。

基于追踪数据的容量规划

阶段:负载建模与容量预测

解决的问题:大促前扩容靠经验“拍脑袋”:到底加多少实例?加少了扛不住,加多了浪费钱。追踪数据提供了每次请求经过的服务清单与耗时,可以精确统计每个服务被调用的频率、依赖深度和平均处理时间,从而推算出目标 QPS 下的资源需求。

具体案例:某支付团队在双 11 前基于 Jaeger 数据做容量测算:支付网关的核心链路当前 6 个实例扛 5 万 QPS,平均耗时 85ms。按大促预估 10 万 QPS 计算,需要扩容至 12 实例;同时发现“风控服务”存在 30% 的重复调用,优化后只需 10 实例。

服务质量 SLO 度量与告警

阶段:数据聚合与合规评估

解决的问题:对外承诺“下单接口 99.9% 可用”,但缺少请求级延迟数据来量化达成情况。追踪系统天然记录每个请求的真实耗时,是计算 SLO 达成率最可靠的数据源;与告警平台联动,可自动生成工单并附上慢 Trace。

具体案例:某互联网公司以“下单接口 P99 < 800ms”为 SLO,每天从追踪数据自动统计达标率;若连续 5 分钟跌破 99.5%,自动创建告警工单并附带最近的慢 Trace 链接,值班工程师点开即可看到完整调用链。

Python 演示

python79 行
import timeimport randomimport json  class Span:    """单个追踪跨度,好比快递中转站的一次扫码记录"""     def __init__(self, span_id, parent_id, service, operation, start, duration):        self.span_id = span_id        self.parent_id = parent_id        self.service = service        self.operation = operation        self.start = start        self.duration = duration     def to_dict(self):        return {            "span_id": self.span_id,            "parent_id": self.parent_id,            "service": self.service,            "operation": self.operation,            "start": round(self.start, 4),            "duration": self.duration,        }  class Tracer:    """模拟 OpenTelemetry Tracer:为一次请求生成一条 Trace"""     def __init__(self, service):        self.service = service        self.trace_id = str(random.randint(10**15, 10**16))        self.spans = []     def start_span(self, operation, parent_id=None):        span_id = str(random.randint(10**8, 10**9))        span = Span(span_id, parent_id, self.service,                    operation, time.time(), 0)        self.spans.append(span)        return span     def end_span(self, span, duration):        span.duration = duration     def export(self):        return {            "trace_id": self.trace_id,            "spans": [s.to_dict() for s in self.spans],        }  def simulate_request():    """模拟一次下单请求:网关 -> 订单服务 -> 支付服务"""    tracer = Tracer(service="api-gateway")    root = tracer.start_span("POST /api/order")    time.sleep(0.001)     order_span = tracer.start_span("order.create", parent_id=root.span_id)    time.sleep(0.002)     pay_span = tracer.start_span("pay.deduct", parent_id=order_span.span_id)    time.sleep(0.003)     tracer.end_span(pay_span, duration=0.004)    tracer.end_span(order_span, duration=0.006)    tracer.end_span(root, duration=0.010)    return tracer.export()  if __name__ == "__main__":    traces = [simulate_request() for _ in range(3)]    print("===== 模拟分布式追踪导出 =====")    for i, trace in enumerate(traces):        print(f"Trace {i + 1}: {trace['trace_id']}")        for s in trace["spans"]:            print(f"  Span {s['span_id']} parent={s['parent_id']} "                  f"{s['service']}@{s['operation']} {s['duration']}s")        print(json.dumps(trace, ensure_ascii=False))
目的:用纯 Python 模拟一个微服务调用链的 Trace 生成与导出过程,演示 Span 的父子关系和最终 JSON 输出结构。
  1. class Span:定义单个追踪跨度,对应快递中转站的一次扫码记录。
  2. tracer.trace_id = str(random.randint(...))为整条链路生成全局唯一运单号。
  3. tracer.start_span("order.create", parent_id=root.span_id)在子服务中创建 Span 并挂到父 Span 下。
  4. tracer.end_span(pay_span, duration=0.004)记录耗时,相当于扫码后填上离开时间。
  5. tracer.export()把整条 Trace 导出为 JSON,模拟上报给 Jaeger/Tempo。

常见误区

误区我刚搭好 Jaeger,为了不漏掉任何线上问题,把采样率直接配成 1.0(100%),这样每条请求都有完整链路数据,排查时心里踏实。

正解100% 采样意味着每个请求、每个 Span 都要序列化、传输、存储。在高 QPS 场景下,存储成本随 QPS × 链路深度线性暴涨,查询也会因为数据量过大而变慢;同时 SDK 全量上报会占用应用线程和带宽,可能拖慢业务本身。生产环境通常取 5%~20% 基础采样率,再配合尾部采样补采错误链路。

为何常见:人们直觉上认为“数据越全越安全”,却忽略了追踪数据是有成本的,而且采样器和存储设计本来就是配套的取舍问题。

误区Jaeger 和 Tempo 都是分布式追踪后端,功能重叠,公司引入其中一个就够了,同时部署纯属重复建设。

正解两者可以共存甚至互补。Jaeger 自带完整 UI 和丰富存储后端,适合快速启动、中小规模场景;Tempo 与 Grafana 生态深度集成,对象存储成本低,适合海量归档。实践中常见方案是:通过 OpenTelemetry 统一出口,上游双写到两套系统,或先 Jaeger 后迁移 Tempo。

为何常见:二者确实在“追踪后端”这一能力上有重叠,容易让人误解为排他性竞争,忽略了架构理念和成本模型的差异。

误区只要接入了分布式追踪,再复杂的线上故障都能靠一个 Trace ID 查到根因,日志和指标都不太需要了。

正解Trace 只回答“请求经过哪些服务、各花了多久、哪段报错”,但崩溃现场的业务日志、CPU/内存指标、数据库慢查询等仍然需要日志和指标体系配合。可观测性三支柱各司其职:日志回答“发生了什么”,指标回答“有多严重”,追踪回答“在哪条链路上发生”。

为何常见:追踪在定位慢请求时的体验太直观,容易让人高估它的能力边界,忽略了其他信号源的不可替代性。

误区我们的服务之间都用消息队列异步通信,不走 HTTP Header,所以不用做 OpenTelemetry 的上下文传播,追不到也正常。

正解消息队列场景同样需要传播 Trace Context,只是载体从 HTTP Header 变成了消息 Header(如 Kafka Headers)或 gRPC Metadata。OpenTelemetry 对主流 MQ 客户端提供了自动注入与提取,生产者发送时写入 traceparent,消费者接收时读取并挂到同一棵树上。

为何常见:大家接触上下文传播的示例大多是 HTTP 的 traceparent 头,容易把“一种载体”误当成“唯一方式”。

面试题

Q1. 可观测性三支柱中,分布式追踪主要回答什么问题?

单选题 · easy

  • A.系统每秒处理多少请求
  • B.一次请求完整经过哪些服务← 正确答案
  • C.服务器磁盘剩余空间多少
  • D.代码库有多少行注释内容

答案:B。分布式追踪回答的是“一次请求的完整路径与各阶段耗时”,对应快递追踪的“包裹走过哪些中转站”。A 属于指标监控范畴,C 属于基础设施监控,D 与可观测性无关。

Q2. 一条 Trace 由多个 Span 组成,Span 之间的结构关系是?

单选题 · easy

  • A.兄弟节点构成的链表
  • B.父子节点构成的树形结构← 正确答案
  • C.双向循环的图状关联
  • D.毫无关联的孤岛式结构

答案:B。Span 通过 parentSpanID 形成层级关系,整条 Trace 是一棵有向树。同一层级的 Span 可能互为兄弟,但整体结构是树,不是链表、循环图或孤岛。

Q3. 下列哪个字段用于将子 Span 挂到父 Span 上?

单选题 · mid

  • A.traceID 全局链路标识
  • B.parentSpanID 父级定位标识← 正确答案
  • C.serviceName 服务归属标记
  • D.operationName 操作名称标记

答案:B。parentSpanID 指向父 Span 的唯一 ID,是新 Span 接入追踪树的关键。traceID 标识整条链路而非层级关系;serviceName 和 operationName 只是描述性元数据,不参与树结构构建。

Q4. Jaeger 组件中,负责接收应用侧 Span 并转发给 Collector 的是?

单选题 · mid

  • A.Agent 轻量代理组件← 正确答案
  • B.Query 查询服务入口
  • C.Ingester 数据写入组件
  • D.Distributor 分发调度模块

答案:A。Jaeger Agent 以 DaemonSet 方式部署在业务节点旁,通过 UDP 接收应用上报的 Span 并批量转发给 Collector。Query 负责查询展示,Ingester 与 Distributor 是 Tempo 的组件,与 Jaeger 无关。

Q5. 头部采样(Head-based Sampling)的采样决策发生在什么时候?

单选题 · mid

  • A.请求结束后由后台异步决定
  • B.根 Span 创建时一次性决策← 正确答案
  • C.每个 Span 分别独立随机决策
  • D.查询 Trace 时动态临时决策

答案:B。头部采样在请求入口(根 Span 创建时)按概率决定整条链路是否采样,并把决策结果通过上下文传播给所有子 Span。它不需要等待请求结果,开销小但可能漏掉错误请求。

Q6. Grafana Tempo 存储追踪数据主要依赖什么后端?

单选题 · hard

  • A.对象存储服务如 S3← 正确答案
  • B.单节点的本地 SQLite
  • C.无持久化能力的 Redis
  • D.进程内的临时内存队列

答案:A。Tempo 的设计核心是利用对象存储(S3、GCS、MinIO)保存全量 Trace 数据,Ingester 先把数据缓冲到内存或 SSD,随后刷成 block 落盘对象存储。SQLite、Redis 或纯内存队列在成本和持久性上都不适合海量追踪数据。

Q7. 日存储量公式估算中,QPS 变为原来 2 倍且采样率降为原来一半时,存储量如何变化?

单选题 · hard

  • A.变为原来存储的四倍
  • B.变为原来存储的两倍
  • C.保持原有存储量不变← 正确答案
  • D.缩减为原来存储的一半

答案:C。存储量与 QPS 成正比、与采样率成正比。QPS × 2、采样率 × 0.5,两者相乘恰好抵消,所以日存储量不变。这提醒我们:调低采样率可以直接对冲流量增长带来的存储压力。

Q8. 下列哪些属于常见的分布式追踪系统?

多选题 · easy

  • A.Jaeger 分布式追踪系统← 正确答案
  • B.Grafana Tempo 追踪平台← 正确答案
  • C.Prometheus 指标监控系统
  • D.Zipkin 开源追踪库成果← 正确答案

答案:ABD。Jaeger、Tempo、Zipkin 都是成熟的分布式追踪后端,属于可观测性三支柱中的 Traces 支柱。Prometheus 是指标监控系统,属于 Metrics 支柱,不负责追踪调用链。

Q9. 关于追踪上下文传播,下列哪些说法正确?

多选题 · mid

  • A.可借助 HTTP Header 传递← 正确答案
  • B.只能使用 UDP 协议传递
  • C.消息队列场景可透传← 正确答案
  • D.必须将 ID 写入数据库

答案:AC。HTTP Header(如 W3C traceparent)是最常见的传播载体,消息队列场景则通过消息 Header 透传上下文。UDP 只用于 Jaeger Agent 上报,不负责服务间传播;把 ID 写入数据库既慢又不必要。

Q10. 下列哪些方案适合“重点保留错误和慢请求链路,同时控制存储成本”?

多选题 · hard

  • A.尾部采样保留异常链路← 正确答案
  • B.头部采样随机抽检异常
  • C.错误标记触发动态补采← 正确答案
  • D.永久关闭追踪采样功能

答案:AC。尾部采样在请求结束后根据错误/延迟权重决定是否保留,天然聚焦异常链路;错误标记触发动态补采(如 OpenTelemetry 的 Tail Sampling Processor 按错误标签决策)也是成熟做法。头部采样是随机盲抽,可能漏掉异常;关闭采样则彻底失去追踪能力。