写在前面
做后端久了,你会发现所有性能问题本质都是同一个问题:你对各种操作到底有多快、有多慢,没有量级直觉。
为什么缓存要分三级?为什么等数据库时不能阻塞线程?为什么一次微服务调用"看起来只是个函数调用"却贵那么多?为什么压缩有时候反而更快?——这些决策的答案,都藏在一张表里:每个常见操作大概要花多少时间。
这篇就把这张表摆出来,逐个量级讲清楚,再落到工程决策上。不是让你背绝对值,而是建立"什么东西比什么东西快/慢多少个数量级"的直觉。有了它,你不用压测就能大概估出一个设计靠不靠谱——这就是所谓封底估算(back-of-the-envelope estimation)的底气。
这张表最早来自 Google 的 Jeff Dean 在 2010 年左右的那张著名幻灯片,后来被很多人更新过。本文用 Colin Scott 2020 年的更新版为基准,再补上真实系统里的常见量级。
一、那张经典表:每个操作要多久
| |
几个一眼能看出来的事实:
- 从 L1 缓存到主存,差了 200 倍(0.5 ns → 100 ns);
- 从主存到 SSD 随机读,又差了 1500 倍(100 ns → 150 μs);
- 从 SSD 到机械硬盘寻道,再差近 100 倍(150 μs → 10 ms);
- 一切"快"的操作都在纳秒级,一切"慢"的操作都在毫秒级——微秒是分水岭。
记住这个"分水岭",比记住任何一个具体数字都重要。
关于这张表的出处和精度:它是 Jeff Dean 2010 年那张著名幻灯片的经典数值集(业界转载数十年的版本),给你的是数量级而非精确值。比如 SSD 随机读 150 μs 是 SATA 时代的数,NVMe 已压到 ~16–100 μs;现代 CPU 的 L1 也更接近 1 ns。比例关系(主存比 L1 慢 200 倍、磁盘比内存慢 10 万倍)才是长期稳定的部分。完整来源与逐年演进见第十节。
二、把 1 纳秒放大成 1 秒
人对纳秒、毫秒没感觉,对秒、分钟、年有感觉。把整张表按 “如果 1 纳秒 = 1 秒” 放大,就能直观体会 CPU 视角下的世界有多慢:
| |
CPU 的"一秒"是 L1 缓存级别的瞬间。它要读个主存,相当于你等一杯咖啡;要等一次磁盘寻道,相当于等一个学期;要等一个跨洲网络包,相当于等大学读完。
这就是为什么"CPU 很快、磁盘很慢"不是一句口号,而是差着七八个数量级的鸿沟。 一切性能优化技巧——缓存、批量、异步、数据本地性——本质上都是"别让 CPU 去等那个学期的磁盘"。
三、CPU 与内存:跨不过的"内存墙"
CPU 这十几年频率涨不动了(GHz 顶天),靠的是多核和缓存。真正卡性能的是 CPU 和主存之间那条"内存墙"。
| |
这条层次结构直接决定了三条写代码的铁律:
数据本地性(locality) 顺序访问数组,缓存命中,几乎免费;跳跃访问链表节点,每次都可能 miss 主存,慢两个数量级。这就是"同样逻辑,
List<T>比LinkedList<T>快得多"的根因——不是算法复杂度,是缓存。分支预测 if 分支如果不可预测(比如对无序数据做条件筛选),CPU 流水线要付出 ~5 ns 的预测错误代价。把数据先排序再筛选,反而更快——因为分支变得可预测了。这是教科书级别的反直觉。
结构体布局 把经常一起用的字段放一起、紧凑排列(
struct、缓存行对齐、避免伪共享),就是为了让一次缓存行加载尽量多带"有用数据"。.NET 里 Span、Memory、struct、stackalloc的存在,全是服务于这条。
这些点在《.NET 高并发编程全景》《.NET 性能优化与 Profiling》里都有 .NET 视角的落地,这里只讲它背后的硬件量级。
四、磁盘:机械、固态、NVMe 三个量级
存储这条线上,硬件代际差着整整三个数量级,而且"随机"和"顺序"是两套世界。
| |
几个关键直觉:
- 机械硬盘的随机访问是灾难性的。一次寻道 ~10 ms,因为它真的有个机械臂在物理移动。HDD 上做随机小 IO,每秒撑死几百次;
- SSD 把"寻址"从机械运动变成了电子操作,随机访问掉了一个半数量级。所以数据库从 HDD 换 SSD,是"换了个数据库"级别的提升;
- 但随机 vs 顺序的差距,SSD 仍然存在(哪怕 NVMe)。所以"把随机写变成顺序写"这条铁律,在 SSD 时代依然成立。
这条铁律催生了两个最经典的工程实践:
- WAL(预写日志):数据库不直接随机改磁盘上的数据页,而是先把改动顺序追加到日志。fsync 一条顺序日志,比随机改 N 个数据页便宜得多。详见《数据库系列(四):事务与 ACID》——里面给过 fsync 的实测对照:HDD ~10 ms / SSD ~1 ms / NVMe ~0.1 ms;
- LSM 树 / Kafka:把随机写转成顺序写,牺牲一点读放大,换写入吞吐。Kafka 能扛百万级 TPS,根本原因就是它本质上是"顺序追加日志 + 零拷贝",没有任何随机写。
五、网络:从机柜到跨洲
网络的延迟,主要由距离(光速)和跳数(路由器/交换机)决定。同一个机房和跨洲,差着两三个数量级:
| |
RTT(Round-Trip Time)= 一个数据包打个来回的时间。地球表面两点的 RTT,物理上被光速(光纤里约 2×10⁸ m/s)封顶,再快的网络也突破不了——北京到上海 ~1200 km,光纤走线更长,单程再快也得 ~5 ms,来回就是十几到二十几 ms,这就是"跨城必然 20ms 起"的物理原因。
一个反直觉的点:小请求的延迟是 RTT 主导的,不是带宽主导的。
| |
这就解释了为什么 批量(batching)永远有效:把 1000 次"各跑一个来回"合成 1 次"跑一个来回",延迟和往返次数一起降。
六、一次 HTTP 请求到底慢在哪
把上面几节拼起来,拆一次普通的 HTTPS 调用:
| |
两个直接结论:
- 连接复用是第一优化。冷启动要付 2 个 RTT 的握手税;复用连接(HTTP keep-alive / 连接池)后,每次只剩 1 个 RTT。
HttpClient、Redis 连接池、数据库连接池,本质上都是在"省掉那 2 个 RTT"。这也是为什么new HttpClient()每次新建连接是个著名反模式——《HttpClient 的前世今生》那篇讲的就是这个坑; - 跨城调用时,握手成本会把业务处理时间淹掉。所以"把服务和它的强依赖部署在同一个机房"几乎永远是值的。
七、把这些数字用起来:8 条工程决策
有了量级直觉,下面这些"经验法则"就不再是口口相传的玄学,每条都能对回那张表:
| |
八、现实世界一次操作要多久
经典表给的是硬件/网络的原子操作量级。这一节把它们组装成你日常后端真正会发起的那些操作——读一次 Redis、查一次库、两个 Pod 之间通一次信、穿过一层 sidecar。同样声明在前:全是量级参考,不是精确值,随硬件、CNI、负载、运行时而漂移;尤其 K8s 网络和 sidecar,不同 CNI(Calico/Cilium)和配置能差出几倍。
进程内 / 运行时
| 操作 | 典型量级 |
|---|---|
| 本地函数调用 | ~1 ns |
| 本地缓存命中(IMemoryCache / 字典) | ~ns–μs |
| 一次系统调用(syscall) | ~0.1–1 μs |
| 一次线程上下文切换 | ~1–10 μs |
| 一次 JSON 序列化(中等对象) | ~10–100 μs |
| Minor GC 暂停 | ~1–10 ms |
| Full GC 暂停 | 数百 ms–秒级 |
数据层
| 操作 | 典型量级 |
|---|---|
| Redis GET(同机房,连接复用) | ~0.1–0.3 ms |
| Redis pipeline / MGET(批量,单条摊薄) | ~几 μs/条 |
| DB 主键查询(buffer pool 命中) | ~0.5–2 ms |
| DB 索引范围扫描(返回 100 行) | ~2–10 ms |
| DB 查询触发磁盘 IO | + ~1 ms(SSD)/ ~10 ms(HDD) |
| Elasticsearch 查询(命中分片) | ~5–50 ms |
| Kafka 单条 produce(acks=1,同步) | ~1–5 ms |
| Kafka 单条 consume(顺序读页缓存) | ~μs–ms |
| DNS 递归查询 | 10–100 ms(本地命中 <1 ms) |
RPC / 网络层(K8s 视角)
这一层经典表里没有,但微服务时代每天都打。RTT 主导,每跨一跳加一次 RTT:
| 操作 | 典型量级 |
|---|---|
| 同 Pod 内(loopback,如 app ↔ sidecar) | ~10–20 μs |
| 同 Node 跨 Pod(veth pair) | ~20–50 μs |
| 跨 Node Pod 通信(overlay/VXLAN,同机房) | ~0.1–0.5 ms |
| 经 ClusterIP(kube-proxy / iptables DNAT) | 几乎零额外(μs 级,高并发 conntrack 有损耗) |
| 经 Service Mesh sidecar(Envoy,每 hop) | 2–5 ms(每 hop 额外;Istio 官方 ~3 ms p50 / ~10 ms p99) |
| 经 Ingress / API 网关(TLS + 路由 + 鉴权) | +1–5 ms |
| gRPC 单次调用(同机房,HTTP/2 连接复用) | ~0.5–2 ms |
| REST/HTTP 单次调用(同机房,连接复用) | ~1–3 ms |
| 跨可用区调用(同城) | +2–5 ms |
| 跨 region 调用 | +几十 ms |
Service Mesh 的"税"有多重:sidecar 模式下,一个请求要走 app → 本地 Envoy → 网络 → 对端 Envoy → 对端 app,多出两个本地 hop、两次 mTLS 加密。Istio 官方基准给的是 ~3 ms p50 / ~10 ms p99,业界普遍引用 2–5 ms 每 hop;高负载(数千 RPS)时一篇 arXiv 对比测出 Istio 延迟增加可达基线的 166%。这就是为什么 Ambient mesh(sidecar-less,ztunnel)和 eBPF 数据面(Cilium)在抢这块——把 sidecar 从数据路径上挪走。
拆解的乐趣:现实操作 = 原子项之和
把上面这些和经典表对照着看,你会发现真实系统的延迟,基本都能拆回"那张表"里的几个原子项:
- 一次 DB 查询 ≈ 若干次内存访问 + 可能的一次磁盘 IO;
- 一次同机房 gRPC ≈ 1 个数据中心 RTT + 两端序列化 + 两端处理;
- 一次穿 Service Mesh 的调用 ≈ 上一条 + 两个 sidecar 的 2–5 ms 税;
- 一次跨 region 调用 ≈ 基础调用 + 光速封顶的几十 ms RTT。
建立这种"把现实操作拆回原子项“的习惯,是性能估算的核心能力——也是这张表真正的用法。
九、数量级直觉练习
拿几个常见问题练手(答案都在前面那张表里):
| |
这种估算不需要跑压测,对着表心算就行。一个能随时做这种估算的工程师,做设计时几乎不会犯"明显不合理"的性能错误。
十、数字从哪来,以及别死记
最后,关于这些数字本身:
- 出处:本文那张经典表用的是 Jeff Dean 2010 年那张著名幻灯片(“Latency Numbers Every Programmer Should Know”)的经典数值集——业界被转载了十几年的版本。UC Berkeley 的 Colin Scott 把它扩展成按年份滑动的交互图,能看到 1990→现在每个数怎么随硬件漂移;想看最易读的可视化,推荐 samwho 的 2020 版;
- 它们是量级参考,不是精确值。Dean 经典值和 Scott 的现代值在几处差一档(同数量级内):L1 缓存 0.5 ns → 现代 ~1 ns、分支预测 5 ns → ~3 ns、L2 缓存 7 ns → ~4 ns、SSD 随机读 150 μs → NVMe 时代 ~16–100 μs;而主存 100 ns、数据中心 RTT 0.5 ms、磁盘寻道 10 ms 这些长期稳定。硬件还在继续漂移(HBM/CXL 压缩 CPU↔内存鸿沟、QUIC/TLS 1.3 砍握手 RTT)。抓"数量级和相对比例”,而不是死记绝对值——5 年后绝对值会过时,“主存比 L1 慢 200 倍"这种比例关系却长期稳定;
- 要测自己的系统。生产里的真实数字,永远来自你自己的监控:看 p50 / p95 / p99 / p99.9,不要看平均值(长尾会被平均掉)。平均值是性能世界里最大的谎言之一。
参考资料
- Latency Numbers Every Programmer Should Know — Colin Scott(交互图,按年份滑动)
- colin-scott/interactive_latencies — GitHub 源数据仓库
- Latency Numbers Every Programmer Should Know — samwho(2020 可视化)
- More Numbers Every Awesome Programmer Must Know — High Scalability
- Latency numbers every programmer should know — Hacker News 讨论(含质疑:部分数字是推算而非实测)
- Jeff Dean, Software Engineering Advice from Building Large-Scale Distributed Systems(Stanford, 2009)—— 那张表的原始出处
- Performance and Scalability — Istio 官方(sidecar 延迟基准:~3 ms p50 / ~10 ms p99)
- Performance Comparison of Service Mesh Frameworks — arXiv(mTLS 下各 mesh 延迟对比)
想深入,推荐读《Designing Data-Intensive Applications》(Martin Kleppmann)第一章——它对"封底估算"这件事有极好的示范。
小结
- 一张表:从 L1 缓存(0.5 ns)到跨洲网络(150 ms),跨 8 个数量级;微秒是快慢的分水岭;
- 人化直觉:把 1 ns 当 1 秒,CPU 读主存 = 等一杯咖啡,等磁盘 = 等一个学期,等跨洲网络 = 等到毕业;
- CPU↔内存:差 200 倍,缓存层次和数据本地性是一切优化的根基;
- 磁盘:HDD/SSD/NVMe 差三个量级,“随机 vs 顺序"是永恒命题,WAL/LSM/Kafka 都是把随机转顺序;
- 网络:RTT 受光速封顶,小请求延迟是 RTT 主导而非带宽,所以批量永远有效;
- HTTP 请求 = 几个 RTT + 服务端处理,连接复用省掉的就是那 2 个握手 RTT;
- 现实操作量级:Redis/DB/gRPC 同机房都是 ms 级;K8s 同 Node 跨 Pod 是 μs 级、跨 Node ~0.1–0.5 ms;Service Mesh sidecar 每 hop 再 +2–5 ms;每跨一层网络加一次 RTT;
- 8 条决策:缓存分层、异步并发、批量、数据本地性、顺序写、有依据的超时、正视微服务调用成本、压缩的权衡;
- 方法论:抓数量级而非绝对值,看 p99 而非平均,用封底估算做设计判断。
记不住全部没关系——记住"内存比缓存慢 200 倍、磁盘比内存慢 10 万倍、一次跨网至少毫秒级"这三句,日常 90% 的性能判断就够用了。其余的,回来查这张表。