想象一下:你在网上买了一个包裹,从杭州发到北京,中间要经过揽收、分拣、干线运输、派送等好几个站点。包裹在哪个环节被卡住了?只要每个站点都扫描同一个运单号,所有记录就能串成一条线。微服务调用的追踪也是同一个道理——一个用户请求会穿过网关、订单、支付、库存等多个服务,任何一个环节慢了,前端都会报错。W3C Trace Context 就是分布式追踪世界里的“标准运单号”,它定义了 traceparent 和 tracestate 两个头部字段,让所有服务都能识别同一条调用链;head-based 和 tail-based 采样则像“是否全程重点跟单”的两种决策模式。本文就用这条“快递物流线”带你完整理解链路如何拼接、上下文如何传播、采样如何取舍。

岗位关联

掌握 W3C Trace Context 与采样策略,是搭建微服务可观测性体系的必修课,直接支撑你在生产环境中快速定位慢请求和错误根因。

目录
  1. 可视化演示
  2. 链路追踪概述
  3. traceparent 头结构
  4. tracestate 头结构
  5. 上下文传播机制
  6. 头部采样原理
  7. 尾部采样原理
  8. 采样策略选型
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示W3C Trace Context跨服务链路传播、头部与尾部采样策略对比及分布式调用链根因定位,交互理解链路追踪核心机制。

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

'+modeData.propagation.caption+'

链路追踪概述

你寄一个跨城包裹,要经过揽收、分拣、干线运输、派送等多个站点。想知道包裹在哪被卡住,就得把各站点的扫描记录串成一条时间线。微服务也一样:一个用户请求穿过网关、订单、支付、库存等多个服务,任何一个环节慢了,用户就投诉。可到底慢在哪?如果每个服务各写各的日志,像四个仓库各自记各自的账,谁也拼不到一起。

当服务只有两三个时,靠日志里塞一个 request ID 还能勉强对上号。可服务一多、调用出现分叉(比如下单同时调支付和库存),简单的请求 ID 就对不上了。于是我们需要为一次请求生成全局唯一的 trace-id,每经过一个服务记一个 span,span 之间用 parent-id 串成一棵树。这棵树就是完整调用链,也就是 Metrics、Logs、Traces 三大可观测性支柱里 Traces 要解决的问题。

定义上,一条完整链路是所有调用段的集合:

有了这条链,就像包裹的每一段揽收记录挂在同一个运单号下——哪个环节延误、哪个环节抛错,一点就能看到。链路追踪的价值,就在于把“一堆散日志”变成“一条可走查的完整路径”。

L=⋃i=1nsi\displaystyle L = \bigcup_{i=1}^{n} s_i
完整调用链 L 等于一路上所有服务调用段 s₁、s₂、…、sₙ 的并集,缺了任何一段,链条就不完整。
  1. 调用段 $s_i$链路上第 i 个服务处理这一请求所产生的一段追踪信息,包含服务名、开始时间、耗时与状态。
    si=(servicei,starti,durationi,statusi)s_i = (\text{service}_i, \text{start}_i, \text{duration}_i, \text{status}_i)
  2. 并集 $\bigcup$把所有段拼接起来,就得到从入口到出口的完整链路视图。
    L=s1∪s2∪⋯∪snL = s_1 \cup s_2 \cup \dots \cup s_n

traceparent 头结构

如果每个快递公司都用自己的单号规则——顺丰 12 位数字、圆通 14 位字母数字——交叉物流时就没法互相识别。W3C Trace Context 就像物流行业统一运单号格式,定义了 traceparent 这个标准头。只要所有服务都按同一个格式解析,谁都能认出“还是那个包裹”。

traceparent 头由四个短横线分隔的字段组成:

任何服务拿到 traceparent,先看 trace-id 知道属于哪条链,再看 parent-id 知道自己是链上哪个节点的子节点,最后看 flags 决定要不要落盘。这就是“万能运单号”的全部秘密——格式简单,信息密度却很高。

traceparent=ver  −  trace-id  −  parent-id  −  flags\displaystyle \text{traceparent} = \text{ver} \; - \; \text{trace-id} \; - \; \text{parent-id} \; - \; \text{flags}
traceparent 的格式是“版本号-全局追踪ID-当前段ID-采样标记”。版本号让协议未来能演进,trace-id 标识整条链路,parent-id 记录当前这一段,flags 告诉下游要不要记录。
  1. 版本号 $\text{ver}$当前标准固定为 00,未来若格式升级可通过版本号区分新旧协议。
    ver=0016\text{ver} = 00_{16}
  2. 全局 ID $\text{trace-id}$128 位随机数,用 32 个十六进制字符表示,整条链中永远不变,是全链路唯一标识。
    trace-id∈[0-9a-f]32\text{trace-id} \in [\text{0-9a-f}]^{32}
  3. 当前段 ID $\text{parent-id}$64 位随机数,16 个十六进制字符,表示“当前这个 span 是谁”。每经过一个服务都会更新为新 span 的 ID。
    parent-id=span-idcurrent\text{parent-id} = \text{span-id}_{\text{current}}
  4. 采样标记 $\text{flags}$8 位比特串,最低位为采样位,01 表示已采样、00 表示未采样。
    flags0∈{0,1}\text{flags}_0 \in \{0, 1\}

tracestate 头结构

运单号是统一格式,但各家快递公司还顺手在箱子上贴自己的附加标签:“易碎”“加急”“需冷藏”。这些标签不参与统一识别,却承载了各自业务需要的额外信息。tracestate 就是链路追踪里的“附加标签区”,让不同供应商在同一个请求里携带各自的数据。

tracestate 是一个由逗号分隔的键值对集合:

标签再多也不能把运单号挤没——traceparent 永远是主角,tracestate 只做配角。这也是 W3C 的设计哲学:核心识别信息统一标准,扩展信息留给各厂商自由发挥。

tracestate={(k1,v1),(k2,v2),…,(km,vm)}\displaystyle \text{tracestate} = \{(k_1, v_1), (k_2, v_2), \dots, (k_m, v_m)\}
tracestate 可以装最多 32 个供应商的键值对,每个键值对用 vendor=value 形式书写,值内不能有逗号与等号。它会被无条件透传给下游,直到超过传输限制。
  1. 键 $k_i$通常是供应商简称,如 dd= 代表某 APM 厂商、ot= 代表 OpenTelemetry,避免不同厂商互相覆盖。
    ki=vendor-prefixk_i = \text{vendor-prefix}
  2. 值 $v_i$厂商自定义的数据,比如采样率、优先级标记或上游服务的采样决策,限制为不含分隔符的字符串。
    vi∈{printable ASCII, no comma/equal}v_i \in \{\text{printable ASCII, no comma/equal}\}
  3. 组合规则键值对之间用逗号连接,整体拼成一个头;协议规定单键不超过 256 字符、总条目不超过 32 个。
    tracestate=k1=v1, k2=v2, …\text{tracestate} = k_1\text{=}v_1\text{, } k_2\text{=}v_2\text{, } \dots

上下文传播机制

包裹每到一站,站员扫一下运单号,在系统里登记“已收到”,再把运单号传给下一站。链路上下文也是这样逐跳接力:服务 A 处理完请求,把携带 trace-id 的 traceparent 通过 HTTP 头传给服务 B,B 再传给 C,直到请求结束。

这个接力过程可以抽象为一个映射关系:

在 OpenTelemetry 等实现里,这一步通常由 SDK 自动完成:服务只要在入口调用一次“提取”、在出口调用一次“注入”,其余什么都不用管。一旦某个环节没有提取或者手工把头删了,链路就在这断开——和包裹丢在中转站一样,上下记录对不上号。

Hn+1=propagate⁡(Hn)\displaystyle H_{n+1} = \operatorname{propagate}(H_n)
第 n 个服务收到的上下文头 Hₙ,经过一步传播变成第 n+1 个服务收到的 Hₙ₊₁。传播动作不是原样复制,而是更新 parent-id、保留 trace-id 与采样位。
  1. 上下文头 $H_n$第 n 跳时携带的 traceparent 与 tracestate 集合。
    Hn=(traceparentn,tracestaten)H_n = (\text{traceparent}_n, \text{tracestate}_n)
  2. 传播函数 $\operatorname{propagate}$SDK 或中间件执行的动作——解析头部、生成新 span、把新 span-id 写回 parent-id 位置。
    traceparentn+1=ver−trace-id−new-span-id−flags\text{traceparent}_{n+1} = \text{ver} - \text{trace-id} - \text{new-span-id} - \text{flags}
  3. 不变量trace-id 全程不变,tracestate 中的键值对逐跳追加或透传。
    trace-idn+1=trace-idn\text{trace-id}_{n+1} = \text{trace-id}_{n}

头部采样原理

快递公司人手有限,不可能每件包裹都派专人一对一跟到收件人。所以寄件时先抽签:被抽中的包裹全程重点跟踪,没被抽中的只在系统里留条扫描记录。head-based 头部采样就是这种“寄件时抽签”——在请求到达入口服务的那一刻,决定整条链路要不要被追踪。

决策结果写进 traceparent 的 flags 最低位:01 表示追踪、00 表示不追踪。这个标记随头一路透传,下游服务读到采样位是 0,就直接跳过落盘逻辑。因此头部采样的最大优点是开销极低——决策只需一次随机数,链路中间环节零额外成本。代价是它的决策先于事实:一个真正出错或极慢的请求,可能恰好没被抽中,成了“漏网之鱼”。

每秒写入存储的 trace 数与入口请求量满足简单关系:

抽签省了人力,但可能漏掉最该看的问题。这正是高可靠性系统转向尾部采样的原因——先别急着扔,等看完结果再做决定。

T=Q×p\displaystyle T = Q \times p
其中 T 是每秒写入存储的 trace 条数,Q 是入口每秒请求数,p 是采样率。比如 Q=1000、p=0.1,则每秒只存 100 条 trace。
  1. 入口请求量 $Q$网关或首层服务每秒收到的请求总数。
    Q=requests per secondQ = \text{requests per second}
  2. 采样率 $p$一个 0 到 1 之间的小数,表示每个请求被抽中的概率。
    p∈[0,1]p \in [0, 1]
  3. 存储流量 $T$采样后实际进入存储系统的 trace 数量,决定了日志存储的成本上限。
    T=Q×pT = Q \times p

尾部采样原理

有些大型物流中心不搞寄件抽签,而是把所有包裹的流转信息都先存进总仓,晚上统一复盘:哪些延误超过 24 小时?哪些投诉率高?再决定要不要专门调监控查这些包裹。tail-based 尾部采样就是这种“事后复盘”——先把整条链路的 span 全部记录缓冲,等链路完整跑完,再由后端服务根据错误码、耗时等真实结果决定保留哪些。

尾部采样的核心是“先全量、后筛选”,因此它的存储压力比头部采样大得多。设全量链路数为 N,每条链路平均有 C 个 span,则缓冲阶段需要管理的总 span 数:

尾部采样好在哪?它按最终结果决策,错误 trace 和超慢 trace 几乎总会被保留,“漏网之鱼”大幅减少。但代价也清楚:N 越大,仓库租金越贵。所以尾部采样通常不会独自扛下全部流量,而是作为“二次筛选”出现在昂贵存储系统之前。

K=N×C\displaystyle K = N \times C
缓冲的总 span 数 K 等于链路数 N 乘以每条链路的平均 span 数 C。这就是尾部采样最直接的存储成本公式。
  1. 链路数 $N$采样窗口内进入系统的所有完整链路数量。
    N=total tracesN = \text{total traces}
  2. 平均 span 数 $C$每条链路平均包含的服务调用段数量,反映调用链深度。
    C=总 span 数NC = \frac{\text{总 span 数}}{N}
  3. 总缓冲量 $K$需要暂存的总 span 数,决定内存与磁盘的容量规划。
    K=N×CK = N \times C

采样策略选型

小快递站人手有限,寄件抽签最省事;大型物流中心宁愿仓租贵一点,也要保证大客户的高价值包裹都能复盘。选 head 还是 tail,本质是“仓库租金”与“追踪精度”之间的权衡。没有任何一种采样策略能同时做到零成本、百分百召回——你必须知道自己到底想要什么。

生产实践中更常见的是分层混合策略:入口处用 head 采样把海量流量砍到可接受比例,比如 1%;同时对 ERROR 状态和超过 P99 的慢请求标记为“必须保留”;最后再在存储层前加一道 tail 二次过滤,把最值得复盘的高危 trace 从全量缓冲里捞出来。这样既控制了成本,又保证出了问题找得到。

用投资回报的视角看,采样收益可以用一条简单的比率衡量:

没有万能的快递方案,只有适合你预算的跟单策略。小集群用 head 采样的低成本足够;核心交易链路建议混合策略,用一点存储成本换排障时的清晰视野——这平衡术,就是可观测性工程师的价值所在。

R=AScost\displaystyle R = \frac{A}{S_{\text{cost}}}
采样回报 R 等于可排查问题数 A 除以存储成本 S_cost。当一次线上事故造成的损失远高于存储成本时,提高采样率就是划算的。
  1. 可排查问题数 $A$采样系统中能成功定位根因的 trace 数量,代表采样的“有效产出”。
    A=traces with root cause foundA = \text{traces with root cause found}
  2. 存储成本 $S_{\text{cost}}$保存这些 trace 所需的存储与查询成本,是表达式的分母。
    Scost=存储单价×存储量S_{\text{cost}} = \text{存储单价} \times \text{存储量}
  3. 回报率 $R$比值越大,说明同样的存储成本换来了更多可排查的问题。
    R=AScostR = \frac{A}{S_{\text{cost}}}

AI 应用场景

微服务线上慢链路排障

阶段:数据采集与监控

解决的问题:过去查线上慢请求靠 grep 日志瞎猜,先猜服务、再猜接口、最后猜参数,效率极低。引入 W3C Trace Context 后,每个请求都有全局 trace-id,SRE 在 APM 平台里一键拉出完整调用链,从网关到数据库每一跳的耗时清清楚楚。

具体案例:某电商支付链路出现 502,SRE 按 trace-id 在 Jaeger 里搜索,发现 payment-svc 调用第三方支付的 HTTP 客户端重试 3 次后超时,根因是上游设置的 connectTimeout 太短。整个定位过程从半天缩短到 15 分钟。

多厂商 APM 数据打通

阶段:数据标准化与集成

解决的问题:一个大型系统里不同部门选型不同——网关用 Envoy、订单用 SkyWalking、推荐服务用自研 SDK。链路数据各存各的,trace 对不上。统一采用 W3C Trace Context 后,traceparent 在所有组件间按同一标准传递,跨厂商的调用链也能无缝拼接。

具体案例:某云厂商客户在接入 OpenTelemetry Collector 后,原本断裂的两段链路因为 trace-id 一致自动关联,用户终于看到了从浏览器到后端数据库的全链路视图。

高 QPS 系统采样降本

阶段:数据处理与存储优化

解决的问题:广告推荐服务峰值 QPS 5 万,全量埋点一个月存储成本 30 万,而且 99% 的 trace 根本没人查。用 head 采样把常规流量压到 1%,同时对 ERROR 和超 P99 的请求维护白名单,再配一道 tail 二次筛选。

具体案例:优化后存储成本降到 5 万/月,而错误链路召回率反而从 40% 提升到 95%,因为尾部筛选确保了异常 trace 必保留。

Python 演示

python72 行
# -*- coding: utf-8 -*-"""W3C Trace Context 传播与采样模拟演示"""import randomimport uuid  def new_traceparent(sampled):    """生成一个新的 traceparent,sampled 表示是否被采样。"""    trace_id = uuid.uuid4().hex[:32]    parent_id = uuid.uuid4().hex[:16]    flags = "01" if sampled else "00"    return f"00-{trace_id}-{parent_id}-{flags}"  def next_span(tp):    """模拟服务处理:保留 trace-id,更新 parent-id 为新 span。"""    parts = tp.split("-")    parts[2] = uuid.uuid4().hex[:16]    return "-".join(parts)  def trace_id_of(tp):    return tp.split("-")[1]  def is_sampled(tp):    return tp.split("-")[3] == "01"  def head_sample(rate):    return random.random() < rate  class TailCollector:    """模拟尾部采样:全量缓冲,结束后按错误优先保留。"""     def __init__(self, capacity):        self.capacity = capacity        self.buffer = []     def add(self, trace_id, status, duration):        self.buffer.append({"trace_id": trace_id, "status": status, "duration": duration})     def decide(self):        self.buffer.sort(key=lambda x: (x["status"] != "ERROR", -x["duration"]))        self.buffer = self.buffer[: self.capacity]        return [item["trace_id"] for item in self.buffer]  def main():    services = ["gateway", "order", "payment", "storage"]     print("=== head-based 采样 ===")    for i in range(3):        tp = new_traceparent(sampled=head_sample(0.5))        print(f"请求 {i}: trace_id={trace_id_of(tp)}, sampled={is_sampled(tp)}")        for svc in services:            tp = next_span(tp)            print(f"  -> {svc} 收到 traceparent, parent_id 已更新为 {tp.split('-')[2]}")     print("\n=== tail-based 采样 ===")    collector = TailCollector(capacity=2)    for i in range(5):        tid = uuid.uuid4().hex[:16]        status = "OK" if random.random() > 0.3 else "ERROR"        collector.add(tid, status, random.uniform(0.1, 2.5))    kept = collector.decide()    print("保留的 trace:", kept)  if __name__ == "__main__":    main()
目的:演示 traceparent 在服务链中的逐跳传播(parent-id 更新、trace-id 不变),以及 head-based 与 tail-based 两种采样策略的决策逻辑。
  1. def new_traceparent(sampled)生成符合 W3C 格式的 traceparent 头,sampled 决定 flags 是否为 01。
  2. parts[2] = uuid.uuid4().hex[:16]传播时只更新 parent-id,模拟当前服务生成了新 span。
  3. def head_sample(rate)头部采样核心:一次随机数决定整条链路是否被追踪。
  4. self.buffer.sort(key=lambda x: (x["status"] != "ERROR", -x["duration"]))尾部采样核心:先按是否错误排序,再按耗时倒序,错误且慢的 trace 排最前。
  5. self.buffer = self.buffer[: self.capacity]只保留容量范围内的 trace,模拟存储预算受限的筛选。

常见误区

误区初学者常以为 traceparent 是只读的,生成之后所有服务原样传递,谁改谁就是错的。

正解traceparent 中的 trace-id 确实全程不变,但 parent-id 恰恰需要每跳更新——每次服务调用都生成新的 span-id,并写回 parent-id 位置。这就像快递运单号不变,但每个转运站都会登记“上一站是哪里、下一站到哪里”,修改 parent-id 是正确传播的一部分。

为何常见:把 traceparent 当成普通 HTTP 头里的固定 token,而忽略了它在分布式追踪语义里代表“当前链路位置”这一动态属性。

误区看到 flags 最低位是 0,就认为这条 trace 完全没有价值,可以直接丢弃所有相关日志。

正解采样位只是“是否落盘存储”的建议标记,不代表链路自身不能传播或检索。tracestate 中其他厂商可能还保留着自己的采样决策,而且一旦请求出错,运维人员完全可以按 trace-id 手动回溯。采样位更像快递的“重点跟踪”标记,没贴标记的包裹照样能送到,只是不做专人跟进。

为何常见:将“未被采样”误解为“不存在”,混淆了存储决策与链路数据的可检索性。

误区有人觉得把采样率从 1% 提高到 50%,慢请求就基本不会漏了。

正解提高采样率确实减少了漏采概率,但它是随机抽样,慢请求与抽签结果互相独立——采样率 50% 时,最慢的那个请求依然有 50% 概率被丢弃。而且采样率升高带来存储成本线性上涨,QPS 一万时 50% 采样等于每秒存 5000 条链路。这也是头部采样最本质的局限:决策先于事实。

为何常见:直觉上觉得“多抽一些总能抽到”,忽略了随机抽样对尾部行为的无能为力。

误区既然尾部采样更精准,干脆全量采集、尾部筛选,不用再做头部采样。

正解尾部采样的前提是先全量缓冲所有 span,N 条链路平均 C 条 span 的存储成本在超高 QPS 场景下会瞬间击穿内存与磁盘。生产实践通常二者配合:头部采样先把流量砍到一个数量级,尾部再将高危 trace 从缓冲中捞出来。全量缓冲是尾部采样的地基,而不是免费午餐。

为何常见:看到尾部采样的“精准”就忽略它的“前置成本”,没有意识到精准是要用全量仓储换来的。

误区既然 tracestate 是自由扩展区,就把业务参数、用户标识、链路备注全部塞进去。

正解W3C 标准明确限制 tracestate 的键值条目最多 32 个、单个键值对值部分不超过 256 字符,且 HTTP 头整体过长会导致代理或中间件直接截断,破坏链路。这就像快递箱上贴标签可以,但贴满整个箱子甚至挡住运单号,快递公司只能拒收。

为何常见:把 tracestate 当成普通 KV 存储,而忘记了它是随每个请求透传的头部字段,需要为传输效率让路。

面试题

Q1. traceparent 头由几个短横线分隔字段组成?

单选题 · easy

  • A.两个字段:trace-id 与 parent-id
  • B.三个字段:版本、trace-id、flags
  • C.四个字段:版本、trace-id、parent-id、flags← 正确答案
  • D.五个字段:版本、trace-id、父段、子段、采样位

答案:C。W3C 标准定义 traceparent 为 ver-trace-id-parent-id-flags 四段式结构。版本号在前,trace-id 标识整条链路,parent-id 标识当前 span,flags 是 8 位采样标记。A、B 漏掉字段,D 凭空多出“父段/子段”两个概念,都不符合规范。

Q2. W3C Trace Context 中 trace-id 使用多少位十六进制字符表示?

单选题 · easy

  • A.8 位
  • B.16 位
  • C.32 位← 正确答案
  • D.64 位

答案:C。trace-id 是 128 位随机数,每 4 位对应一个十六进制字符,因此共 32 位十六进制串。16 位是 parent-id(64 位)的长度,8 位是 flags 或版本号长度,64 位超出了标准定义。

Q3. head-based 采样最大的优势是什么?

单选题 · mid

  • A.能够保证捕获所有慢请求
  • B.入口一次决策、链路零额外开销← 正确答案
  • C.服务端可根据结果动态调整
  • D.无需在 traceparent 中传递采样标记

答案:B。head 采样在请求入口用一次随机数决定整条链路是否采样,下游只需读取 flags 采样位即可,因此开销极低。但它无法保证慢请求被捕获(A 错);“根据结果调整”是 tail 采样的特性(C 错);采样标记必须通过 flags 传递(D 错)。

Q4. tail-based 采样最显著的代价是什么?

单选题 · mid

  • A.无法标记根因服务
  • B.需要先全量缓冲所有 span,存储开销大← 正确答案
  • C.必须依赖 tracestate 头才能工作
  • D.采样率只能在部署时静态配置

答案:B。tail 采样的前提是先记录全部 span 才能事后筛选,因此 N 条链路 × C 条平均 span 的缓冲成本是不可回避的代价。它能按错误和耗时精准保留,所以 A 错;它依赖 span 缓冲而非 tracestate,C 错;采样策略可在后端动态调整,D 错。

Q5. tracestate 中厂商键值对的作用是什么?

单选题 · mid

  • A.指定 traceparent 的协议版本号
  • B.让不同供应商携带各自的额外追踪数据← 正确答案
  • C.标识当前服务实例的监听端口
  • D.记录链路各跳的时间戳

答案:B。tracestate 是逗号分隔的键值对集合,键名用供应商前缀区分(如 dd=、ot=),让各家 APM 在同一个 HTTP 头里互不干扰地保存扩展数据。版本号由 traceparent 的 ver 字段负责(A 错),端口和时间戳不在该头的定义范围内。

Q6. 某系统每秒入口请求 10000 条,采样率 p=0.1,平均每条链路 40 个 span,head-based 采样下每秒实际写入的 span 数约为多少?

单选题 · extreme

  • A.40000 个← 正确答案
  • B.4000 个
  • C.1000 个
  • D.100 个

答案:A。每秒被采样的 trace 数为 10000 × 0.1 = 1000 条,每条含 40 个 span,因此每秒写入 span 数为 1000 × 40 = 40000 个。4000 是漏乘链路数的结果,1000 是只算了 trace 数,100 是把采样率与链路深度同时搞错。

Q7. 关于 head-based 与 tail-based 采样,以下说法正确的有哪些?

多选题 · mid

  • A.head 在请求入口决策全局采样标记← 正确答案
  • B.tail 需要等待链路全部完成再决策← 正确答案
  • C.head 可能漏掉真正慢的请求← 正确答案
  • D.tail 适合存储充足的过滤场景← 正确答案

答案:ABCD。head 在入口把决策写入 flags 并透传全程,A 对;tail 必须缓冲完整链路后才能按结果筛选,B 对;head 是随机抽样,决策先于事实,可能漏采慢请求,C 对;tail 的全量缓冲要求存储充足,通常用于昂贵存储前的二次过滤,D 对。

Q8. 关于 W3C Trace Context 的传播机制,下列说法正确的是?

多选题 · hard

  • A.trace-id 在整个调用链中保持不变← 正确答案
  • B.parent-id 每经过一个服务都会更新← 正确答案
  • C.traceparent 通过 HTTP 头逐跳传递← 正确答案
  • D.tracestate 不能传递给下游服务

答案:ABC。trace-id 是全局唯一标识,传播过程中不变,A 对;parent-id 是“当前 span 是谁”,每跳生成新 span 后必须更新,B 对;传播依赖 HTTP 头在服务间接力,C 对;tracestate 恰恰是为了透传下游供应商数据而设计的,D 错。

Q9. traceparent 中 flags 字段最低位为 1 表示什么?

单选题 · easy

  • A.这是链路中的第一个请求
  • B.该链路已被采样,需要记录存储← 正确答案
  • C.当前服务发生了错误
  • D.链路版本号已升级

答案:B。flags 是 8 位标记,最低位是采样位,01 表示已采样、00 表示未采样。它不是首请求标记,也不表示错误,更与版本号无关——版本由首个字段 ver 表示。

Q10. 生产环境设计采样策略时,合理的做法包括哪些?

多选题 · hard

  • A.入口用 head 采样砍掉大量常规流量← 正确答案
  • B.对 ERROR 与超时请求提高保留优先级← 正确答案
  • C.在昂贵存储前用 tail 二次筛选高危 trace← 正确答案
  • D.放弃 traceparent 改用自定义请求头

答案:ABC。典型混合策略是 head 先降量、白名单保异常,再用 tail 对缓冲 trace 二次筛选,A、B、C 均正确。自定义请求头会破坏 W3C 标准互操作性,导致跨厂商链路无法拼接,D 错。