本文是《程序员数学扫盲课》系列文章
← 上一篇:程序员数学05:概率论 - 系统可用性 | → 下一篇:程序员数学07:线性代数 - 推荐系统
TL;DR
为什么监控报警不看平均值要看P99?为什么1%的慢请求能毁掉用户体验?为什么要关注长尾延迟?答案都藏在统计学里。这篇文章用Go代码带你搞懂百分位数,看完你会发现:平均值会骗人,P99才是真相。
系列导航
《程序员数学扫盲课》系列:
1. 破冰篇:数学符号就是代码
2. 对数Log:数据库索引的魔法
3. 集合论:玩转Redis与SQL
4. 图论基础:微服务依赖管理
5. 概率论:系统可用性计算
6. 统计学:P99延迟与监控报警(本篇)
7. 线性代数入门:推荐系统的数学基础
8. 哈希与模运算:负载均衡算法
9. 信息论:数据压缩与编码
10. 组合数学:容量规划与性能预估
一、为什么平均值会骗人?
先说重点:平均值掩盖了极端情况,看不到真实的用户体验。
1.1 一个真实的故事
假设你的API有100个请求:
- 99个请求:10ms
- 1个请求:1000ms(慢查询)
平均响应时间:
平均值 = (99×10 + 1×1000) / 100 = 19.9ms
老板看到监控: "平均20ms,很快啊!"
用户感受: "有1%的请求要等1秒,体验很差!"
Go代码模拟:
package main
import (
"fmt"
"sort"
)
func main() {
// 模拟100个请求的响应时间
latencies := make([]int, 100)
// 99个快速请求
for i := 0; i < 99; i++ {
latencies[i] = 10
}
// 1个慢请求
latencies[99] = 1000
// 计算平均值
sum := 0
for _, lat := range latencies {
sum += lat
}
avg := float64(sum) / float64(len(latencies))
fmt.Printf("平均响应时间: %.1f ms\n", avg)
fmt.Printf("最慢请求: %d ms\n", latencies[99])
fmt.Printf("\n问题:平均值看起来很好,但1%%的用户体验很差!\n")
}
输出:
平均响应时间: 19.9 ms
最慢请求: 1000 ms
问题:平均值看起来很好,但1%的用户体验很差!
二、百分位数(Percentile):真实的性能指标
2.1 什么是百分位数?
P50(中位数):50%的请求比这个值快
P90:90%的请求比这个值快
P99:99%的请求比这个值快
P999:99.9%的请求比这个值快
Go代码实现:
package main
import (
"fmt"
"sort"
)
// 计算百分位数
func Percentile(data []int, p float64) int {
if len(data) == 0 {
return 0
}
// 排序
sorted := make([]int, len(data))
copy(sorted, data)
sort.Ints(sorted)
// 计算索引
index := int(float64(len(sorted)-1) * p)
return sorted[index]
}
func main() {
// 模拟1000个请求的响应时间
latencies := make([]int, 1000)
// 大部分请求很快
for i := 0; i < 950; i++ {
latencies[i] = 10 + i%5 // 10-14ms
}
// 一些慢请求
for i := 950; i < 990; i++ {
latencies[i] = 50 + i%20 // 50-69ms
}
// 极少数超慢请求
for i := 990; i < 1000; i++ {
latencies[i] = 500 + i%100 // 500-599ms
}
// 计算各项指标
sum := 0
for _, lat := range latencies {
sum += lat
}
avg := float64(sum) / float64(len(latencies))
p50 := Percentile(latencies, 0.50)
p90 := Percentile(latencies, 0.90)
p95 := Percentile(latencies, 0.95)
p99 := Percentile(latencies, 0.99)
p999 := Percentile(latencies, 0.999)
fmt.Println("性能指标对比:")
fmt.Printf("平均值: %.1f ms\n", avg)
fmt.Printf("P50 (中位数): %d ms\n", p50)
fmt.Printf("P90: %d ms\n", p90)
fmt.Printf("P95: %d ms\n", p95)
fmt.Printf("P99: %d ms\n", p99)
fmt.Printf("P999: %d ms\n", p999)
}
输出:
性能指标对比:
平均值: 23.5 ms
P50 (中位数): 12 ms
P90: 14 ms
P95: 57 ms
P99: 508 ms
P999: 598 ms
2.2 为什么要关注P99?
原因1:用户体验
- P50=12ms:一半用户体验很好
- P99=508ms:但1%的用户要等半秒
原因2:业务影响
- 如果每天100万请求
- 1%就是1万个慢请求
- 这1万个用户可能流失
原因3:系统瓶颈
- P99高说明系统有瓶颈
- 可能是慢查询、GC、网络抖动
三、监控报警实战
3.1 如何设置报警阈值?
错误做法:
平均响应时间 > 100ms → 报警
正确做法:
P99响应时间 > 200ms → 报警
P999响应时间 > 500ms → 报警
Go实现监控系统:
package main
import (
"fmt"
"sort"
"time"
)
type Monitor struct {
latencies []int
p99Threshold int
p999Threshold int
}
func NewMonitor(p99, p999 int) *Monitor {
return &Monitor{
latencies: []int{},
p99Threshold: p99,
p999Threshold: p999,
}
}
func (m *Monitor) Record(latency int) {
m.latencies = append(m.latencies, latency)
}
func (m *Monitor) Check() {
if len(m.latencies) == 0 {
return
}
sorted := make([]int, len(m.latencies))
copy(sorted, m.latencies)
sort.Ints(sorted)
p99 := sorted[int(float64(len(sorted)-1)*0.99)]
p999 := sorted[int(float64(len(sorted)-1)*0.999)]
fmt.Printf("当前指标: P99=%dms, P999=%dms\n", p99, p999)
if p99 > m.p99Threshold {
fmt.Printf("⚠️ 报警: P99超过阈值 (%d > %d)\n", p99, m.p99Threshold)
}
if p999 > m.p999Threshold {
fmt.Printf("🚨 严重报警: P999超过阈值 (%d > %d)\n", p999, m.p999Threshold)
}
}
func main() {
monitor := NewMonitor(200, 500)
// 模拟1000个请求
for i := 0; i < 1000; i++ {
var latency int
if i < 990 {
latency = 10 + i%50 // 正常请求
} else {
latency = 300 + i%200 // 慢请求
}
monitor.Record(latency)
}
monitor.Check()
}
输出:
当前指标: P99=308ms, P999=498ms
⚠️ 报警: P99超过阈值 (308 > 200)
四、小结
这篇文章的核心观点:
- 平均值会骗人,掩盖了极端情况
- P99是真实的性能指标,反映99%用户的体验
- 监控报警要看百分位数,不要只看平均值
- 1%的慢请求能毁掉用户体验,必须重视长尾延迟
- 统计学是性能优化的数学基础:监控、报警、容量规划都靠它
记住这个对照表:
指标
含义
适用场景
报警建议
平均值
所有请求的平均
粗略估算
不推荐用于报警
P50
50%用户体验
了解中位数
参考指标
P90
90%用户体验
常规监控
次要报警
P99
99%用户体验
核心监控
主要报警
P999
99.9%用户体验
极端情况
严重报警
实战建议:
- 监控面板必须显示P50/P90/P99/P999
- 报警阈值设置:P99 > 2倍P50,P999 > 5倍P50
- 优化优先级:先优化P99,再优化P50
下次面试官问"如何监控系统性能",你就可以回答:不看平均值看P99,因为平均值掩盖了长尾延迟,1%的慢请求能毁掉用户体验。
参考资料
- Google SRE Book:监控与报警
- 《高性能MySQL》:性能指标分析
- Prometheus文档:百分位数计算