想象你是一家跨国快递公司的客服。一位客户气冲冲打来电话:“我的包裹在转运途中卡住了,到底卡在哪个环节?”你打开系统输入运单号,屏幕上立刻出现完整的旅程地图:上午 9:00 揽收、10:30 到达分拨中心、12:00 装车、14:00 海关清关……每一站的时间、操作人、异常记录全都在一条时间线上。这里的“完整旅程”就是分布式追踪中的 Trace,而每个站点的“处理回执记录”就是 Span。微服务架构就像一张庞大的快递网络:一次用户请求会穿越网关、商品服务、支付服务、数据库等多个节点,每个节点都在忙自己那一小段。如果没有一张“总运单”把它们串起来,任何一个环节变慢或出错,你都只能挨个服务器翻日志,无异于大海捞针。本文就来拆解这张数字化“总运单”的设计原理。

岗位关联

掌握 Span 与 Trace 的设计,是搭建微服务与 AI 系统可观测性的基本功。你将能快速定位“模型推理慢在哪一环”,让排障时间从小时级降到分钟级。

目录
  1. 可视化演示
  2. 核心概念:什么是 Trace 与 Span
  3. 链路构建:父子关系与树形结构
  4. 上下文传播:把“运单号”传递给下一个服务
  5. 采样策略:不是每一单都要全程稽核
  6. 标准规范:快递业为何需要统一面单
  7. 实践应用:链路分析与性能定位
  8. 设计要点:把面单填规范,链路才有用
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示OpenTelemetry Span结构、Trace上下文跨服务传播、采样策略与AI推理管道追踪,切换模式掌握分布式可观测性核心原理。

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

Span设计原则与属性建模 Span是追踪最小单元,包含操作名、起止时间戳、SpanContext(traceID+spanID)、属性(Attributes)和事件(Events) Span通过父子关系构建调用树,SpanKind标记类型(Server/Client/Internal/Producer/Consumer),Status记录结果状态 Span = {name, spanID, traceID, parentID, duration, attributes} Span建模需平衡信息丰富度与采集开销,核心属性优先

核心概念:什么是 Trace 与 Span

一单从杭州发往东京的包裹,会依次经过揽收站、分拨中心、海关、海外仓、派送员之手。每个站点都会在系统里生成一条“处理记录”,写明当前时间、耗时、是否正常。这些记录放在一起,就是包裹的完整旅程。在分布式系统里,一次用户请求(比如点击“下单”)同样会穿梭于网关、商品、支付、数据库等多个服务之间。把整条调用路径上所有处理记录按先后顺序拼起来,就得到了 Trace;而其中某一次具体处理——例如“调用支付服务花了 200 毫秒”——就是一个 Span。Span 是构成 Trace 的最小工作单元,相当于快递在某个站点的一次“收件—处理—发出”的回执。

一个 Span 至少包含:操作名称 name、全局唯一的 spanId、指向父级记录的 parentId、开始与结束时间戳 start/end、业务标签 attributes、结果状态 status。Trace 则是这些 Span 按调用关系组成的有向无环图,通常呈现为一棵树。无论一次请求经过了多少个服务,只要它们携带同一个 traceId,就能被还原成一条完整的调用链。

有了这张“总运单”(traceId)和每一站的耗时记录(duration),工程师就能像客服查快递一样,精确回放整条请求的每一段旅程。

duration=tend−tstart\displaystyle \text{duration} = t_{\text{end}} - t_{\text{start}}
这个公式表示一个 Span 的耗时:用操作结束的时刻减去操作开始的时刻,得到该处理单元消耗的时间,单位通常是毫秒。
  1. 取结束时间戳Span 处理完成的那一瞬间,SDK 会记录下 end 时间戳,标记该操作收尾完毕。
    tendt_{\text{end}}
  2. 减去开始时间戳从结束时刻减去开始处理时刻 start,得到该站点从“签收扫描”到“装车发出”的净耗时。
    duration=tend−tstart\displaystyle \text{duration} = t_{\text{end}} - t_{\text{start}}

链路构建:父子关系与树形结构

快递分拣是层层拆单的:杭州总部生成一张干线单,干线单到达分拨中心后拆成若干派送单,派送单再交给快递员。每一张下级单据都必须填写“上一级单据号”,这样任何一张回执都能追溯到它的上级。Span 之间的父子关系正是这样建立的:网关作为入口生成根 Span,调用商品服务时生成子 Span,商品服务再查询数据库又生成孙 Span。每个子 Span 的 parentId 精确指向父 Span 的 spanId,层层挂接。

当所有携带同一 traceId 的 Span 都到达追踪后端后,系统按照 parentId 把它们组织成一棵树。树的根是请求入口处的根 Span;层数越深表示调用链越长;同一父节点下的多个子节点,则代表并行发起的不同调用。整棵树的形状,就是一次请求在微服务网络中的真实足迹。

如同快递单据层层关联一样,parentId 让散落各处的回执单有了归属,工程师得以从根节点一路查到叶子节点。

Trace={s0,s1,…,sn},parent(si)=sj.spanId\displaystyle \text{Trace} = \{ s_0, s_1, \dots, s_n \}, \quad \text{parent}(s_i) = s_j.\text{spanId}
Trace 是所有 Span 的集合;除了根 Span 外,每个 Span 都能通过 parentId 找到唯一的父 Span,从而拼出一棵完整的调用树。
  1. Trace 是 Span 的集合所有携带同一 traceId 的处理记录放在一起,构成一次完整请求的调用链。
    Trace={s0,s1,…,sn}\text{Trace} = \{ s_0, s_1, \dots, s_n \}
  2. parent 函数对任意非根 Span,都能通过 parentId 找到唯一的父 Span,把散落在各服务的记录挂成一棵树。
    parent(si)=sj\text{parent}(s_i) = s_j
  3. 根 Span 无父节点整棵树有且只有一个根 Span,它就是请求进入系统的入口。
    parent(sroot)=∅\text{parent}(s_{\text{root}}) = \varnothing

上下文传播:把“运单号”传递给下一个服务

包裹从一个站点转运到下一个站点时,交接的第一件事就是扫描运单号。运单号永远跟着包裹走,下一站的记录才能挂到同一趟旅程上。微服务之间同样需要传递这种“运单号”——只是进程之间不共享内存,必须通过通信协议把 traceId 和 parentId 带给下一个服务。这个动作在分布式追踪里叫作 上下文传播(Context Propagation)。

最常见的传播介质是 HTTP 请求头。W3C 制定了 Trace Context 标准,其中定义了 traceparent 请求头:当服务 A 调用服务 B 时,A 会把当前 Span 的信息写进请求头;B 解析这个请求头,在自己的进程里创建子 Span,并把 A 的 traceId 与 A 的 spanId 记录为父关系。消息队列场景则把 traceparent 放进消息属性,gRPC 则放入 metadata,机制完全一致。

就像交接单必须随包裹走一样,traceparent 必须跟着每一次 RPC 或消息传递走,链路才能在跨进程的边界上不断延伸。

traceparent=00⏟版本−trace-id⏟32位−parent-id⏟16位−01⏟采样标记\displaystyle \text{traceparent} = \underbrace{\texttt{00}}_{\text{版本}} - \underbrace{\text{trace-id}}_{32\text{位}} - \underbrace{\text{parent-id}}_{16\text{位}} - \underbrace{\texttt{01}}_{\text{采样标记}}
traceparent 是 W3C 标准定义的链路上下文请求头:依次为版本号、全局 Trace ID、父 Span ID 和采样标记。下游服务解析这个头,就能接力续写同一段旅程。
  1. 版本号当前固定为 00,表示遵循 W3C 当前版本的 Trace Context 标准。
    00\texttt{00}
  2. trace-id32 位十六进制的全局唯一 ID,相当于快递的总运单号,一次请求只有一个。
    trace-id∈[0,2128)\text{trace-id} \in [0, 2^{128})
  3. parent-id当前调用方(即服务 A 的父 Span)的 16 位 ID,下游用它作为新 Span 的父节点。
    parent-id=spanIdparent\text{parent-id} = \text{spanId}_{\text{parent}}
  4. 采样标记flags 的最低位若为 1,表示这条链路应被记录,否则可丢弃。
    flags: sampled=1\texttt{flags}: \ \text{sampled} = 1

采样策略:不是每一单都要全程稽核

快递公司不会对每一单包裹都做“全程视频稽核”,那样成本高到无法承受。更常见的做法是随机抽取 2%~10% 的运单做重点核查,同时对用户投诉的单子 100% 核查。分布式追踪面临同样的成本问题:高流量系统如果全量记录每个请求的每个 Span,存储和 CPU 开销会迅速吞噬服务资源。采样(Sampling) 就是决定“哪一单要全程记录”的策略。

最简单的策略是固定采样率:每个新请求到达时,生成一个 0 到 1 之间的随机数,小于设定阈值 r 就记录整条链路。更精细的策略还有头部采样和尾部采样:头部采样在请求刚进入系统时就做出决定,实现简单但无法预知请求是否会失败;尾部采样先把少量数据缓存在内存中,等请求结束后根据结果决定是否保留,因此能确保所有错误请求都不被遗漏。动态采样则根据当前流量高低自动调整 r,流量低谷时多记录,高峰时少记录。

抽样稽核让快递公司用可控成本覆盖流程监督;采样则让追踪系统在高流量下仍然经济可用,把宝贵资源留给真正值得分析的请求。

sampled={1,U(0,1)<r0,otherwise\displaystyle \text{sampled} =\begin{cases}1, & U(0, 1) < r \\0, & \text{otherwise}\end{cases}
采样决策的数学表达:为每个请求生成一个均匀随机数 U,若 U 小于采样率 r 则记录链路,否则丢弃。r=0.1 意味着平均每 10 个请求只留 1 条。
  1. 生成随机数SDK 为每个新 Trace 生成一个 0 到 1 之间的均匀随机数 U。
    U∼Uniform(0,1)U \sim \text{Uniform}(0, 1)
  2. 比较采样率当 U 小于 r 时,链路被保留;否则被丢弃。
    U<r⇒sampled=1U < r \Rightarrow \text{sampled} = 1
  3. 设定采样率r=0.1 表示约 10% 的请求会留下完整链路。
    r=0.1r = 0.1

标准规范:快递业为何需要统一面单

早年间各家快递公司各用一套运单号,跨公司转运时系统无法互认,对账全靠人工。后来国家邮政推动统一面单规范,条形码一扫码就能识别出快递公司、运单号与包裹状态,整个网络才真正联通。微服务的可观测性领域也曾经历过这种“格式混乱”:Jaeger、Zipkin、SkyWalking 各定义各的数据模型,业务方每换一个监控后端就要改一遍埋点代码。

OpenTelemetry(简称 OTel) 就是可观测性领域的“统一面单标准”。它是一个由 CNCF 维护的开源标准,提供统一的 API、SDK 与传输协议(OTLP)。应用只需按 OTel API 埋一次点,数据就可以导出到 Jaeger、Prometheus、SkyWalking 等任意兼容后端;其 traceparent 格式与 W3C Trace Context 标准保持一致,保证了跨厂商互认。OTel 的架构分为三层:API 层定义埋点接口,SDK 层实现采样、导出等逻辑,Exporter 层负责把数据发往具体后端。

统一面单让任何两家快递公司都能互认包裹;OTel 则让任何服务的追踪数据都能无障碍汇入统一的可观测性平台。

Ttotal=T1+T2+⋯+Tn\displaystyle T_{\text{total}} = T_1 + T_2 + \dots + T_n
串行调用场景下,一条请求的总耗时近似等于各阶段 Span 耗时之和;若存在并行分支,则总耗时取并行分支中的最大值。这个模型是链路分析的基础。
  1. 串行求和网关→鉴权→模型推理依次执行时,总耗时为各 Span 时长之和。
    Ttotal=T1+T2+⋯+TnT_{\text{total}} = T_1 + T_2 + \dots + T_n
  2. 并行取最大两个子调用并发发出时,父 Span 的总耗时由最慢的子调用决定。
    Tparallel=max⁡(T1,T2)T_{\text{parallel}} = \max(T_1, T_2)

实践应用:链路分析与性能定位

当客户投诉“包裹三天没动”时,客服会打开全链路时间轴,逐站查看哪一段耗时异常——是卡在海关清关,还是派送员迟迟未取件。这套“查每一站耗时”的思路,正是分布式追踪最核心的实践价值。在后端系统中,追踪平台会把一条 Trace 绘制成瀑布图:纵轴是 Span 树,横轴是时间,每个 Span 是一条横条,长度代表耗时。哪根横条最长,瓶颈就一目了然。

在 AI 推理场景中,这种能力尤其宝贵。假设一个 LLM 应用的“首 Token 延迟”飙到了 10 秒,传统手段只能逐服务看日志猜原因;瀑布图则会直接显示:认证耗时 300ms、RAG 检索耗时 6.5s、模型推理仅 1.2s。矛头立刻指向 RAG 检索中的向量召回环节。除了人工看图,还可以用算法定位:在关键路径上找到耗时最长的 Span 作为首要优化目标,再结合百分位数值(如 p99,即 99% 的请求耗时都低于该值)评估波动。

客服按站点耗时锁定了“卡在海关”;工程师按瀑布图锁定“卡在检索”。可观测性的终极价值,就是把抽象的网络延迟还原成一条条可定位的因果链。

瓶颈 Span=arg⁡max⁡i  (tiend−tistart)\displaystyle \text{瓶颈 Span} = \arg\max_{i} \; (t_i^{\text{end}} - t_i^{\text{start}})
并行与串行混合的链路中,对整个响应时间贡献最大的 Span,就是最值得优先优化的瓶颈节点。
  1. 计算各 Span 时长括号内的差值表示第 i 个 Span 的净耗时。
    tiend−tistartt_i^{\text{end}} - t_i^{\text{start}}
  2. 取耗时最大者argmax 在所有 Span 中挑出耗时最长的那一个,它就是排障的第一目标。
    arg⁡max⁡i  (… )\arg\max_{i} \; ( \dots )

设计要点:把面单填规范,链路才有用

经验丰富的快递员面单填写很有讲究:环节名写清楚、时间戳精确到秒、异常情况必填备注,同时面单上收件人电话的中间四位会被隐去。不规范的填写会让后续对账寸步难行。Span 的埋点设计同样遵循这些原则。首先是粒度控制:建议把一个 Span 对齐到一次有业务意义的动作——一次 RPC 调用、一次数据库查询、一次模型推理;而不是在 for 循环里每迭代一次就创建一个 Span,否则链路会膨胀成噪音。

其次是开销控制:每个 Span 从创建、序列化到传输、存储都有成本,采样可以在入口处兜底,但埋点本身也要克制。第三是异常处理:失败的 Span 必须标记错误状态并附上原因,否则“快”与“慢”的对比失去了参照。最后是安全脱敏:不要把 Prompt 全文、Token、手机号等敏感信息写进 Span 的 attributes,它们会随着链路数据进入日志与监控平台,形成泄露风险。

面单填得规范,全网的包裹才能高效流转;Span 设计得规范,整条链路才能在排障时真正派上用场。

Ctotal=Nspans×csingle\displaystyle C_{\text{total}} = N_{\text{spans}} \times c_{\text{single}}
追踪系统的总开销近似等于 Span 数量乘以单个 Span 的处理成本。控制 Span 数量,就是控制系统开销。
  1. 单位成本每个 Span 在创建、导出、存储时都会消耗一定的 CPU 与内存资源。
    csinglec_{\text{single}}
  2. 线性放大Span 数量越多,总开销越大,因此埋点粒度必须克制。
    Ctotal∝NspansC_{\text{total}} \propto N_{\text{spans}}

AI 应用场景

场景一:LLM 推理全链路延迟定位

阶段:模型服务运维与可观测性

解决的问题:用户反馈 Chat 接口“转圈 10 秒才出第一个字”,但无法判断瓶颈在网关、鉴权、Prompt 处理还是模型推理。传统方式是逐个服务查日志,耗时数小时。

具体案例:某 AI 应用接入 OpenTelemetry 后,为每次 Chat 请求创建 Trace:网关 Span → 鉴权 Span → Prompt 预处理 Span → RAG 检索 Span → 模型推理 Span → 流式输出 Span。瀑布图显示模型推理只占 1.2s,而 RAG 检索中的向量库召回占 6.8s。团队将向量召回改为并发分片查询,首 Token 延迟从 10s 降到 2s。

场景二:RAG 系统检索链路上下文接力

阶段:AI 应用集成与联调

解决的问题:RAG 问答中“检索质量差”与“检索延迟高”常常同时出现,但检索链路跨越 Query 改写、Embedding 调用、向量库、重排序多个组件,难以归因。

具体案例:研发团队将 traceparent 放进 HTTP 头与 gRPC metadata,让一次用户问题在四个组件间接力传播同一个 traceId。链路分析显示重排序服务在高峰期 P99 延迟激增至 4s,进一步定位到该服务的 CPU 被某模型副本占满。扩容后 P99 回落到 800ms。

场景三:离线训练数据流水线瓶颈排查

阶段:训练数据准备与预处理

解决的问题:每日特征工程流水线包含 30 多个子任务,某天执行时间从 1 小时暴涨到 3 小时,无法确定是数据拉取、清洗还是特征计算变慢。

具体案例:流水线框架为每个子任务创建 Span,并记录文件读取字节数与处理耗时。链路树显示耗时集中在某个 OSS 分片下载任务——该分片所在存储节点出现拥塞。团队据此将大文件切分成更小分片并发下载,流水线恢复到 50 分钟。

Python 演示

python81 行
# -*- coding: utf-8 -*-"""简易分布式追踪模拟:Span/Trace 与上下文传播""" import randomimport timeimport uuid  class Span:    """一个 Span 代表一次操作单元,类似快递的一个站点回执"""     def __init__(self, name, trace_id, parent_id=None):        self.name = name        self.trace_id = trace_id        self.parent_id = parent_id        self.span_id = uuid.uuid4().hex[:16]        self.start_time = None        self.end_time = None     def start(self):        self.start_time = time.time()     def finish(self):        self.end_time = time.time()     def duration_ms(self):        if self.start_time is None or self.end_time is None:            return 0        return int((self.end_time - self.start_time) * 1000)     def to_traceparent(self):        parent = self.parent_id or self.span_id        return "00-{}-{}-01".format(self.trace_id, parent)  class Trace:    """一个 Trace 代表一次完整请求的调用链"""     def __init__(self, sample_rate=0.8):        self.sample_rate = sample_rate        self.sampled = random.random() < sample_rate        self.trace_id = uuid.uuid4().hex[:32]        self.spans = []     def new_span(self, name, parent_id=None):        if not self.sampled:            return None        span = Span(name, self.trace_id, parent_id)        self.spans.append(span)        return span  def simulate_request():    trace = Trace(sample_rate=0.8)    root = trace.new_span("api-gateway")    if root is None:        print("本次请求未采样,不记录链路")        return    root.start()    time.sleep(0.05)     order = trace.new_span("order-service", parent_id=root.span_id)    order.start()    time.sleep(0.03)     db = trace.new_span("db-query", parent_id=order.span_id)    db.start()    time.sleep(0.08)    db.finish()    order.finish()    root.finish()     print("traceId =", root.trace_id)    print("传递的 traceparent =", root.to_traceparent())    for s in trace.spans:        print("- {} spanId={} parentId={} duration={}ms".format(            s.name, s.span_id, s.parent_id, s.duration_ms()))  if __name__ == "__main__":    simulate_request()
目的:用纯 Python 模拟一个微服务请求的链路构建过程——创建根 Span、子 Span,通过 parentId 构建树,并输出可跨服务传递的 W3C traceparent 字符串。
  1. self.sampled = random.random() < sample_rateTrace 创建时根据采样率决定是否记录,相当于快递公司决定这一单要不要全程稽核。
  2. span = Span(name, self.trace_id, parent_id)新 Span 绑定全局 traceId 与父 Span 的 spanId,回执单挂到了正确的旅程上。
  3. self.span_id = uuid.uuid4().hex[:16]每个 Span 生成独立的 16 位十六进制 ID,作为自己在树上的唯一身份。
  4. return "00-{}-{}-01".format(self.trace_id, parent)序列化 W3C traceparent 头,下游服务解析后即可接力续写链路。
  5. trace.new_span("db-query", parent_id=order.span_id)孙 Span 通过 parent_id 指向父 Span,树形结构由此生长。
  6. s.duration_ms()计算每个 Span 的净耗时,对应瀑布图中横条的长度。

常见误区

误区“我已经让每个服务在入口和出口都打了日志,出了问题翻日志就能拼出整条调用链,没必要再引入追踪系统。”

正解没有全局唯一的 traceId,日志之间就无法建立关联。就像每个快递站点都记了一本台账,但台账上不写运单号,包裹丢了根本无法还原它走过的路线。分布式追踪的本质是先为一次请求生成全局唯一的 traceId,让所有服务在日志和 Span 中都携带它,后端才能按 ID 把它们关联成一条链。

为何常见:单机时代靠时间戳和日志行号就能定位问题,这种经验被惯性带入了分布式场景。

误区“追踪数据当然越多越好,万一漏掉的那条就是故障现场呢?所以采样率必须开到 100%。”

正解全量采样在低流量环境可行,但在高并发的微服务或 AI 推理网关前,每个请求都会产生几十上百个 Span,存储与 CPU 开销会指数级上涨。更务实的组合是固定采样率(如 10%)兜底,再配合尾部采样保证失败链路 100% 保留,实现“重要数据不缺,总体成本可控”。

为何常见:对可观测性有“数据越全=True 越安全”的直觉,忽略了成本曲线。

误区“埋点粒度越细,定位问题时看得越清楚,所以函数里的每个循环、每个分支都应该创建 Span。”

正解Span 粒度过细会带来三宗罪:埋点代码侵入业务逻辑、海量 Span 拖慢 SDK 与存储、瀑布图被噪音淹没。建议粒度对齐“一次有业务意义的动作”——一次 RPC、一次 DB 查询、一次模型推理;高频小操作改用结构化日志或指标(Metrics)记录。

为何常见:新手容易把“可观测性”理解为“打点越多越好”,而忽略了每个 Span 都有真实成本。

误区“我们的核心链路都走 Kafka 消息队列,不是 HTTP 调用,所以分布式追踪用不上。”

正解只要消息中携带 traceparent(Kafka 消息头、RabbitMQ 消息属性、gRPC metadata),异步消息驱动的链路同样能被完整串起来。就像快递转运既用卡车也用飞机,交接方式变了,运单号照样跟随包裹走。

为何常见:绝大多数教程示例都用 HTTP 中间件演示,让人误以为只有 HTTP 才能传播上下文。

误区“traceId 只是个标识符,我用短 UUID 或者随机字符串拼接一下,够用就行。”

正解traceId 遵循 W3C 规范应为 128 位全局唯一值。若采用短随机数,在每秒上万请求的并发下碰撞概率会显著上升,一旦两个请求共用一个 traceId,两条链路就会串成一条,排障时产生荒谬结论。生产环境应使用标准库生成 32 位十六进制 ID,并注意与日志系统、后端存储的索引格式对齐。

为何常见:本地单测时随机数“看起来一直没重复”,让人低估了大规模并发下的碰撞概率。

面试题

Q1. 在分布式追踪中,Trace 最准确的含义是什么?

单选题 · easy

  • A.一次完整请求的调用链路← 正确答案
  • B.单个函数的执行日志信息
  • C.数据库中的一条查询记录
  • D.服务器上的一个进程信息

答案:A。Trace 对应一次完整请求从入口到出口的全部调用记录,相当于快递包裹的完整旅程;单函数日志、单条查询、单个进程都只是旅程上的一站,不能代表整条链路。

Q2. Span 表示什么?

单选题 · easy

  • A.一次完整请求调用记录
  • B.单个操作的一个处理单元← 正确答案
  • C.一条文本格式的日志记录
  • D.一台服务器主机的进程

答案:B。Span 是 Trace 中的最小工作单元,代表一次具体的处理动作,例如一次 RPC 调用或一次数据库查询。它记录该动作的名称、起止时间、父子关系与状态,相当于快递在某一站的处理回执。

Q3. 根 Span(root span)最核心的特征是什么?

单选题 · easy

  • A.没有父级Span的根节点← 正确答案
  • B.链路中耗时最长的节点
  • C.第一个被采样到的节点
  • D.每个服务都会产生的点

答案:A。根 Span 是整条 Trace 的入口,它没有父 Span,parentId 为空。耗时最长、被采样、每个服务都产生,这些都不是根 Span 的本质定义。

Q4. W3C 标准中 traceparent 请求头的主要作用是什么?

单选题 · mid

  • A.传递认证令牌与用户信息
  • B.跨进程传递链路标识信息← 正确答案
  • C.压缩数据传输量与带宽
  • D.记录服务器地址与端口

答案:B。traceparent 按 W3C Trace Context 标准传递 trace-id、parent-id 和采样标记,让下游服务能把新 Span 挂到同一棵树上。认证令牌、数据压缩、服务器地址都不属于它的职责。

Q5. 固定采样率 r=0.1 的准确含义是什么?

单选题 · mid

  • A.每十分钟就采样一次
  • B.约一成请求被记录链路← 正确答案
  • C.只记录最耗时的百分之十
  • D.每个请求记录十条记录

答案:B。固定采样率为每个请求生成一个 0~1 的随机数,小于 0.1 则记录,因此平均约 10% 的请求被保留。它与时间间隔、耗时排名、Span 数量都无关。

Q6. Span 的 duration 是如何计算的?

单选题 · mid

  • A.终点时间减去起点时间← 正确答案
  • B.起点时间减去终点时间
  • C.所有子Span的时长之和
  • D.网络请求的固定超时值

答案:A。duration = 结束时间戳 − 开始时间戳,即该操作自身的净耗时。父 Span 的时长包含了子 Span 的等待时间,但不能用各子 Span 之和替代,更不能使用固定超时值。

Q7. 与头部采样相比,尾部采样的核心优势是什么?

单选题 · hard

  • A.实现最简单且开销最低
  • B.可先判断结果再决定采集← 正确答案
  • C.不需要存储任何先验数据
  • D.能自动修复已采集的错误

答案:B。头部采样在请求开始时立刻决策,无法预知请求会不会失败;尾部采样先把少量数据暂存,等请求结束后根据结果决定是否保留,因此能保证错误请求 100% 被记录。它需要缓存,实现成本更高,也无法“修复”错误数据。

Q8. 下列哪些方式可以传播 Trace 上下文?

多选题 · mid

  • A.HTTP请求头传播上下文← 正确答案
  • B.消息队列消息属性携带← 正确答案
  • C.日志文件内手动拼接
  • D.gRPC元数据传输← 正确答案

答案:ABD。traceparent 可通过 HTTP 请求头、消息队列的消息属性、gRPC 的 metadata 等通信载体跨进程传递。日志文件只是记录结果的地方,不具备“随请求传播”的能力,不能作为上下文传播方式。

Q9. 关于采样策略,下列说法正确的有哪些?

多选题 · hard

  • A.固定采样率实现简单← 正确答案
  • B.尾部采样可保留错误链路← 正确答案
  • C.动态采样依据流量自适应← 正确答案
  • D.头部采样必须缓存全部数据

答案:ABC。固定采样率实现最简单,尾部采样能保留错误链路,动态采样按流量调整采样率。头部采样在请求入口即刻决策,不需要缓存全部数据,因此 D 错误。

Q10. 关于 OpenTelemetry,下列说法正确的有哪些?

多选题 · extreme

  • A.提供统一API与SDK← 正确答案
  • B.属于W3C官方标准组织
  • C.支持多语言客户端← 正确答案
  • D.只能用于HTTP传输

答案:AC。OpenTelemetry 是 CNCF 维护的开源可观测性标准,提供统一 API/SDK 并支持多语言;它遵循 W3C 的 Trace Context 规范来定义 traceparent,但自身并非 W3C 组织。传输方面支持 gRPC、HTTP、消息队列等多种协议,D 错误。