第 19 章 · 监控与结果分析
本章目标:理解 p50/p95/p99 百分位指标的含义,掌握 Locust 与 Prometheus/Grafana 的集成方法,学会系统性地定位性能瓶颈。
19.1 理解响应时间百分位
平均响应时间是最容易误导人的指标——它会被大量快请求稀释掉少量慢请求的影响。生产级性能分析必须关注尾部延迟:
text
100 个请求的响应时间分布:
p50 (中位数) = 120ms ← 一半用户比这快
p90 = 350ms ← 90% 用户比这快
p95 = 500ms ← 95% 用户比这快(SLA 常用标准)
p99 = 1200ms ← 尾部 1% 用户可能等 1.2 秒!
平均值 = 180ms ← 被大量快请求拉低,掩盖了问题为什么平均值会骗人
假设 95 个请求耗时 50ms,5 个请求耗时 2000ms:
- 平均值 = (95×50 + 5×2000) / 100 = 147ms ← 看起来还行
- p95 = 2000ms ← 暴露了真实问题!
如果你的 SLA 是"99% 的请求在 500ms 内完成",平均值完全无法反映真实情况。
Locust 内置统计解读
Locust Web UI 和 CSV 报告中的关键列:
| 列名 | 含义 | 关注点 |
|---|---|---|
| Average | 平均响应时间 | 参考值 |
| Median (50%) | 中位数 | 正常用户体验 |
| 95% | p95 | SLA 阈值常用 |
| 98% | p98 | 高要求场景 |
| 99% | p99 | 极端尾部延迟 |
| RPS | 每秒请求数 | 吞吐量 |
| Failures/s | 失败率 | 稳定性 |
19.2 Prometheus + Grafana 集成
Locust 支持通过事件钩子将统计数据推送到 Prometheus,实现实时监控和历史趋势分析。
方式一:使用 locust-exporter(推荐)
locust-exporter 是一个独立的 Prometheus exporter,直接从 Locust master 的 API 拉取数据:
yaml
# docker-compose.yml — 一键启动完整监控栈
version: "3.8"
services:
locust:
image: locustio/locust
ports:
- "8089:8089"
volumes:
- ./locustfile.py:/mnt/locust/locustfile.py
command: -f /mnt/locust/locustfile.py --master
prometheus:
image: prom/prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana
ports:
- "3000:3000"yaml
# prometheus.yml
scrape_configs:
- job_name: "locust"
static_configs:
- targets: ["locust:8089"]
metrics_path: /export/prometheus方式二:自定义事件钩子推送
python
import time
from locust import events
class MetricsCollector:
"""收集自定义指标并通过 OpenTelemetry 上报"""
def __init__(self):
self.request_count = 0
self.error_count = 0
self.total_response_time = 0.0
self.slow_requests = []
@events.request.add_listener
def track_metrics(request_type, name, response_time, response_length, exception, **kwargs):
collector.request_count += 1
collector.total_response_time += response_time
if exception:
collector.error_count += 1
if response_time > 800:
collector.slow_requests.append({
"name": name,
"time": response_time,
"timestamp": time.time()
})19.3 实时图表解读
Locust 自带的 Web UI 提供两张核心图表:
Total Requests per Second(RPS 图)
- 绿色线:成功的请求/秒
- 红色线:失败的请求/秒
- 黄色区域:当前并发用户数
Response Times(响应时间图)
- 展示中位数和 95% 百分位随时间的变化
图表异常模式识别
| 模式 | 可能原因 |
|---|---|
| RPS 锯齿状剧烈波动 | wait_time 太短或被测服务不稳定 |
| RPS 平坦但用户数增加 | 被测系统已饱和(吞吐量上限) |
| 响应时间阶梯式上升 | 连接池耗尽或数据库锁竞争 |
| 失败率突然飙升 | 服务重启、连接超时、资源耗尽 |
| 响应时间周期性尖刺 | GC 暂停或定时任务冲突 |
19.4 瓶颈定位方法论
当发现性能问题时,按以下顺序排查:
text
第 1 步:确认压测工具本身不是瓶颈
└→ 检查 Locust CPU < 80%(见第 17 章)
第 2 步:区分"所有接口都慢"还是"特定接口慢"
└→ 看 Locust 统计表中各接口的分列数据
第 3 步:如果只有特定接口慢
├→ 该接口是否涉及数据库查询?
│ └→ 用 EXPLAIN ANALYZE 检查慢 SQL
├→ 是否调用了外部 API?
│ └→ 检查外部服务的响应时间
└→ 是否有 N+1 查询问题?
第 4 步:如果所有接口都慢
├→ 检查服务器 CPU/内存/磁盘 I/O
├→ 检查数据库连接池大小
└→ 检查线程池/workers 配置实战案例:定位数据库连接池瓶颈
python
# 场景:增加用户数后 RPS 不再增长,p95 从 200ms 升到 2000ms
# 1. 在被测 FastAPI 应用添加诊断日志
import time
from contextlib import asynccontextmanager
@asynccontextmanager
async def timed_db_session():
start = time.monotonic()
session = SessionLocal()
acquire_time = time.monotonic() - start
if acquire_time > 0.1: # 获取连接超过 100ms 说明池不够
logger.warning(f"DB pool wait: {acquire_time:.3f}s")
try:
yield session
finally:
session.close()
# 2. 日志显示 "DB pool wait: 1.2s" → 确认是连接池太小
# 3. 解决方案:增大 pool_size 或使用 NullPool本章小结
- 平均响应时间会掩盖尾部延迟,必须看 p95/p99;
- Prometheus + Grafana 提供 Locust 数据的持久化和可视化;
- RPS 图表的异常形态能快速指向问题类型;
- 排查顺序:先排除 Locust 自身 → 区分全局/局部 → 逐层深入。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 为什么 p95 比平均响应时间更有参考价值?
2. RPS 曲线平坦但用户数持续增加,最可能的原因是什么?
3. 发现某个特定 API 的 p99 远高于其他接口,应该首先检查什么?
4. 以下哪种现象最可能表明存在数据库连接池不足的问题?
🛠️ 动手实践
- 对 httpbin.org 做 3 分钟压测,导出 CSV 后用 Python pandas 绘制 p50/p95/p99 三条曲线。
- 搭建一个包含 Locust + Prometheus + Grafana 的 Docker Compose 环境,观察 Grafana 中的实时 RPS 图。
- 故意在一个 FastAPI 接口中加入
time.sleep(2),然后通过 Locust 统计数据找出这个"慢接口",记录你的排查过程。
完成练习后,进入下一章:综合实战。