Latency 与 Throughput

Latency 看单个请求多久完成,throughput 看单位时间完成多少。用 p50/p99 分布而不是平均值描述 latency,用 Little's law 把并发、吞吐和延迟连成一个式子,再讲 batch、排队、fan-out 怎么让两者此消彼长,附算例、翻车表和面试答法。

这一章回答三个问题:latency 应该用什么数字描述、throughput 的上限由什么决定、为什么把吞吐推高之后延迟会突然变差。

latency 是一次操作从开始到完成所花的时间。

throughput 是单位时间内完成的操作数量。

通常目标是:在可接受 latency 下最大化 throughput。例如在 clustering 场景里,active-active 通过更多 nodes 并行处理提升 throughput,也能改善 response time。但如果 node-to-node communication 成为瓶颈,latency 反而会拖慢扩容效果。

latency-vs-throughput

有约束的设计问题

一个下单 API,高峰 2000 req/s,要求 p99 低于 300 ms。每次下单要查一次库存服务、写一次订单库、再往 Kafka 发一条订单事件;订单事件的下游是一个每晚跑的报表任务,只关心一晚上能处理完多少条。

同一个系统里有两种目标:下单接口按 latency 考核,用户在等;报表任务按 throughput 考核,没人盯着单条记录。后面每个手段(batch、加并发、排队、异步)都要回答:它帮的是哪一边、伤的是哪一边。

Latency 要看分布,不看平均

1000 个请求里 990 个 20 ms、10 个 2 秒,平均值是 (990 × 20 + 10 × 2000) / 1000 ≈ 40 ms,看起来很健康;但每 100 个用户就有 1 个等了 2 秒。所以 latency 用百分位描述:

  • p50:一半请求比它快,代表「典型体验」。
  • p99 / p999:100 个或 1000 个请求里最慢那一个的水平,代表尾延迟(tail latency)。SLO 通常写在这里,例如「p99 < 300 ms」。

三条实际约束:

  1. 百分位不能相加、不能取平均。 10 台机器各自的 p99 求平均,不是整个服务的 p99。要合并就合并直方图(histogram),再从合并后的分布算百分位。Google SRE book 的建议就是按延迟分桶计数(0–10 ms、10–30 ms、30–100 ms……),而不是记录平均值。
  2. 尾延迟会被 fan-out 放大。 一个请求并行调用 N 个后端、要等全部返回,那么只要有一个后端慢,整个请求就慢。单个后端 p99 = 1 秒时,整体变慢的概率是 1 − 0.99^N。Dean 和 Barroso 在 The Tail at Scale 里算过:N = 100 时,63% 的用户请求会超过 1 秒。SRE book 的说法是:一个后端的 99 分位,很容易变成前端的中位数。
  3. 压测工具本身会低估尾延迟。 发压端如果「等上一个响应回来再发下一个」,服务卡住的那段时间里它根本没发请求,这些本应很慢的请求就没被记录,这叫 coordinated omission。wrk2 的 README 专门讲了这个问题,它按固定速率发请求来避免。

Little's law:并发 = 吞吐 × 延迟

稳态下,系统里同时在处理的请求数 L、到达速率 λ、每个请求在系统里的停留时间 W 满足:

L = λ × W

这是 John Little 1961 年证明的排队论结论,不依赖到达分布和服务分布。放到工程里有两种用法:

  • 算需要多少并发槽位。 下单 API 2000 req/s,每个请求平均停留 150 ms,同时在途约 2000 × 0.15 = 300 个。线程池、连接池、Node 的并发请求数至少要容得下 300,还要给峰值留余量。
  • 算吞吐上限。 把式子改写成 λ = L / W:订单库连接池 50 个连接、每次写 10 ms,这条路径的上限是 50 / 0.01 = 5000 次/秒;写变成 50 ms(比如锁等待),上限直接掉到 1000 次/秒,低于 2000 req/s 的需求,请求开始排队。

所以 latency 和 throughput 不是两个独立指标:在并发上限固定时,每个请求变慢,吞吐上限就按比例下降。熔断章节里「支付网关变慢把线程池占满」就是同一个式子。

利用率越高,排队越长

服务器不是「没满就不排队」。用最简单的 M/M/1 模型(泊松到达、指数分布服务时间、单服务台)粗估,平均响应时间约为 S / (1 − ρ),S 是服务时间,ρ 是利用率。S = 10 ms 时:

利用率 ρ平均响应时间
50%20 ms
80%50 ms
90%100 ms
95%200 ms

真实系统不满足这些假设,数字只看趋势:利用率从 50% 推到 95%,吞吐只多了不到一倍,平均延迟涨了 10 倍,尾部涨得更多。这就是为什么容量规划不会把机器压到 95%,而是给 latency 敏感的服务留足余量。

常见手段各帮哪一边

手段对 throughput对 latency适合
Batch(攒一批再处理)升:每批只付一次网络往返或 fsync升:每条记录要等批次攒满或超时日志、事件、报表导入
加并发 / 加实例升,直到共享资源(DB、锁)饱和未饱和时降;饱和后排队,p99 恶化无状态服务横向扩展
异步化(先入队再处理)升:接口只做入队接口延迟降,端到端延迟升通知、邮件、转码
缓存升:少打后端命中降;未命中不变甚至更慢读多写少
Hedged request(慢了就再发一份)略降:多发少量请求p99/p999 显著降只读、幂等的 fan-out 查询

Batch 在 Kafka producer 上有现成的旋钮:batch.size(默认 16384 字节)控制一批最多多大,linger.ms 控制最多等多久再发。Kafka 4.0 起 linger.ms 默认 5 ms,之前是 0;官方文档说明,把它设成 50 会减少请求数,但每条记录最多多等 50 ms。报表链路可以调大,下单链路的同步调用就不该这样攒。

Hedged request 的数据来自 The Tail at Scale:在 Google 的一个 BigTable 基准里,读 100 台服务器上的 1000 个 key,等 10 ms 没回来就发一份备份请求,99.9 分位从 1800 ms 降到 74 ms,只多发了 2% 的请求。前提是请求幂等,写请求不能这样重发。

常见翻车

翻车看到什么修法
只盯平均延迟仪表盘平稳,客诉说「偶尔卡好几秒」看 p99/p999,用直方图合并多实例数据
为吞吐把 batch 调大到下单同步路径p50 突然多出一个 linger.ms同步路径不攒批;只在异步链路 batch
压测用闭环发压压测 p99 很好看,上线后尾部差很多用固定速率发压(wrk2 一类工具)并记录直方图
机器压到 90% 以上还加流量吞吐几乎不涨,延迟陡增按目标利用率扩容;设排队上限,超出就拒绝
Fan-out 调用无超时一个慢分片拖慢所有请求每个下游设超时;只读查询做 hedging 或返回部分结果
超时设得比 Little's law 算出的容量还长下游一慢,在途请求把池子占满超时按 SLO 倒推;配合熔断和隔离舱

算一遍:下单 API 要多少容量

假设(面试时要和面试官确认):高峰 2000 req/s,p99 目标 300 ms;库存服务调用 p99 60 ms,订单库写 p99 40 ms,Kafka 发送 p99 20 ms,串行执行。

  1. 延迟预算。 串行路径的 p99 不能简单相加(各段的尾部不一定同时出现),但相加 60 + 40 + 20 = 120 ms 是一个偏保守的上界,离 300 ms 还有余量,留给网络和排队。
  2. 在途请求。 平均停留按 80 ms 算,2000 × 0.08 = 160 个在途。每实例并发上限 50,至少 4 个实例;按「利用率不超过 60%」留余量,部署 6–7 个。
  3. 订单库连接池。 每次写平均 15 ms,2000 次/秒需要 2000 × 0.015 = 30 个连接同时忙。7 个实例每个 10 个连接,一共 70,够用;如果每个 50,就是 350 个连接,超过 PostgreSQL 默认的 max_connections = 100,要加 PgBouncer 一类连接池。
  4. Kafka 发送。 下单路径同步等 ack 时保持 linger.ms 很小;如果改成「写库后异步发事件」(outbox),接口延迟里就不再含这 20 ms,代价是事件晚一点到。
  5. 报表任务。 它只看 throughput:一晚 8 小时处理当天约 2000 × 86400 × 0.3 ≈ 5200 万 条(假设全天平均流量是高峰的 30%),需要约 1800 条/秒,用大 batch 批量读写即可,单条延迟不重要。

面试时这样回答

  1. 先分目标。 「下单接口按 p99 考核,报表按吞吐考核」,不要对整个系统只说一句「要高性能」。
  2. 用百分位说延迟。 给出 p99 目标,并说明 fan-out 会放大尾延迟,必要时用超时、hedging 或部分结果来控尾部。
  3. 用 Little's law 算容量。 报出「每秒 X 个请求、每个 Y 毫秒、在途 X × Y 个」,推出线程池、连接池和实例数。
  4. 说一个取舍。 例如「报表链路调大 batch 换吞吐,下单链路不攒批保延迟」,或「异步化让接口变快,但端到端延迟变长」。
  5. 说一个故障和修法。 下游变慢时在途请求暴涨占满池子,用超时、熔断和排队上限兜住。

相关章节:Performance 与 Scalability、Circuit breaker、Message Queues、Caching、SLA、SLO 与 SLI、延迟数字速查。

一手证据