本文是 2025年10月出版的《Latency: Reduce delay in software systems》第二章的读书笔记

时延的定律#

Little’s Law#

利特尔法则(Little’s Law)是排队论和运筹学中最经典、最直观,但也最具威力的定律之一。

Little’s Law 的数学表达式如下所示: $$L = \lambda \times W $$

  • $L$ (Inventory / Queue Length):系统中平均拥有的“东西”数量(比如排队的人数、仓库的库存、处理的任务)
  • $\lambda$ (Throughput / Arrival Rate):单位时间内进入或离开系统的平均数量(吞吐率/到达率)
  • $W$ (Wait Time / Cycle Time):一个“东西”在系统里停留的平均时间(前置时间/等待时间)

需要注意的是,Little’s Law 描述的是一个稳态系统:在一个稳定的系统中,存货数量 = 到达速率 x 停留时间

举个例子,一个咖啡馆内,平均每分钟有 2 个客人进店($\lambda$),每个客人在店里从进门到拿咖啡走人平均停留 10 分钟($W$)。那么,任一时刻店中的停留人数 $L = \lambda \times W$ 为 20 人,即在这 10 分钟内店中累积的人数,第 11 分钟到达速率等于离开速率,系统达到平衡。

在并发系统中,$\lambda$ (吞吐量) 可以理解为每秒处理多少个请求(QPS/TPS),$W$ (响应时间) 可以理解为每个请求平均花多少秒(Latency),$L$ (并发数) 可以理解为在任一给定时刻,已经进入系统但尚未处理完成的请求总数。

在计算机系统中,每一个 $L$ 都不是免费的,它必须占用某种物理资源:

  • 内存:每个请求可能对应一个线程栈(Thread Stack)、一个对象、一块 Buffer。
  • 连接:每个请求占用一个 TCP 连接、一个数据库连接池名额。
  • 句柄:每个请求可能占用一个文件描述符。
  • CPU 调度:如果 $L$ 很大,CPU 需要在成千上万个线程间切换,产生巨大的上下文切换开销。

而每个系统都有一个物理上的最大并发容量($L_{max}$)

  • 如果你的线程池大小是 200,那么你的核心处理能力 $L$ 被限制在 200 左右。
  • 当请求不断涌入,如果 $L$ 超过了系统的处理能力,多出来的请求就会进入队列(Queue)

这时的公式演变为:

$$L_{total} = L_{processing} + L_{queuing}$$ 所以,当观察到 $L$ 增大,超过了 $L_{max}$,响应时间 $W$ 会迅速飙升(增加了排队时间)。常见的场景是 system load » cpu core 时,出现的系统雪崩。

如何利用 Little’s Law#

  1. 通过系统的平均响应时间和期望的吞吐量,计算出合理的线程池大小
  2. 如果系统能够承受的最大并发数(资源上限)是 $L_{limit}$,那么当 $\lambda \times W > L_{limit}$ 时,需要采取措施:
    1. 降低 $\lambda$:限流(Rate Limiting),拒绝多余请求
    2. 降低 $W$:降级(Fallback),直接返回默认值或缓存,不调用耗时的下游服务
  3. 异步编程(如 Java 的 CompletableFuture, Go 的 Goroutine, Node.js)的本质就是极大地降低了单个 $L$ 占用的资源成本
    1. 在同步模型中,一个 $L$ 占用一个操作系统线程(1MB 内存)。
    2. 在异步模型中,一个 $L$ 只占用一个状态对象(几KB 内存)。
    3. 因此,异步系统可以支撑大得多的 $L$(并发数),从而在 $W$ 变长时,系统不至于因为资源耗尽而崩溃。

在底层视角下,$L$ 是压力,$\lambda$ 是动力,$W$ 是阻力。高并发优化的核心,就是在 $L$(资源消耗)可控的前提下,通过降低 $W$(阻力)来提升 $\lambda$(动力)

Amdahl’s Law#

阿姆达尔定律(Amdahl’s Law) 是计算机科学、并行计算以及系统性能优化领域中一个非常重要的定律。它由 IBM 的计算机架构师吉恩·阿姆达尔(Gene Amdahl)在 1967 年提出。

简单来说,这个定律描述了:在一个任务中,即使你增加了再多的并行计算资源(如 CPU 核心),系统的整体加速比(Speedup)仍然受限于该任务中“必须串行执行”的部分。 $$S = \frac{1}{(1-p)+\frac{p}{n}}$$ 其中:

  • $S$ (Speedup):总的加速比。
  • $p$ (Parallel fraction):程序中可以并行化的部分所占的时间比例(0 到 1 之间)。
  • $1−p$ (Serial fraction):程序中必须串行执行的部分所占的比例。
  • $n$ (Number of processors):执行任务的处理器(核心)数量。

4a6f97960712b09039fde1069b76f53e_MD5

Amdahl’s Law 的启示#

收益递减律(Diminishing Returns)

随着处理器数量 $n$ 的增加,加速比的提升速度会越来越慢。当 $n$ 趋于无穷大时,最大加速比限制在: $$S_{max} = \frac{1}{1-p}$$ 这意味着,如果一个程序有 10% 的代码必须串行运行,那么无论你用多少个核心,你的最高加速比永远无法超过 10 倍。

串行部分是“致命伤”

定律强调了系统性能的瓶颈往往在于那小部分无法并行的代码。如果想显著提升性能,与其增加 CPU 核数,不如想办法减少串行部分的比例(即降低 $1-p$)

OSDI ‘25 论文 Principles and Methodologies for Serial Performance Optimization 总结了提高串行部分效率的三个原则:

  • task removal
  • replacement
  • reordering

并在这三个原则之上,提炼了 8 个可实践的方法(利用一个或多个上述原则达到提高串行效率的目的):

  • batching
  • caching
  • precomputing
  • deferring
  • relaxation
  • contextualization
  • hardware specialization
  • layering

567e4e6ff62ea8a4ea3ff4ef5c951406_MD5

时延分布和长尾时延#

平均时延能够衡量一个系统的整体性能,但是往往会隐藏掉系统的长尾时延。这也是为什么在关注时延时,我们更多会关注时延的分位指标,如 P95,P99,P99.9 等。

1e4a6d30b31e83fe6bc6ae54a143745a_MD5

Dean 和 Barroso 在 Google 2013 年的论文 “The Tail at Scale” 中介绍,当我们尝试使用分区和并行执行来降低 service latency 时,长尾时延往往会主导整个 service latency。

论文中举了一个例子,service 并行向多个服务器发送了子请求,等待它们返回所有的结果。假设每个子请求需要花费 10 ms 来响应,但是其中会有偶发时延,需要花费 1s(长尾时延)。假设每 100 个请求会有 1 个请求遇到这种情况,service 需要发送 100 个子请求完成(fanout count 100,处理一个请求需要与 N 个其它组件通信,称作 fanout count N)。那么大约会有 63% 的用户会受到这个长尾时延的影响($1 - 0.99^{100} = 0.63$)。

用户可能比我们想象中更容易遇到组件长尾时延带来的体验下降。

时延产生的源#

物理限制#

时延产生的最终限制是物理规则。不管怎么调优,光在介质中的传播时延是理论时延的极限。因此,通过就近访问原则(同可用区、同机房、同 TOR)降低链路物理距离是降低时延的有效手段。

CPU 和硬件#

CPU 和硬件带来的时延开销通常涉及到缓存和时钟频率,比如 CPU 的多级缓存,在 cache miss 时造成 CPU 执行指令的等待时延,分支预测失败导致重新取指,跨 NUMA 访问,超线程产生的竞态,CPU 降频等。

虚拟化层#

如果运行了虚拟化层(如 KVM、Docker 等),虚拟化的资源竞争也是时延产生的重要因素之一。如:

  • Hypervisor 的开销
  • 不同虚机之间的资源抢占
  • 网络和 I/O 虚拟化带来的时延
  • 硬件仿真

操作系统和驱动层#

  • 上下文切换
  • 中断处理
  • 驱动中的 queuing、batching 等操作

运行时#

编程语言的运行时带来的时延开销,常见的如 JIT,GC 开销等。

应用层#

最后一层就是应用软件本身产生的处理时延。比如为了数据一致性、高可用、错误容忍产生的额外时延,不合理的算法产生的处理时延等等。

时延的累积#

时延大致可以分为三类:

  • 传输时延:数据在物理链路中传递产生的时延
  • 处理时延:数据处理产生的时延
  • 排队时延:数据在等待被处理时产生的时延

请求的总时延大致就是这三类时延的组合。而根据任务的处理模式,时延的累积又分为如下几种:

  • 串行累积(Serial compounding):时延组合之间存在前后依赖关系,所以总时延是各个部分时延的累加。
  • 并行累积(Parallel compounding):时延组合可以并行运行,那么总时延是所有部分时延中值最大的一个。比如 MapReduce 任务在 Map phase 进行的处理时延。如果这个最长时延很长,即便其它的任务处理得很快,总体时延仍不会短。
  • 多数共识累积(Quorum compounding):常见的分布式共识算法如 Raft、Paxos 等,时延不需要等到所有的节点达成共识,只需要多数节点达成共识即可(N/2)+1。

时延的观测#

Observer Effect#

观察者效应(Observer Effect)是一个跨学科概念,其核心含义是:“观察或测量一个系统的行为,必然会不可避免地改变这个系统。” 因为只要我们参与其中,我们就是系统的一部分。

如何缓解 Observer Effect#

缓解 Observer Effect 的核心思路是:

  • 减少交互: 尽量不触碰运行中的数据(采样、eBPF)。
  • 解耦时间: 不要让观察行为阻塞主逻辑(异步日志)。
  • 时空平移: 将观察行为转移到离线环境或镜像环境(Core Dump、影子流量)。

最好的观察者应该像一只“墙上的苍蝇”(Fly on the wall),存在但隐形。

Coordinated Omission#

协调遗漏(Coordinated Omission)是性能测试(尤其是延迟/响应时间测试)领域中一个非常重要且常见的陷阱。它指的是当被测系统发生延迟时,测试工具也无意中减慢了发送请求的速度,从而导致测量结果看起来比实际情况要好得多。 之所以叫“协同(Coordinated)”,是因为被测系统的阻塞测试工具的发送节奏之间产生了一种无意的“步调一致”。系统慢了,工具也跟着慢了,导致那些本该在阻塞期间发生的、排长队的请求被“遗漏”掉了。

举个例子,测试一个每秒处理 100 个请求的系统。测试程序每 10ms 发送一个请求。理想状态下,第 0ms 发送第一个请求,第 10ms 收到响应后发送第二个请求。平均时延是 10ms。

当系统出现阻塞时,第一个请求在 1000ms 才返回,如果测试工具等待前一个请求返回后再发送第二个请求,那么第二个请求会在第 1000ms 发送,1010ms 收到响应。平均时延是 505ms。

但是真实情况是,第一个用户的请求阻塞后,第二个用户不会等到第一个用户完成响应再发送请求。第二个请求原本是在第 10ms 发送,所以第二个请求的时延是 990ms(等待第一个请求完成)+ 10ms(处理时延)= 1000ms。平均时延是 1005ms。测试工具的结果完全失真。

Coordinated Omission 会导致高分位(P99,P999)严重失真,掩盖了最坏的情况。另外,由于压力源会受到阻塞的影响给不到被测系统足够的压力,还会造成吞吐量假象。

如何避免#

要避免 Coordinated Omission,测试工具必须采用**“开放系统模型”(Open System Model)**:

  1. 独立调度: 测试工具应该按照预定的频率(比如每 10ms 一个)发送请求,而不管前一个请求是否已经返回。
  2. 纠正计时: 记录时间时,应该基于“计划发送时间”计算,而不是“实际发送时间”。
  3. 使用支持修复该问题的工具:
    • wrk2:由 Gil Tene 改进,专门修复了 Coordinated Omission。
    • Hyperfoil
    • JMeter:需要正确配置(如使用非阻塞模型或专门的插件)。
    • HdrHistogram:一种用于准确记录和分析延迟分布的库,考虑了这种补偿。

时延的可视化#

常见的时延分布可视化方式有两种:

  • Histogram(直方图):将数据范围划分为若干个连续的区间(Bins),并统计落入每个区间内的样本数量或频率。它本质上是对概率密度函数 (PDF) 的一种估计。
  • eCDF (经验累积分布函数):对于每一个观测值 $x$,计算小于或等于 $x$ 的样本占总样本量的比例 $F(x) = \frac{count(X \le x)}{n}​$。它是一个从 0 到 1 递增的阶梯函数,是对累积分布函数 (CDF) 的直接估计。

CDF(累积分布函数 Cumulative Distribution Function)是基于概率密度的积分,来源于数学公式和参数:$F(x) = P(X \le x)$。而 eCDF(Empirical Cumulative Distribution Function)是基于实际观测点的样本统计量。

直方图的优势在于:

直观展示分布形状: 直方图能够非常直观地展示数据的双峰性(Bimodality) 或多峰性。例如,在双层缓存系统中,直方图可以清晰地显示出本地缓存和远程缓存产生的两个明显峰值。 • 易于识别众数:用户可以一眼看出数据最集中的区域(即“尖峰”出现的位置)。

劣势:

受限于分桶(Bucketing): 直方图必须将数据分配到不同的“桶”中。分桶会导致数据丢失,且具体的数值会被遮蔽,使得用户难以精确识别峰值的具体位置。 • 难以估算占比: 若想了解某个区间内数据占总量的比例,用户必须进行复杂的 “曲线下面积”心理估算,这在直方图上很难准确实现。 • 难以读取百分位数: 在直方图上直接读取如 P99 或 P75 等百分位数值是非常困难的

382e8ea6998cf88b2119d4fe23eb6812_MD5

eCDF 的优势在于:

精确读取百分位数: eCDF 可以被看作是直方图的累加或积分。用户可以直接在 Y 轴找到对应的百分比(如 0.65 或 0.75),向右平移触碰曲线后,在 X 轴直接读出精确的数值。 • 清晰展示相对贡献: eCDF 极易展示不同部分的相对占比。例如在缓存系统中,如果第一个峰值对应 70% 的数据,曲线会在 0.7 处发生转折,这让用户能立刻判断出缓存命中率约为 70%。 • 无分桶限制: eCDF 不需要分桶,因此可以无损地缩放(Zoom)。无论是在 X 轴还是 Y 轴进行缩放,曲线的形状都不会改变,这使得观察尾部数据(Tail) 的细节变得非常容易。 • 数学应用广泛: eCDF 方便计算机生成基于实测分布的随机数,也容易实现离群值(Outlier)检测(例如查询某个特定值在分布中处于什么百分位)。 • 信息无损转换: 从 eCDF 转换回直方图非常简单,但从直方图转换回 eCDF 则会因为分桶导致的信息丢失而变得困难。

劣势:形态不如直方图直观: 对于习惯看“峰值”和“谷值”的用户来说,eCDF 的曲线形态可能需要一定的适应过程,它不如直方图那样能瞬间反映出分布的形状特征。

4fa6453b3fc13338dc0d39941dcd60b5_MD5