2580 字
约 8 分钟
14
S27-A16 · 本机与服务器的差异:为什么本地跑得快

一份实测记录说"优化后查询从 900ms 降到 120ms"。复现时在服务器上跑了 780ms——因为作者的"优化"里有一半收益来自"本机是 NVMe + 数据只有 20 万行",而生产是网络存储 + 800 万行。

本机与服务器的差异不是"配置高低",而是几个会改变结论方向的变量:磁盘类型、数据规模、网络距离、核数与并发、以及本机上"没有别人在跑"。这六项之外还有一个"元差异":本机的结论是给你一个人看的,服务器的结论会被别人引用。所以本地测速可以粗糙,而写进文档的数字必须在会被人拿去用的环境里测。
这些差异不消除,本地结论就不能外推。


1. 六项会改变结论的差异

差异 影响方向
磁盘类型(NVMe vs 网络存储) IO 型查询差一个数量级
数据规模 索引与全扫的交叉点不同
网络距离 每跳增加往返
核数与超线程 并发与 GC 线程数
其它负载 缓存与 CPU 争抢
运行时与参数 默认值可能不同

第一项最容易被低估:本地 NVMe 的随机读比网络存储快百倍,于是"加了索引快 10 倍"在本地成立、在服务器上可能只快 2 倍——因为瓶颈从 CPU 换到了 IO。

第五项常被完全忽略:本机只有你一个进程,服务器上有 20 个。第六项(运行时与参数)最隐蔽:本机可能跑的是 JDK 17 默认参数,服务器跑了 12 个 -XX。同一个程序在两处的 GC 行为可以完全不同,而"版本相同"这句话不足以说明可比性。
缓存命中率、CPU 时间片都不同。

2. 数据规模:最常被"凑合"的一项

做法 是否可用
生产数据脱敏导出 可用
按比例缩小(1/10) 谨慎(分布会变)
造数(随机) 只用于功能验证
用生产从库 可用且推荐

第一项(生产脱敏导出)要注意"脱敏是否改变了分布":把手机号替换成随机串,会让唯一索引的选择性变化,于是计划可能不同。脱敏要做"保分布"的替换(如按原值哈希)。
第二项的风险是"交叉点被移动":索引在全扫更便宜时不会被选中,于是"缩小 10 倍的数据"测出的计划与生产不同。

第四项是实践中的最优:在从库上跑只读查询,规模与分布都是真的,代价是要拿到访问权限。

// 数据规模的三个必写数字
rows: 8,120,000
avg_row_bytes: 420
index_count: 5
// 缺"行数"时,任何耗时数字都无法判断适用性
// 缺"分布"时,无法判断是否包含倾斜

3. 本机"快"的三种假象

假象 机制
缓存全在内存 重复跑同一查询
编译/预热已完成 JIT 已编译
无并发争抢 独占 CPU

第一项最普遍:同样的查询跑第二遍,数据页已在 OS 缓存里第一项的量化方式很简单:同一查询连续跑 5 次,记录每次耗时。如果第 1 次是 800ms、第 2–5 次是 120ms,那 120ms 是"缓存命中"的数,不是"生产可用"的数。
,于是"第二遍比第一遍快三倍"——而这个收益在生产上不存在(因为数据量大得多)。

第二项在 JVM 上明显:首次运行是解释执行,而稳态是编译后的代码。所以"第一次慢"是本机特有现象,除非你显式预热。

// 消除三种假象的最小做法
// 1. 清 OS 缓存(需要权限)或换数据文件路径
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
// 2. 预热:先跑 3 轮丢弃
for i in 1 2 3; do run_query > /dev/null; done
// 3. 记录"第几轮"与"是否清缓存"

还有一项"看不见"的差异:文件系统与挂载参数。本机可能是 ext4 直连,服务器可能是 overlayfs + 网络挂载,而后者的小文件随机写在某些参数下会慢几十倍——这类差异在"明明硬件更好"的机器上尤其让人困惑。

// 确认磁盘与挂载类型(差异的源头)
df -T /var/lib/mysql            # 文件系统类型
mount | grep -E ' / | /var'     # 挂载参数(noatime? relatime?)
// 对比两边的随机写能力
fio --name=r --rw=randwrite --bs=4k --size=1G --numjobs=4 --runtime=30 --time_based

4. 网络距离的量化

场景 典型 RTT
同机 < 0.05ms
同机房 0.2–0.5ms
跨可用区 1–2ms
跨地域 20–100ms

第四项的差异不是"加法"而是"乘法":一次请求若串行 10 次跨地域调用,就是 200ms–1s 的固定开销,与代码优化无关。

这里有个"够不够"的判断:当网络开销占比超过 20% 时,端到端数字已经不能反映代码质量。此时应当报告"服务端处理耗时"而不是端到端。
所以本机测的"端到端耗时"必须减去这部分,或者明确标注"不含网络"。

一个实用的折中:在本地做"参数化测量",把结论写成关于变量的函数。比如"耗时 ≈ 20ms + 行数 × 0.08ms"——这样在服务器上只要代入真实行数,就能估算,而不必每次都复测。

// 参数化结论的写法(比单个数字有用得多)
实测:2 万行 22ms,20 万行 38ms,100 万行 121ms
拟合:T ≈ 20 + rows × 0.0001 (ms)   ← 线性段
修正:超过 50 万行后斜率变大(索引失效),需分段
// 用法:服务器 800 万行 → 落在"斜率变大"段,不能直接外推

5. 怎么让本地结论可外推

做法 说明
在等规模环境复测 最可靠
只报告"相对改善"并注明环境 有限可用
用服务器跑基准(而非本机) 推荐
分别报告 CPU 时间与墙钟时间 剔除网络

第四项很实用:CPU 时间不受网络与排队影响第三项(用服务器跑基准)的实践要点是"不要在生产实例上跑":用一台同规格的空闲机器,或在从库上跑只读。在有流量的实例上测,得到的是"带负载的数字",而它无法归因。
,于是"CPU 时间从 800ms 降到 110ms"是一个更可外推的结论。

// 两边都报告的写法
local:  wall 120ms, cpu 110ms, rows 200k, nvme, no concurrent load
server: wall 780ms, cpu 140ms, rows 8.1M, network disk, 20 concurrent
// 结论:算法改善成立(cpu 110 vs 原 800),但墙钟收益主要被 IO 吃掉

最后一条实践建议:把"环境差异"写成一份清单,随实测记录一起提交。清单不需要长,六行就够——而它的价值在于让以后读这份记录的人立刻知道"这个数字能不能用"。

// 环境差异清单(附在实测记录末尾)
disk:    local=nvme  server=network-ssd
rows:    local=200k  server=8.1M
rtt:     local=0     server=0.4ms
cores:   local=8     server=16 (ht on)
load:    local=none  server=20 concurrent
runtime: local=jdk17-default  server=jdk17-12-XX-flags

6. 排错:五种"本地快服务器慢"

现象 成因 处理方向
本地 120ms 服务器 780ms IO 类型不同 在服务器测
第二遍快三倍 OS 缓存 清缓存或标注轮次
并发一上来就崩 无争抢假设 加并发压测
服务器上计划不同 数据规模/统计 等规模复测
换机器结果又变 参数/版本不同 打印实际配置

第三项(并发一上来就崩)的根源是"本机没有并发":连接池、线程池、GC 线程数这些参数在单请求下都不起作用,而它们恰恰是高并发下的主要瓶颈。
第四项最典型:本机 20 万行时优化器选了索引,服务器 800 万行时选了全扫。这不是"优化失效",而是"优化只在特定规模下成立"。

7. 边界:什么情况本地结论足够

场景 为什么 做法
纯算法复杂度对比 与 IO/网络无关 本地够
数量级差异(10 倍以上) 噪声不影响方向 本地够
功能与正确性验证 不涉及性能 本地够
已明确瓶颈在 CPU 本机 CPU 可代表 本地够,但注核数

判断口径一句话:这次结论会不会被用来决定"上不上生产"。最后一条与"记录"有关的边界:记录里必须写"在哪测的",而不只是"什么机器"。因为"本机"与"同规格空闲机"在很多团队里被混称,而它们的结论强度完全不同。
会,必须在等规模环境复测;只用来判断"方向对不对",本地够。


8. 自测

  1. 六项差异里,哪一项最容易造成"一个数量级"的误判?为什么它会把瓶颈从 CPU 换到 IO?
  2. 为什么"按比例缩小数据"会移动交叉点?最优替代是什么?
  3. 本机"快"的三种假象分别是什么?怎么消除它们?
  4. 网络距离为什么是"乘法"而不是"加法"?
  5. "分别报告 CPU 时间与墙钟时间"为什么让结论更可外推?
  6. 判断"本地结论是否足够"的那个问题是什么?
S27-A16 · 本机与服务器的差异:为什么本地跑得快
https://www.clxhxhhr.top/posts/6499/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。