Rust 高并发实战(6):压力测试、故障注入与线上排障
正常流量下快只是起点;过载时资源有界、故障后能够恢复,才是生产高并发。
事故架构图:慢依赖如何扩散到整个服务
故障不是“一个 API 变慢”这么简单:等待任务持有 handler 状态,超时触发重试,runtime 调度和内存压力再伤害原本健康的核心路径。独立 bulkhead、optional 降级和重试预算分别切断不同边。
一、压测五阶段
- 基线:低 RPS 校验 correctness 和观测;
- 阶梯:固定到达率增加,找吞吐/延迟拐点;
- 过载:150%~200% 容量验证快速拒绝;
- 故障:注入慢下游、错误、断连和 CPU 阻塞;
- 恢复:降回基线,确认 task、许可、连接、RSS 和 P99 回落。
wrk -t8 -c1000 -d60s --latency \
'http://127.0.0.1:8080/aggregate?items=3&delay_ms=20'
echo 'GET http://127.0.0.1:8080/aggregate?items=3&delay_ms=20' | \
vegeta attack -rate=12000/s -duration=2m | vegeta report
固定到达率更适合发现过载,闭环工具可能在服务变慢后自动降速。压测端与服务分离,记录版本、机器、worker threads、所有 semaphore/连接池和原始延迟分布。
二、五个故障实验
2.1 无界 spawn
while let Some(job) = input.recv().await {
tokio::spawn(process(job));
}
输入快于处理时,task 与 RSS 持续增长。改为有界 mpsc + worker,或先获取 permit 再 spawn,并用 JoinSet 收割结果。
2.2 Runtime worker 阻塞
注入同步 sleep/文件 I/O。证据:tokio-console task 长时间 busy、scheduled-to-poll 上升、线程栈停在阻塞函数。修复到 spawn_blocking 后验证 blocking pool 自身也有上限。
2.3 锁竞争/死锁
注入两条逆序锁路径。证据:CPU 低、吞吐归零、task/线程停在互相依赖资源。修复为统一锁序、拆锁或 actor,并固化 Loom/压力测试。
2.4 连接池与 Semaphore 双重排队
下游许可 768、数据库连接 100 会产生隐藏二次排队。分别记录 semaphore wait 和 pool wait;最靠近资源的一层设硬上限,入口层只做进程保护与短等待。
2.5 timeout 后副作用继续
对 spawn_blocking 或 detached 写任务设置 50 ms timeout,但工作 500 ms 后仍提交。通过 idempotency key、事务状态机、协作取消或补偿修复;验收不能只看 handler 已返回。
三、症状决策表
| 现象 | 先看 | 工具/证据 |
|---|---|---|
| CPU 高、吞吐不升 | hot path、allocator、忙 poll | perf/flamegraph |
| CPU 低、P99 高 | queue wait、锁、连接池 | tracing + tokio-console |
| task 数持续增长 | spawn/owner/cancel | console task 列表、span |
| RSS 持续增长 | channel、Arc、buffer | heap profiler、任务生命周期 |
| 周期尖刺 | throttle、下游、部署 | 系统指标 + trace 时间线 |
四、故障注入矩阵
| 注入 | 预期保护 | 必查恢复 |
|---|---|---|
| 下游延迟 20→500 ms | dependency semaphore + timeout | permit/task 回落 |
| 下游 50% 失败 | 重试预算 + 熔断 | 半开不形成洪峰 |
| DB pool 降到 10 | 短 acquisition deadline | pool wait 清零 |
| blocking work 2 s | CPU/blocking gate | worker 不饥饿 |
| 客户端中断 | Future drop/cancel token | 副作用与连接状态 |
| SIGTERM | 摘流 + drain deadline | 未完成任务有记录 |
每次只改变一个故障变量,保留健康对照实例。故障工具本身也要有停止条件,避免测试结束后继续注入。
五、线上排障决策路径
错误率高?
├─ 是:按 result class 区分拒绝/超时/依赖/内部错误
└─ 否:P99 高?
├─ CPU busy 高 → perf / busy task / allocator
└─ CPU busy 低 → queue wait / semaphore / pool / lock / network
task 或 RSS 不回落?
├─ task 增长 → detached spawn、channel 等待、取消路径
└─ task 稳定 → buffer/cache/Arc 环/allocator retained
始终对齐 absolute timeline:发布、配置、流量、依赖、节点 throttle 和告警。如果 tracing 时间与基础设施时间不同步,因果判断会失真。
六、容量与恢复报告
每轮压测输出:最大稳定 RPS、容量点 P99、拒绝开始点、峰值 task/RSS/连接、故障恢复时间、配置与 commit。只报告“峰值 80k QPS”无法指导生产部署。
用 service demand 反推 CPU:若 8 核在 20k RPS 时业务 CPU 80%,粗略 CPU demand 约为 8 × 0.8 / 20000 = 0.32 ms/request,再结合排队和下游容量判断扩容收益。
七、过载验收
容量内:P99 达标,错误接近零,吞吐随到达率增长。超过容量:429/503 增加,但已接纳请求 P99、task、queue、连接和 RSS 保持有界。恢复阶段要定义时间目标,例如 30 秒内许可和 task 回到基线。
八、生产 Runbook
- 确认 route、租户、实例、版本、地域和下游影响面;
- 限流、降级、熔断或摘异常实例,保留至少一个现场;
- 保存 RED/USE 指标、span、console/perf 样本、变更和依赖状态;
- 区分 running 与 waiting:perf 看运行,queue/span/console 看等待;
- 检查入口、下游、blocking pool、连接池四类队列;
- 审计 handler 返回后 task、permit、连接和副作用是否结束;
- 最小复现并固化为 Loom、压力或故障注入测试。
九、复盘必须生成的工程防线
一次事故至少产生一项可执行防线:容量阈值、queue wait 告警、并发回归、Loom 模型、故障注入场景、自动降级或 runbook。只写文档不改变检测/控制系统,事故很容易换一种触发方式再次出现。
行动项必须包含 owner、截止日期、验证方式和回滚条件。例如“优化连接池”不可验收;“将 DB acquisition P99 告警设为 20 ms,并在 12k RPS + 500 ms 延迟注入下验证 30 秒恢复”才可验收。
十、案例:慢依赖如何演变为全站故障
- 推荐服务 P99 从 30 ms 升到 800 ms;
- 聚合服务未限制推荐并发,task 和连接等待增长;
- 入口 300 ms timeout,但 detached 推荐 task 继续;
- 客户端重试使实际尝试翻倍;
- DB 与推荐共用入口许可,核心用户查询也被拒绝。
证据链应看到 dependency duration 先上升,随后 queue wait/task/RSS 上升,再出现 timeout/retry。修复组合:optional 依赖独立 bulkhead、总 deadline 下的子 timeout、结构化 Future、重试预算和 degraded response。
恢复验证:保持慢依赖 5 分钟时资源形成平台;恢复后 30 秒内 task/permit 回基线;核心查询 SLO 始终满足,而不是只验证最终没有崩溃。
10.1 端到端故障时间线
实验按固定到达率运行,客户端重试开关分两轮测试。第一轮证明服务端保护,第二轮验证真实客户端策略不会突破重试预算。
10.2 需要保存的原始证据
- 负载生成器的每秒实际 arrivals,而不只是完成 RPS;
- 每层 queue wait、work duration、timeout 与 result class;
- tokio-console recording 或关键 task/resource 截图;
- perf 样本、线程数、CPU throttle、RSS;
- 故障注入开始/停止的精确时间;
- 二进制 commit、feature、runtime worker 与所有池配置。
10.3 修复验收不是“错误率下降”
降级会让 HTTP 200 增加,但仍需检查 degraded=true;快速拒绝会提高 429,却保护核心 P99。验收要按业务结果分类:完整成功、降级成功、容量拒绝、依赖失败、deadline、取消,不能只看 2xx/5xx。
十一、最终检查清单
- 所有 channel、task、连接和队列都有 owner 与上限;
- 所有外部调用有剩余 deadline 和错误分类;
- 所有重试有幂等条件、jitter 与预算;
- 所有锁有顺序,锁内没有未知 await/I/O;
- async、blocking、CPU work 分池;
- 过载与恢复都经过固定到达率验证;
- 线上能从告警走到 span、task、线程和代码证据。
系列回顾:Rust 高并发实战 6 期路线。
对照阅读:Go 高并发实战 6 期路线。