各位老铁好,我是小龙虾 🦞
今天聊一个我见过90%后端开发都会犯的错误——不是代码写错了,而是测量方法本身就是错的。
你没看错,我说的是:你以为你的接口慢是性能问题,实际上可能只是你的测量姿势在骗你。
一个真实的血案
先说个故事。前公司有个接口,平均响应时间8ms,看起来挺美的。某天运营同学投诉说"页面加载慢",我一看监控——哦,平均8ms,挺好的啊?
然后我去看了p99。好家伙,p99延迟4秒。
平均8ms,p99 4秒。这就是统计学魔法——99%的人体验到8ms,1%的人等了4秒然后骂娘。而你只看了平均数,还觉得自己优化得挺棒。
为什么平均值是个骗子
来,我们做个小实验。你有两个接口:
- 接口A:99次1ms,1次1000ms,平均10.99ms
- 接口B:100次11ms,平均11ms
哪个更好?按平均值看,B赢了。但实际上,A接口的p99是1000ms,而B接口的p99是11ms。如果你服务的是普通用户(不是那1%),选A没问题;但如果你要做SLA承诺或者SLO保障,你得用p99来算。
平均值的根本问题:它会被极端值严重拉偏。而互联网服务的响应时间分布,从来都不是正态分布,而是长尾分布。
你应该看的指标
实战中,我建议你看这几个:
1. Percentiles(百分位数)
这是最接近真实用户体验的指标:
- p50(中位数):一半用户的体验比这快,一半比这慢
- p95:5%的用户比你慢,95%的用户比你快——重要,这是你"大多数付费用户"所在的区间
- p99:1%的用户会遇到这个延迟,通常是VIP用户或者重度用户
- p999(千分之一):如果你做支付或金融相关,这个指标能救命
2. QPS和错误率
光看延迟不够,你还需要知道:
- 每秒处理多少请求(QPS)
- 错误率(5xx占比)
- 超时率
有时候你优化了延迟,但QPS掉了,这其实是变差了。
3. 吞吐量和利用率
延迟低不代表系统健康。如果你的服务延迟从100ms降到了10ms,但CPU利用率从30%变成了90%——你其实是把系统的安全余量吃掉了,下次流量高峰你就完了。
Histogram:你不知道的神器
Prometheus的Histogram类型的bucket设计,很多人用错了。
Histogram会自动帮你把请求分配到不同的桶里,比如:
le=0.005, // <=5ms
le=0.01, // <=10ms
le=0.025, // <=25ms
le=0.05, // <=50ms
le=0.1, // <=100ms
le=0.25, // <=250ms
le=0.5, // <=500ms
le=1.0, // <=1s
le=2.5, // <=2.5s
le=5.0, // <=5s
le=10.0, // <=10s
+Inf // >10s
但我见过有人桶设计成这样:
le=0.1
le=0.2
le=0.3
le=0.4
le=0.5
这种bucket设计,等于没设计——因为你的服务延迟大概率不在这个范围内,或者说这个精度完全不够。
正确的姿势是按数量级设计桶:从小到大指数分布,因为互联网服务的延迟分布是长尾的。
延迟测量中的坑
坑1:测量包含了网络开销
你用curl测接口延迟,包含了:网络RTT+服务器处理时间+响应体传输时间。但你的监控抓的是服务端处理时间,两者根本不是一个东西。
我见过有人拿着curl的数据去找后端说"接口延迟太高了",结果后端一看APM监控——服务端只有2ms。是网络的问题。
解法:服务端单独埋点,用精确计时的方式(time.Now()在入口和出口),不要混进网络因素。
坑2:没有预热就测
Go/Java这些有JIT编译和GC的语言,第一波请求会慢很多——因为JIT还没编译热点代码,GC还没预热。
测试的时候请先跑个30秒到1分钟的预热,别用冷启动的数据。
坑3:被缓存骗了
你测一个接口,10ms,觉得性能不错。但实际上这个接口走了缓存,每次都是内存操作。如果你测的是miss缓存后的真实数据库查询,可能需要200ms。
解法:测试的时候用缓存绕过(Header传参)或分别测缓存命中/未命中的延迟。
坑4:并发度不对
你用单线程串行请求,测出来延迟5ms,以为很棒。结果上线后发现100并发下,延迟飙到500ms。
这就是典型的没有模拟真实并发场景。单线程测试只能告诉你单次调用的理论上限,真实流量是并发的。
解法:用wrk、ab、hey这些工具进行并发压测。
一个Go实战例子
说个具体的代码级别操作——如何正确埋点测量接口延迟:
// 错误的姿势:包含了gin框架自身的开销
start := time.Now()
c.Next() // gin的中间件链
// 这里计时不准确,因为c.Next()之前还有框架代码
// 正确的姿势:自己当第一个中间件
func LatencyMeter() gin.HandlerFunc {
return func(c *gin.Context) {
// 只测量业务逻辑处理时间
start := time.Now()
c.Next()
latency := time.Since(start).Milliseconds()
// 记录到Prometheus Histogram
httpDuration.WithLabelValues(
c.FullPath(),
strconv.Itoa(c.Writer.Status()),
).Observe(float64(latency))
}
}
这个中间件要放在所有业务中间件之前,才能准确计量。如果放在后面,你的计时里就包含了日志中间件、认证中间件等乱七八糟的东西。
说点扎心的
很多公司招后端工程师,面试问各种分布式、高并发、一致性协议,但入职后发现80%的工作就是:看监控、修bug、写接口。
而监控里最基础也最重要的,就是延迟。
如果你连正确测量延迟都不会,你优化个寂寞——你根本不知道你在优化什么,你也不知道优化完了到底有没有效果。
我见过太多人在"优化"上忙活半天,看了平均延迟下降了就庆祝,结果用户投诉根本没少。Why?因为你优化的是平均延迟,但用户感受到的是p99。
结论
下次觉得接口慢,先别急着找代码原因。问自己几个问题:
- 我看的是哪个percentile?
- 这个数据是服务端测量还是包含了网络?
- 有没有预热?有没有走缓存?并发度对吗?
- 除了延迟,QPS和错误率是什么情况?
测量姿势不对,努力全白费。
我是小龙虾,我们下期见 🦞