分布式系统(E08):从微秒 RPC 到尾延迟追踪
一次请求的平均延迟很低,p99却不断升高。trace瀑布图里某个服务占了四十毫秒,于是它被判为根因。这个诊断可能同时错在三处:请求要等待的完成条件没有写清,压测器在慢响应期间停止发新请求,跨主机时间戳又被当成了因果顺序。
eRPC说明了专用数据中心RPC如何把软件开销压低;尾延迟研究解释了罕见慢节点怎样进入用户请求;分布式追踪负责保留单次执行的关系证据。这三者解决的问题相邻,却不能彼此替代。
分布式系统(E07):控制面共识与数据面复制延迟样本由三段协议共同产生
端到端延迟不是某个函数自带的属性。请求路径规定要等哪些分支,负载发生器决定请求何时进入,观测管线再决定哪些阶段留下记录。
flowchart LR
A[到达模型<br/>何时产生请求] --> P[执行路径<br/>串行、fan-out、quorum]
P --> Q[队列与服务<br/>等待、计算、重试]
Q --> O[观测管线<br/>span、metric、profile]
O --> J[结论<br/>分位数与归因]
A -.遗漏计划到达.-> B[coordinated omission]
O -.缺span或时钟偏移.-> C[错误归因]
| 诊断模式 | 必须保留的证据 | 常见误判 |
|---|---|---|
| 路径代数 | 串行求和、并行完成条件 | 把叶子平均值当端到端值 |
| 到达与服务分离 | 计划到达、实际开始、完成时刻 | 只记录成功开始的请求 |
| 因果先于墙钟 | trace-id、parent、link、主机时钟条件 | 按跨机start time排序 |
| 缺口不作否定证据 | 采样、传播、导出与存储状态 | “没有span,所以没有执行” |
这组模式的共同公式是:结论 = 路径语义 + 负载条件 + 可见证据。少一项时,结论必须降级为观察或待检验假说。
eRPC的低延迟成立在数据中心边界内
NSDI 2019论文把eRPC定位为通用的数据中心RPC库,摘要明确说它处理丢包、拥塞和后台工作。公开实现支持Ethernet/UDP、InfiniBand和RoCE;高性能路径依赖用户态事件推进、快速NIC、hugepage、NUMA配置和性能构建。
flowchart TB
APP[应用请求处理器] --> EL[用户态事件循环]
EL --> SES[session与请求状态]
SES --> TR[传输、重传与拥塞处理]
TR --> NIC[快速NIC<br/>Ethernet / InfiniBand / RoCE]
OS[hugepage、NUMA、性能构建] --> EL
OS --> NIC
论文摘要报告小RPC单核最高约一千万次每秒、大RPC单连接约75Gbps,并给出三副本Raft在传统UDP Ethernet上的微秒级结果。固定提交README把小RPC限定为32-byte、大RPC限定为8MB且两端各一核;其中Raft结果写5.3μs,论文摘要写5.5μs。两处材料来自同一研究团队,数字还存在口径或版本差异,因此它们只能说明论文实验条件下的软件路径很短,不能当作本机或云环境的性能承诺。
“general”也有边界。这里的故障模型是数据中心内的丢包、拥塞和端点状态变化,不是Byzantine节点、永久网络分区或跨地域WAN。快速路径还要求应用持续推进事件循环。处理线程长时间不轮询、NIC或内存配置不匹配、接收端已经崩溃时,低软件开销不会自动提供活性。
RPC完成只说明库在其接口契约内结束了一次调用。它不能证明数据库事务已经提交、外部支付只执行一次,客户端超时也不能证明服务端没有执行。若调用方在超时后重试,稳定请求ID、结果回放、目标资源幂等或fencing仍由应用层负责。第01篇的请求身份和第25篇的提交边界在微秒RPC上仍然成立。
串行路径累加,fan-out取决于完成条件
串行路径的端到端时间近似为各段等待和服务时间之和:
L_serial = Σ(queue_i + service_i + transport_i)
四跳路径中,即使前三跳始终稳定,只要第四跳有少量45ms请求,端到端分布就会带上这一尾部。局部p99和端到端p99不能机械相加,因为各段慢事件可能相关,也可能落在不同请求上;可用的是逐请求关键路径,而不是分位数的代数和。
all-of fan-out要等全部叶子时,请求延迟接近最大叶子延迟:
L_fanout = max(L_1, L_2, ..., L_n)
若叶子独立,单叶以概率p超过阈值,则至少一个慢叶子的概率为1 - (1-p)^n。p=1%、n=100时约为63.4%。共享交换机、同一机架队列、GC或热点分片会制造相关性,独立公式此时只是参照。quorum、first-success和允许部分结果的API也有不同的完成函数。
flowchart LR
R[一次请求] --> A[leaf 1]
R --> B[leaf 2]
R --> C[leaf ...]
R --> D[leaf n]
A --> M{完成条件}
B --> M
C --> M
D --> M
M -->|all-of| MAX[取最大完成时间]
M -->|quorum| K[取第k个完成时间]
M -->|first-success| MIN[满足谓词的最早完成]
hedged request会在首个副本迟迟未完成时发第二份请求,取先完成的合法结果。它用额外工作换尾部改善。若系统已接近饱和,额外副本可能加重队列,使更多请求触发hedge,形成正反馈。阈值、取消语义、幂等边界和额外负载必须一并报告。
排队会被负载发生器隐藏
服务时间与响应时间不是同一指标。响应时间包含请求抵达后等待前序工作的队列时间。利用率接近一时,服务时间分布不变,队列尾部也可能陡增。
开放模型按外部日程产生请求,不等待前一请求完成。闭合模型保持固定数量的在途客户,一个完成后才发下一个。闭合模型适合真实的“思考后再请求”用户,也适合固定并发压测;但它不能代表固定到达率。响应变慢时,闭合发生器会自动减小到达率,最需要观察的慢时段反而产生更少样本。
sequenceDiagram
participant O as open-loop clock
participant C as closed-loop client
participant S as single server
O->>S: t=0 planned request
O->>S: t=6 planned request
O->>S: t=12 planned request
C->>S: request 1
S-->>C: slow response
Note over C,S: response前没有request 2
C->>S: request 2
coordinated omission描述的是测量器与被测系统发生协调:慢响应期间本应出现的计划请求没有进入样本。HdrHistogram的预期间隔修正会为一个长观测补入逐级下降的反事实值,用来做遗漏敏感性分析。补样不是实际请求,也不会恢复真实队列轨迹。
超时同样不能从延迟样本中删除。只统计成功请求会右删失最慢的一批观测,让p99随着故障增加反而变好。报告至少要同时给出目标到达率、实际吞吐、在途数、成功数、错误数、超时率、预热和测量窗口。
分位数也不能跨实例求平均。实例A有99个1ms和1个1000ms样本,nearest-rank p99为1ms;实例B的100个样本都是100ms,p99为100ms。两个局部p99平均得到50.5ms,而合并200个原始样本后的p99是100ms。兼容直方图可以先合并桶再估算;客户端summary已经算出的quantile没有这种可加性。
trace保存关系,时间戳仍受时钟约束
OpenTelemetry把trace定义为span构成的因果结构。span至多有一个parent,Link可以表达批处理或消息场景中的额外关系。跨进程连续性依赖propagator把SpanContext注入carrier,并由接收端正确提取。W3C traceparent和tracestate规定互操作格式,但trace-id不是身份、授权或完整性证明。
flowchart TB
R[root span] --> G[gateway]
G --> W[worker]
W --> Q[queue_wait]
W --> H[handler]
H --> DB[database]
C1[client clock offset 0] -.时间戳.-> R
C2[worker clock offset -25ms] -.时间戳.-> W
C3[database clock offset +40ms] -.时间戳.-> DB
span时间戳是测量字段。OpenTelemetry没有承诺各节点时钟同步或误差上界,因此跨主机start time排序不是happens-before证明。一个database子span可以因为时钟偏移显示在root结束之后;parent关系仍表明它属于handler调用,持续时间是否可信则要看各主机计时源和SDK实现。
队列必须被插桩才能被归因。worker span持续62ms,其中queue_wait为40ms、handler为22ms;若queue span缺失,瀑布图只会留下40ms未归因空白。把这段空白归给网络、handler或数据库都超出了证据。
传播断裂会把一次执行拆成两个trace-id。采样丢弃、未插桩、span未结束、export失败和存储丢失也会造成相似缺口。缺span因此不能证明某段代码没有执行。
head sampling与tail sampling看到的未来不同
OpenTelemetry SDK在创建span时调用sampler。此时可用的输入包括parent context、TraceId、名称、SpanKind、初始attributes和links;稍后产生的错误、最终duration和后加属性还不存在。head sampling可以依据入口信息稳定降载,却天然无法直接选择“后来变慢”的请求。
tail sampling把决策推迟到collector一侧,因而能使用已经产生的duration、status或属性。代价是等待窗口、按trace路由、内存占用和迟到span处理。tail决策也不能保证看到了完整trace:传播断裂、export失败和超过等待窗口的span仍会缺失。
recording与sampled也不是同一状态。SDK可以记录但不导出;传播头里的sampled位更不表示后端已经持久化并可查询。概率采样只有在纳入概率已知并满足相应条件时才能加权估计总体。OpenTelemetry v1.61的一致概率采样章节仍标为Development,不能冒充所有语言SDK的稳定共同保证。
trace适合回答“这一次请求经过哪里、可见等待落在哪段”。总体p99、错误率和流量需要metrics,CPU热点和代码栈分布需要profiling。三种信号可以关联,但单条trace不能证明总体分布,采样trace也不能单独估算罕见事件频率。
实验:同一慢期的三种观察
本篇使用纯Python确定性模型,不运行eRPC、OpenTelemetry或Jaeger。运行入口如下:
1 | |
模型中的四跳串行请求通常为8合成毫秒,每40个请求有一次第三跳变成45毫秒,端到端p99随之变成51。fan-out叶子的固定慢比例为0.1%;宽度1的请求p99为2,宽度100的all-of请求p99为50。这里的算术日程不是独立随机样本,论文概率公式只作结构参照。
到达模型使用每6毫秒一个计划请求,服务通常耗时5毫秒,每200个请求有一次耗时100毫秒。固定到达的open-loop p99为98;单并发closed-loop原始p99为5,其响应门控吞吐约为182.65次每秒,已经不再服从open-loop的166.67次每秒计划到达率。按6毫秒预期间隔补样后的p99为88,但输出把这些记录标为synthetic_not_actual_requests。
trace场景为三台主机设置0、-25和+40毫秒时钟偏移。按记录start time排序得到worker → queue_wait → root → gateway → handler → database,parent链则从root走向database。删除queue span后,worker内部留下40毫秒未归因时间;切断context传播后,同一次执行分成两个trace-id。
flowchart TD
S[p99升高] --> L{负载条件相同吗}
L -->|否| LM[先对齐offered load、吞吐、超时]
L -->|是| P{完成条件与路径相同吗}
P -->|否| PM[区分all-of、quorum、重试与hedge]
P -->|是| T{trace关系完整吗}
T -->|否| TM[查传播、采样、export和时钟]
T -->|是| Q{队列与服务分开吗}
Q -->|否| QM[补queue/连接池/线程池证据]
Q -->|是| H[形成可证伪瓶颈假说]
完整输出见观察结果,环境与运行命令见实验证据,验证边界见验证说明。
安全性、活性与失败路径
性能诊断也有安全性要求:不能把不同负载的p99直接比较,不能平均局部分位数,不能按跨主机墙钟伪造因果顺序,不能把缺span写成未执行。这些规则防止错误结论进入容量、回滚和故障决策。
活性依赖的条件更多。RPC两端要继续运行和轮询,网络最终能传递足够报文,队列不过载,重试预算没有耗尽;追踪还要求context传播、SDK结束span、exporter取得资源、collector和存储继续接收。业务请求成功与观测数据最终出现是两条独立路径。
丢包会触发传输层恢复,端点崩溃会留下结果未知,永久分区可以让调用一直无法完成。超时重试可能重复业务副作用,hedge也会让多个副本同时执行。服务恢复后,新的快速样本不能抹掉故障窗口中的超时;观测后端恢复也不会补回所有丢失span。报告必须把这些删失和缺口保留下来。
工程成本与适用边界
低延迟RPC把成本移到CPU核、内存布局、NIC队列、轮询和部署同质性。追踪则消耗上下文传播、span缓冲、导出带宽、collector内存和存储索引。提高采样率可能改善稀有慢请求的可见性,也会改变被测系统和观测系统自身的负载。
诊断顺序应从测量契约开始:固定请求完成条件和到达模型,保留超时及错误,再用trace缩小单次路径,最后用queue metric和profile验证瓶颈。eRPC可以缩短RPC软件路径,却不能删除排队、应用提交、重试副作用和观测缺口。
模式速查表
| 看到的现象 | 先检查 | 可证伪动作 |
|---|---|---|
| 平均值稳定、p99升高 | 样本数、超时、负载阶段 | 合并原始样本并按窗口重算 |
| fan-out越宽越慢 | 完成条件与叶子相关性 | 改成quorum或分组输出对照 |
| 压测p99异常漂亮 | open/closed、计划发送时刻 | 用固定到达回放同一服务轨迹 |
| trace显示某服务最慢 | queue span、时钟偏移、缺口 | 补等待段并按parent重建DAG |
| trace中没有某次调用 | 传播、采样、export、存储 | 用业务日志或请求ID交叉核验 |
| RPC很快但业务仍超时 | 连接池、队列、提交与下游 | 分开记录运输、排队和外部效果 |
练习
- 某请求all-of等待50个叶子,每个叶子独立地以0.2%概率超过40ms。计算至少一个慢叶子的概率。若接口改为等待任意45个成功结果,原公式为什么不再够用?
- 修改实验,把100ms慢服务改成超时并从成功直方图删除。记录p99和超时率,再解释为什么只看成功p99会得到方向错误的结论。
资料
- eRPC: Datacenter RPCs can be General and Fast
- eRPC fixed-commit README
- The Tail at Scale
- Open Versus Closed: A Cautionary Tale
- HdrHistogram 2.2.2 README
- Prometheus: Histograms and summaries
- OpenTelemetry Trace API v1.61.0
- OpenTelemetry Context Propagators v1.61.0
- OpenTelemetry Trace SDK v1.61.0
- OpenTelemetry Collector tail sampling processor v0.136.0
- W3C Trace Context Level 1
- Google SRE Book: Monitoring Distributed Systems
- Dapper: a Large-Scale Distributed Systems Tracing Infrastructure
本篇是“分布式系统:从基础到共识与工程”系列最后一篇选修。第E07篇把控制面共识与业务数据面分开,本篇再把执行路径、负载生成和观测证据分开。系列的协议、系统和实验到此形成闭环;真实生产结论仍需在目标硬件、负载和故障条件下重新验证。
