2026-01-04 · 架构
32
架构 · 2026-01-04

程序员数学06:统计学 - P99延迟监控

本文是《程序员数学扫盲课》系列文章

← 上一篇:程序员数学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)

四、小结

这篇文章的核心观点:

  1. 平均值会骗人,掩盖了极端情况
  2. P99是真实的性能指标,反映99%用户的体验
  3. 监控报警要看百分位数,不要只看平均值
  4. 1%的慢请求能毁掉用户体验,必须重视长尾延迟
  5. 统计学是性能优化的数学基础:监控、报警、容量规划都靠它

记住这个对照表:

指标
含义
适用场景
报警建议

平均值
所有请求的平均
粗略估算
不推荐用于报警

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文档:百分位数计算


返回系列总览

👉 程序员数学扫盲课:完整课程大纲

目录 最新
← 左侧翻上一屏 · 右侧翻下一屏 · 中间唤出菜单