本文是《程序员数学扫盲课》系列文章
← 上一篇:程序员数学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文档:百分位数计算








程序员数学扫盲课
AI周刊:大模型、智能体与产业动态追踪
Claude Code 全体系指南:AI 编程智能体实战
Karpathy神经网络零基础课程
最新评论
开源的AI对话监控面板很实用,正好团队在找这类工具。准备试用一下。
折叠屏市场确实在升温,不过售罄也可能是备货策略。期待看到实际销量数据。
从磁盘I/O角度解释B树的设计动机,这个切入点很好。终于理解为什么数据库不用二叉树了。
IT术语转换确实是个痛点,之前用搜狗总是把技术词汇转成奇怪的词。智谱这个方向值得期待。
这个工具结合LLM和搜索API的思路很有意思,正好解决了我在做知识管理时遇到的问题。请问有没有部署文档?
这个漏洞确实严重,我们团队上周刚遇到类似问题。建议补充一下如何检测现有项目是否受影响的方法。
从简单规则涌现复杂性这个思路很有意思,让我想起元胞自动机。不过数字物理学在学术界争议还挺大的。
我也遇到了指令跟随变差的问题,特别是多轮对话时容易跑偏。不知道是模型退化还是负载优化导致的。