Skip to main content

Rust 高并发实战(6):压力测试、故障注入与线上排障

Rainy
雨落无声,代码成诗 —— 致力于技术与艺术的极致平衡
Rainy
8 MIN READ... VIEWS

正常流量下快只是起点;过载时资源有界、故障后能够恢复,才是生产高并发。

事故架构图:慢依赖如何扩散到整个服务

故障不是“一个 API 变慢”这么简单:等待任务持有 handler 状态,超时触发重试,runtime 调度和内存压力再伤害原本健康的核心路径。独立 bulkhead、optional 降级和重试预算分别切断不同边。

一、压测五阶段

  1. 基线:低 RPS 校验 correctness 和观测;
  2. 阶梯:固定到达率增加,找吞吐/延迟拐点;
  3. 过载:150%~200% 容量验证快速拒绝;
  4. 故障:注入慢下游、错误、断连和 CPU 阻塞;
  5. 恢复:降回基线,确认 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、忙 pollperf/flamegraph
CPU 低、P99 高queue wait、锁、连接池tracing + tokio-console
task 数持续增长spawn/owner/cancelconsole task 列表、span
RSS 持续增长channel、Arc、bufferheap profiler、任务生命周期
周期尖刺throttle、下游、部署系统指标 + trace 时间线

四、故障注入矩阵

注入预期保护必查恢复
下游延迟 20→500 msdependency semaphore + timeoutpermit/task 回落
下游 50% 失败重试预算 + 熔断半开不形成洪峰
DB pool 降到 10短 acquisition deadlinepool wait 清零
blocking work 2 sCPU/blocking gateworker 不饥饿
客户端中断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

  1. 确认 route、租户、实例、版本、地域和下游影响面;
  2. 限流、降级、熔断或摘异常实例,保留至少一个现场;
  3. 保存 RED/USE 指标、span、console/perf 样本、变更和依赖状态;
  4. 区分 running 与 waiting:perf 看运行,queue/span/console 看等待;
  5. 检查入口、下游、blocking pool、连接池四类队列;
  6. 审计 handler 返回后 task、permit、连接和副作用是否结束;
  7. 最小复现并固化为 Loom、压力或故障注入测试。

九、复盘必须生成的工程防线

一次事故至少产生一项可执行防线:容量阈值、queue wait 告警、并发回归、Loom 模型、故障注入场景、自动降级或 runbook。只写文档不改变检测/控制系统,事故很容易换一种触发方式再次出现。

行动项必须包含 owner、截止日期、验证方式和回滚条件。例如“优化连接池”不可验收;“将 DB acquisition P99 告警设为 20 ms,并在 12k RPS + 500 ms 延迟注入下验证 30 秒恢复”才可验收。

十、案例:慢依赖如何演变为全站故障

  1. 推荐服务 P99 从 30 ms 升到 800 ms;
  2. 聚合服务未限制推荐并发,task 和连接等待增长;
  3. 入口 300 ms timeout,但 detached 推荐 task 继续;
  4. 客户端重试使实际尝试翻倍;
  5. 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 期路线

Logo
RainLib

Exploring the frontiers of technology, design, and distributed systems. Building tools for the future developers.

Suggestions & Feedback

© 2026 RainLib. Built for the Future.
All rights reserved.
System Normal