题干与适用场景
设计一个供后端与稳定性团队使用的多租户分布式链路追踪平台,用来跟踪一次请求或异步工作流 经过多个服务的完整路径。面试假设是每秒新增 50 万条根工作流,平均每条工作流 15 个 Span, 每个编码后的 Span 在存储压缩前平均 700 字节。保留的链路应在 30 秒内达到 p99 可检索;按 租户和 Trace ID 精确查询的 p99 不超过 2 秒;按服务、操作、错误、耗时和时间窗口进行的常用 检索,p95 不超过 3 秒。埋点不能同步等待中心平台,丢失一个可用区也不能阻止应用继续上报。
平台负责生成或接收 Span、在受支持的传输协议上按 W3C Trace Context 传播上下文、组装延迟 或乱序到达的 Span、在明确预算内对整条链路采样、保存链路详情,并根据保留样本生成服务依赖 图。为每种语言实现完整 SDK、完整 APM 界面、日志平台、指标平台和异常检测均不在范围内。 日志可携带 Trace ID,指标可携带 exemplar,但它们仍是独立系统。
三份相互独立、发表于 2026 年的面试准备页面,都把分布式链路追踪作为系统设计题,并覆盖 Span、上下文传播、链路组装、采样与查询存储。这足以证明题目在当前环境中的代表性,但不能 证明公司归属,因此本文不作公司声明。技术模型以 W3C Trace Context 推荐标准、OpenTelemetry 规范与 Collector 实现,以及 Google 的 Dapper 论文为依据。
面试官考察点
第一个信号是候选人能否跨进程保存因果关系。只有 trace_id 不够。每个操作还需要 span_id、父子关系或显式链接、时间、状态、资源身份和受限属性。W3C 定义了可互操作的 traceparent,以及用于可选厂商状态的 tracestate;它没有把来自不可信请求的 Trace ID 变成鉴权凭证。强回答会校验传入上下文,在接入层隔离租户,并定义跨信任边界时的处理方式。
第二个信号是采样模型是否诚实。头部采样在整条链路尚不可见时作决定,可以降低 SDK、网络 和接入开销,却不能承诺保留之后才出现的每一条错误链路。尾部采样能在看到大部分 Span 后 选择链路,但必须先接收并暂存这些 Span。它降低的是下游存储成本,不是上游采集成本。如果 2% 的头部采样已经丢弃一条链路,后面的尾部采样器无法找回其中的错误 Span。
第三个信号是能否真正解决链路组装,而不是画一条通用事件管道。Span 会重复、延迟、乱序; 异步扇出和批处理还会形成有向无环图,而不总是一棵整齐的树。尾部采样必须把同一条链路的 全部 Span 路由给同一个决策者,设置有界的完成判断、内存保护和迟到 Span 规则。这个有状态 边界决定平台能提供诊断证据,还是悄悄制造采样偏差。
最后一个信号是可运维性。强回答会量化原始流量和保留流量,限制任意属性索引,隔离噪声租户, 监控上下文断裂与 Span 丢失,并避免让遥测管道成为业务依赖。只画到“Span 写数据库”为止, 成本、正确性和故障问题都没有被回答。
回答前需要澄清的问题
- 哪些工作流必须执行尾部决策? 完整尾部采样要求中心端先收到所有候选 Span。本方案对
普通流量在源端做头部采样,仅让一组受控的关键路由以 100% 进入尾部池。若所有路由都要求 基于错误保留,中心网络与状态预算就必须容纳完整原始流。
- “完整链路”怎样定义? 队列和脱离请求的任务没有通用结束标记。同步请求可用根 Span
结束加宽限时间;长工作流需要更长策略或显式完成事件。平台仍要返回 incomplete 标记, 不能把估计伪装成确定事实。
- 哪些查询是硬性契约? 范围内包括按 Trace ID 精确查询,以及对服务、操作、状态、耗时
区间和时间的有界过滤。对每个属性做任意全文条件,会扩大索引成本并引发高基数滥用;未进入 白名单的属性只能先取回链路后再查看。
- 数据保留多久? 假设是七天可索引热数据,外加 23 天压缩对象数据。更长的热保留会改变
存储与索引规模;合规删除也会决定对象键和加密域是否必须按租户拆分。
- 是否允许跨租户链路? 默认不允许。网关把认证凭证绑定到租户,并覆盖 Span 自带的租户
字段。经批准的跨域工作流使用显式链接或另行授权的关联方式,不能信任客户端指定的租户 ID。
- 异步因果关系如何表示? 一个父节点适合一个因果前驱。批量消费和汇聚可能依赖多个生产
者,应该使用 Span Link。强行只选一个父节点会丢信息,把同一 Span 复制到多个父节点下会 破坏链路计数。
- 过载时应用可以损失什么? 业务流量必须继续。本地队列耗尽内存与磁盘预算后可以丢遥测,
但必须按服务、租户、原因和采样类别计数。如果要求遥测零丢失,链路追踪就会变成业务路径 依赖,可用性契约也必须随之改变。
30 秒回答框架
“我会拆分上下文传播、采集、链路决策和查询存储。埋点库创建 Span 并传播经校验的 W3C 上下文,本地 Agent 异步批量上报并使用有界缓冲,因此中心平台不会阻塞请求路径。区域网关 认证租户、执行 Schema 和配额校验,再把 Span 写入按 Trace ID 分区的持久消息流。
“普通路由用一致的头部采样降低上游成本;关键路由完整进入尾部池。同一条链路的所有 Span 到达同一个组装器,组装器等待根 Span 加宽限窗口,再按错误、耗时和基线预算保留,并记录链路 是否不完整。保留链路写对象存储作为标准详情,同时写入白名单热索引用于 Trace ID 与有界条件 查询。我会分别计算采样前状态和采样后存储,在过载时先丢低优先级正常样本,并用金丝雀链路 验证传播、迟到 Span、采样偏差、租户隔离和可用区恢复。”
分步骤深入解答
步骤一:先确定 Span 契约与查询 API。
Span 记录至少包含:
Span {
tenant_id, trace_id, span_id, parent_span_id?, links[],
service, operation, kind, start_time, end_time, status,
resource_attributes, span_attributes, events[],
observed_at, schema_version, trace_flags, tracestate?
}网关从认证信息派生 tenant_id。它校验标识符长度与格式,拒绝超大记录,规范化获准的语义 字段,并限制属性数量、值长度、事件数、链接数和总字节数。平台同时保存事件时间和 observed_at:服务时钟用于展示时间线,采集器时间用于发现延迟与时钟偏差。SDK 应使用单调 时钟计算本地 Span 耗时;跨主机排序仍需要墙上时钟和因果边。
主要查询契约如下:
GetTrace(tenant_id, trace_id)
SearchTraces(tenant_id, start, end, service?, operation?, status?,
min_duration?, max_duration?, cursor?, limit?)GetTrace 返回 Span、链接、采样策略及版本、firstobservedat、lastobservedat 和完整性 警告。SearchTraces 必须带有界时间范围,并用游标而不是无限偏移分页。详情查询与二级检索 是两条不同访问路径,不应把一条宽链路记录复制进每个二级索引。
步骤二:传播上下文,同时不把它当作信任。
在 HTTP 中,SDK 提取并注入 W3C traceparent,其中包含版本、Trace ID、父 Span ID 和标志; tracestate 携带可选的厂商状态。消息生产者把相同传播字段写入消息元数据。接收端先校验 格式,再加入链路。无效上下文会新建链路并增加传播错误计数,不能污染现有键空间。
在互联网入口或跨租户边界,服务可以主动创建新的内部链路,并对获准的上游上下文增加 Link。 这样既保留关联,也不让外部调用者选择内部父节点或采样控制。Baggage 是另一种随上下文传播的 键值机制。它会扇出到每个下游节点,所以必须有白名单和大小限制,并剥离密钥及个人信息。
优先自动埋点常见 HTTP、RPC、数据库和队列库。Dapper 说明了公共库埋点为何能以更低应用 负担提高覆盖率。自定义 Span 适合业务边界,但平台仍要测量缺少预期服务端或客户端 Span 的 服务和路由。链路质量不会超过实际埋点和保留样本的完整度。
步骤三:让采集离开应用请求路径。
结束的 Span 先进入进程内有界缓冲区,再以压缩批次刷新到节点 Agent 或 Sidecar。Agent 使用 有界磁盘暂存应对短暂采集器故障,按指数退避加抖动重试,并维护不同优先级队列。应用完成 响应前,它不会同步等待中心确认。本地预算耗尽后,先丢低优先级正常遥测,并上报准确的丢失 数量与原因。
区域无状态网关认证 Agent、绑定租户、执行字节和 Span 配额、校验 Schema,再把接受的批次 追加到多副本持久消息流。确认只代表区域消息流已经持久接收,并不表示搜索已经可见。消费者 用 (tenantid, traceid, span_id) 加记录版本保证幂等;重复投递可以更新观测元数据,但不能 产生第二个 Span。
消息流按 (tenantid, traceid) 的稳定哈希分区,使一条链路只有一个决策者,又不要求全局 Span 顺序。尾部采样扩容时,第一层采集器可按 Trace ID 负载均衡到第二层有状态采样器。 OpenTelemetry Collector 也明确要求:同一 Trace 的所有 Span 必须到达同一个尾部采样实例。
步骤四:让每个容量边界都可复算。
采样前的工作负载是:
500,000 条工作流/秒 × 15 Span/工作流 = 7,500,000 Span/秒
7,500,000 Span/秒 × 700 字节/Span = 5.25 GB/秒
5.25 GB/秒 × 86,400 秒 = 453.6 TB/天原始数据这些是工作负载假设,不是实测压缩率。若把完整原始流送入中心尾部采样器,就必须先提供 5.25 GB/秒的接入能力,之后还要考虑副本、协议开销、重试、倾斜和故障切换余量。
假设普通路由产生 90% 的 Span,采用 2% 一致头部采样;关键路由产生 10%,以 100% 进入尾部池:
普通流:750 万 × 90% × 2% = 135,000 Span/秒
尾部池输入:750 万 × 10% = 750,000 Span/秒
采集器总输入:885,000 Span/秒 × 700 字节 = 619.5 MB/秒如果尾部策略平均保留候选 Span 的 10%,热存储接收 135,000 + 75,000 = 210,000 Span/秒,即 147 MB/秒、12.7008 TB/天,尚未计算压缩、副本、 索引和对象元数据。七天按未压缩等价量为 88.9056 TB。最终用生产形态的属性分布基准测试决定 压缩与节点数;这组算式只给出下界,并说明不同采样边界分别节省了哪部分成本。
步骤五:把尾部采样当作成败关键的有状态瓶颈。
组装器按租户和 Trace ID 保存部分状态:唯一 Span、最早开始、最晚结束、根节点完成状态、错误 状态、当前耗时、字节数和最后到达时间。同步链路在根 Span 结束且宽限时间过去后可作决定; 达到最大年龄、Span 数或字节数时也必须强制决策。长工作流要有独立策略,否则一条链路就能 无限占用内存。
决策顺序先为显式关键流、错误和高耗时链路预留容量,再在按服务与租户划分的预算内,用一致 概率样本补足基线。“保留全部错误”不是有界全局规则,因为事故期间错误率可能接近 100%。 各类别用令牌桶和硬字节上限控量;容量耗尽时进入可观测的降级策略,不能等到内存溢出。
采样器为迟到 Span 保存决策缓存。已保留链路的迟到 Span 可以追加,并标记链路已更新;已 丢弃链路的迟到 Span 继续一致丢弃。缓存过期后才到达的 Span 计为孤儿,不能创建一条误导性的 单 Span 链路。每条存储链路携带 complete、decision_reason 和迟到 Span 计数。拉长宽限 时间会提高完整度,也会增加内存、决策延迟和采样器故障影响的链路数量。
混合管道是最重要的陷阱。尾部逻辑只能在真正到达的链路中选择。如果上游 2% 头部采样在 下游错误出现前丢了某条普通链路,尾部阶段无法找回它。因此,确实要求基于错误保留的路由, 必须不经丢弃地进入尾部池;或者采用独立触发机制,并承认它无法重建过去。
步骤六:把标准详情与有界索引分开存储。
保留 Span 压实为不可变压缩对象,按租户与时间分区,每次链路修订都有 Manifest。Trace ID 目录把 (tenantid, traceid) 映射到对象位置和最新修订版,用于精确查询。近期对象可以缓存, 但对象层是派生索引的重建来源。
热检索索引每条链路只保存一行摘要:租户、Trace ID、根服务和操作、开始时间桶、耗时、状态、 所选服务集合或指纹、采样原因、完整性和对象指针。只有白名单字段建立二级索引。任意用户 ID、 SQL 文本、URL 和 Baggage 值保留在受保护详情中或被脱敏;默认索引这些字段会导致无界基数、 隐私暴露和写放大。
服务依赖图与延迟视图是保留样本上的流式聚合,必须标注为采样估计。概率已知时,采样权重可 支持一部分无偏计数估计;按错误与耗时偏置的尾部样本不会自动代表流量占比。指标系统负责 精确的集群级比率,链路用于解释单个因果路径。
步骤七:隔离租户并定义故障行为。
网关限制每个租户的每秒字节数、每秒 Span 数、并发部分链路数、查询并发和保留字节。分区键 包含租户身份,受监管租户可以使用独立加密策略,任何索引查询前都要先鉴权。某个租户的超大 链路或高基数属性,不能挤掉其他租户的采样状态。
网关或可用区故障时,Agent 重试另一个区域端点并使用有界暂存。持久消息流变慢时,接入控制 降低普通采样,并在有状态组装前拒绝超额字节。组装器故障后,消息流重放对应分区;检查点加快 恢复,幂等 Span 键吸收重复。索引故障期间,标准对象继续写入并累积索引积压。精确查询近期 数据时可以返回“已接收,正在建索引”,不能把索引未更新误报为不存在。
尾部池过载时,逐步降低概率基线、限制超大链路,再对受影响路由退化到确定性头部策略。控制面 金丝雀链路和一部分有界错误容量需要保留。平台要按租户、服务、可用区和策略监控已接收、已 丢弃、重试、过早淘汰、迟到、孤立和完成索引的 Span。
步骤八:验证事实是否可信,而不只验证吞吐。
传播测试覆盖合法、缺失、格式错误和未来版本 Header,跨租户与互联网边界,Baggage 限制, 队列、重试、扇出、汇聚和批次 Link。组装测试注入重复、乱序、缺父节点、迟到、超大和永不 结束的链路。采样测试证明整条链路决策一致、预算不超限、概率决策可复现、错误与耗时策略正确, 以及上游头部丢弃确实不可恢复。
压测应保留生产形态的链路大小和租户倾斜,并测量 SDK 开销、Agent 丢失、网关准入、消息流 积压、活跃链路内存、决策延迟、保留字节、索引延迟、Trace ID 查询和条件检索。故障测试应 移除一个可用区、在热点分区期间重启组装器、暂停对象存储、耗尽租户配额,并从标准对象重建 热索引。
持续发送图结构已知的合成金丝雀工作流经过每个区域。预期 Span 丢失、父子关系变化、30 秒 检索新鲜度超标或 Trace ID 查询超 SLO 时都要告警。采集器进程健康,不能证明链路完整或可查。
高质量示范回答
“我会先说明链路是 Span 的因果图,不是一袋日志。每个 Span 包含租户绑定的 Trace ID、Span ID、父节点或 Link、时间、状态、资源、有界属性、事件和观测时间。服务通过 HTTP 或消息元数据 传播经校验的 W3C 上下文。在不可信边界,我会新建内部链路并加 Link,因为链路上下文用于关联, 不用于鉴权。
“Span 通过进程内有界缓冲区和本地 Agent 离开请求路径。Agent 批处理、压缩、短时落盘,并在 必要时丢有计数的低优先级数据,不阻塞业务。区域网关认证租户、执行 Schema 与配额,再写入 多副本消息流。按租户和 Trace ID 分区,使每条链路到达一个组装器;按 Span ID 幂等处理重试。
“原始负载是每秒 750 万个 Span、5.25 GB。我不会无意中把全部流量送进尾部采样器。普通路由 用 2% 一致头部采样,受控的 10% 关键路由完整进入尾部池,因此采集器输入是每秒 88.5 万个 Span,即 619.5 MB。如果尾部池保留 10%,存储每秒接收 21 万个 Span,原始量约 12.7 TB/天, 尚未计算索引与副本。这些数是基准测试输入,不是压缩率承诺。
“组装器是难点。它保存部分链路,等待根节点加宽限窗口,并按年龄、Span 数和字节数强制决策。 先为关键、错误和慢链路保留有界容量,再用一致概率基线填充按服务预算。它缓存迟到 Span 的 决策,并标记不完整链路。上游 2% 头部丢弃无法在下游补回,所以真正要求错误感知保留的路由, 必须未采样地进入尾部池。
“保留详情进入压缩对象存储,并由 Trace ID 目录定位。独立热索引每条链路只存一行摘要,且 仅索引白名单内的服务、操作、状态、耗时和时间字段,从而控制基数并支持重建。故障时 Agent 使用有界暂存,消息分区可重放,搜索故障期间对象仍可写入,过载时先牺牲正常基线。验收会覆盖 上下文边界、重复与迟到 Span、采样偏差与上限、噪声租户隔离、可用区故障、重建和端到端金丝雀。”
常见错误
- 说“先采 2%,再由尾部保留所有错误” → 第一阶段已经删除 98% 候选链路,其中包括之后
才发生错误的链路 → 让受保护路由不经丢弃进入尾部池,或降低承诺。
- 不按 Trace ID 把 Span 哈希给不同采集器 → 每个尾部采样器只看到碎片,决策会偏 →
让 (tenant, trace_id) 的所有 Span 到达同一决策者。
- 无限等待完整链路 → 异步工作没有通用结束标记,状态会无限增长 → **使用根节点加宽限、
最大年龄和大小、显式工作流策略及不完整标记。**
- 把
traceparent当作身份或权限 → 外部调用者可以自行选择关联字段 → **独立认证租户,
并在信任边界新建带 Link 的链路。**
- 把 Baggage 或任意属性加入所有索引 → 基数、写放大和敏感信息暴露均会无界增长 →
只索引白名单字段,并限制或脱敏传播值。
- 同步发送 Span 到中心采集器 → 遥测故障会提高业务延迟或降低可用性 → **通过本地有界队列
异步批处理,并准确暴露丢失。**
- 只存每个 Span,不建链路摘要 → 服务、耗时与错误查询需要扫描海量详情 → **保留标准详情,
并为每条链路建立一行有界且可重建的摘要。**
- 把有偏尾部样本当成精确流量分布 → 错误和耗时规则本来就会过度代表异常链路 → **发布
采样元数据,精确聚合比率由指标系统承担。**
- 只测试采集器存活 → 上下文断裂、Span 缺失和索引延迟仍可能不可见 → **运行图结构已知的
金丝雀,并断言完整性、采样、新鲜度和查询 SLO。**
追问及应对
追问一:产品现在要求保留每一条错误链路,但中心接入预算不变,怎么办?
两项要求可能冲突。错误通常要等下游 Span 执行后才知道,源端头部采样无法保证保留。先量化 最大原始 Span 速率和中心尾部容量。若预算容纳不了所有候选路由,就把保证收窄到有界关键路由, 扩容,或增加应用错误触发器,让之后的采样率升高,同时承认过去丢弃的 Span 无法重建。 “全部错误”也必须有字节上限,因为事故期间几乎所有流量都可能报错。
追问二:一条消息汇总了来自 10 条生产者链路的事件,消费者 Span 应该选哪个父节点?
没有一个父节点能表示十个独立原因。为消费者或批处理创建对应工作流 Span,并在 Link 数量上限 内链接十个生产者上下文。如果批次本身有一个投递上下文,可把它作为父节点,把单条输入保留为 Link。查询与可视化需要支持有向无环图,并显示 Link 被截断的信息;把消费者 Span 复制到十棵 树会扭曲耗时与存储。
追问三:尾部采样器总在决策窗口前淘汰部分链路,如何排查?
对比活跃链路数量与字节、链路大小分布、到达延迟、热点分区、决策年龄、过早淘汰计数和租户 倾斜。直接加长等待窗口可能让内存压力更差。先限制超大链路、隔离噪声租户、在不破坏 Trace 亲和性的前提下拆分分区,并降低正常基线;再依据迟到 Span 的实际价值扩容或缩短窗口。通过 影子策略衡量改动对错误、慢链路和完整度的影响。
追问四:搜索故障两小时,但接入和对象存储健康,API 应返回什么?
继续写标准链路对象和持久的索引变更积压。如果 Trace ID 目录仍可用,GetTrace 可以走该 路径;二级搜索返回带 as_of 的陈旧数据或明确的暂不可用状态。不能因为摘要尚未进入索引, 就声称新接收的链路不存在。恢复后幂等重放并比较数量与延迟;若积压损坏,则从对象重建索引。
追问五:怎样证明采样没有让某个低流量服务完全消失?
在共享剩余全局预算前,先为每个服务或操作保留最小基线预算。每条保留链路记录概率与决策原因, 对“有流量却没有保留链路”的服务告警。合成金丝雀独立于概率验证全路径。将链路覆盖情况与 指标请求数比较;某服务有请求却没有 Span 时,分别区分缺少埋点、传播失败、配额丢弃、头部决策、 尾部决策和索引丢失。