第 18 / 23 篇

第17章 基准、容量与性能结论的边界

微基准回答局部成本

随书基准测量工作池接纳、调度与完成一条轻量任务的成本。它不是完整 HTTP 压测,不包含真实 TLS、代理、数据库或业务序列化。测试确保每个 benchmark operation 真正被接纳,队列饱和时的调度重试也计入成本,避免只测到“满了立即拒绝”的便宜路径。

在 Go 1.24.3、darwin/arm64、Apple M4 本机,三次记录分别约为九百五十点九、一千二百一十八和一千一百七十六 ns/op,均为三百零四 B/op、八次分配。这个波动本身就是证据:即便轻量基准也会受到调度、频率和后台活动影响。不能挑最小值宣称生产每秒可以处理多少任务。

这些数字更适合帮助定位下一步问题。例如每任务分配是否主要来自超时对象,某个改动是否引入额外复制,等待完成是否主导基准。只有在负载与环境可比、重复结果足够一致时,才讨论改进。若新实现更快却破坏取消与关闭契约,性能结果没有交付意义。

服务容量需要包含失败

完整服务实验至少要记录到达、接纳、完成、拒绝、超时和取消。只统计成功请求会让过载时的延迟看起来很好,因为最慢或最难的请求已被排除。对于有界服务,拒绝比例是容量曲线的一部分;对于无界服务,排队延迟和内存增长则可能是隐藏代价。

负载模型也要说明。闭环客户端完成一个再发下一个,服务变慢时到达率自动下降,可能掩盖排队;开放到达模型按计划持续发送,更容易暴露过载,但必须确保压测端自身没有先成为瓶颈。书中没有把本机微基准冒充开放流量容量实验。

平均延迟不能代表尾部。少量慢外呼就可能长时间占住 worker,使后续任务排队。应结合分位数、最长等待与拒绝观察,并按任务类型拆分。样本很少时,高分位数不稳定,应给出样本数量和运行时长,而不是用精确小数制造确定感。

资源预算应该共同验收

容量不仅是每秒完成多少,还包括峰值内存、连接数、队列上限和退出时间。一个服务吞吐提高一成,却让慢客户端无限保留连接,不能算完成性能工作。优化报告要把原先故障测试与新的负载结果一起呈现,证明资源边界没有倒退。

并发提高可能先改善吞吐,随后放大上下文切换、内存和下游排队。不存在适用于所有服务的 worker 数公式。CPU 密集、远端等待和大对象处理应分别测量。集群扩容还会放大对共享下游的总压力,单副本安全参数需要与副本数共同管理。

队列长度也不宜用作吞吐旋钮。更大队列可能降低短突发拒绝,却增加尾延迟和过期任务。应依据允许等待时间、到达波动与任务占用计算范围,再用故障实验验证。若业务不允许超过某个截止,接受大量注定过期任务只会浪费资源。

优化实验要有退出条件

开始优化前写下目标与守护约束。例如减少任务分配,同时保持满即拒绝、调用者取消语义和排空退出。若测量无法区分改进与噪声,就停止扩大结论,转向更具代表性的负载。不要因为已经花了很多时间就强行宣布成功。

profile 可以解释变化来源,基准可以量化局部成本,行为测试可以保护语义,三者的作用不同。一个 top 列表变短不一定意味着服务更好,一个基准数字下降也可能只是少做了必须完成的工作。应核对每个操作确实接纳、确实终结,且结果没有被静默丢弃。

环境记录应包含 Go 精确版本、架构、操作系统、CPU、命令和运行次数。不要用“最新版 Go”替代版本,因为编译器、运行时和标准库会变化。Linux CI 重新运行验证可移植行为,但其共享 runner 性能数字不宜与本机直接拼成优化对比。

练习:设计一个诚实的性能报告

先重复微基准至少三次,保留全部结果,再写出它能支持的一项判断和不能支持的两项判断。随后构造轻量、慢外呼和慢客户端三种服务负载,分别记录完成、拒绝、超时与资源峰值。不要为了一个统一吞吐数字把不同资源类型混在一起。

最后做一次过载后恢复实验,确认停止施压后队列排空、连接可用、内存回到合理稳定范围,并能在给定预算内退出。性能报告的最后一页应该说明系统如何退化与恢复,而不只是正常负载下最快的一秒。

本章实测证据

以下是本次三次完整样本,保留波动而不只挑最好值。具体方法见附录B及实验包记录。

BenchmarkPoolSubmit-10  1279885   950.9 ns/op  304 B/op  8 allocs/op
BenchmarkPoolSubmit-10  1000000    1218 ns/op  304 B/op  8 allocs/op
BenchmarkPoolSubmit-10   910029    1176 ns/op  304 B/op  8 allocs/op