《RESAR 性能工程实战》第 2 篇:基准场景实战 —— wrk 压测与软中断证据链
《RESAR 性能工程实战》第 2 篇:基准场景实战 —— wrk 压测与软中断证据链
📚系列目录(全部源码与原始实验日志:GitCode 仓库 https://gitcode.com/cpyaxjq/resar-perf-in-action )
① 开篇:一小时四台 ECS 搭起完整性能实验场 ② 基准场景:wrk 压测与软中断证据链 ③ 容量场景:MySQL 索引优化与 sysbench 梯度压测 ④ 稳定性与异常:Redis 混沌工程四连击 ⑤ 性能结论:生产配置建议
系列导航:本篇是《RESAR 性能工程实战》系列第 2 篇,聚焦「基准场景」。
前情提要(第 1 篇):性能工程方法论与 RESAR 七步分析法总览。
下篇预告(第 3 篇):容量场景与混合业务模型。
一、前言
很多团队一上来就做"容量场景"“全链路压测”,结果报告里只有一句"QPS 上不去"。问题出在哪?你连单接口的天花板(最大 TPS)都不知道,怎么判断线上扛不扛得住、瓶颈到底在哪?
在 RESAR 性能工程体系中,基准场景(Baseline / Single-Interface Benchmark)是整个性能测试的地基:在"干净、无干扰"的条件下,把单个接口 / 单类请求压到它的最大 TPS,拿到一组可复现的基线数据。这组基线将在后续容量场景、稳定性场景、异常场景中作为"标尺"——任何偏离基线的退化,都意味着系统发生了异常。
本文我们用 4 台华为云 ECS,遵循 RESAR 七步分析法,完整走一遍基准场景:
- 定义场景:单接口梯度加压,找到最大 TPS;
- 搭建环境:压力机 → 应用服务器的内网拓扑;
- 执行梯度:静态页、纯 CPU 接口、DB 链路接口;
- 采集证据:wrk 完整输出 + mpstat + pidstat + /proc/softirqs + /proc/interrupts + ss;
- 定位瓶颈:用决策树锁定"进程数不足";
- 调优验证:2 worker → 8 worker,TPS 2.37 倍提升;
- 得出结论:该接口的最大 TPS 到底是多少。
所有数字均来自真实实验回显,绝不编造。
二、环境与架构
2.1 硬件与系统
| 项 | 配置 |
|---|---|
| 云厂商 / 规格 | 华为云 ECS,8C16G(8 vCPU = 4 核 × 2 线程,General Purpose Processor) |
| 操作系统 | Ubuntu 24.04.4 LTS |
| 内核 | 6.8.0-106-generic(x86_64) |
| 内存 | 14Gi 可用,Swap 0B |
| 系统盘 | /dev/vda1,40G,使用 9% |
| 压测工具 | wrk debian/4.1.0-4build2 [epoll]、sysbench 1.0.20、sysstat |
| 应用栈 | nginx + gunicorn + Flask(Python3) |
环境规格通过lscpu/free -h/df -h/uname -a真实采集:
Architecture: x86_64 CPU(s): 8 Model name: General Purpose Processor Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Mem: 14Gi total, 531Mi used, 14Gi free Swap: 0B Linux ecs-5807-0001 6.8.0-106-generic #106-Ubuntu SMP PREEMPT_DYNAMIC ... x86_642.2 拓扑架构(ASCII)
公网(仅SSH运维,已打码) │ ┌────────────────────────┼────────────────────────┐ │ M1 压力机 │ M2 应用服务器 │ M3 / M4 │ 192.168.0.214 │ 192.168.0.102 │ (备用/扩展) │ (公网 113.44.***.***) │ │ │ │ nginx :80 │ │ wrk ───────────────▶│ │ │ │ 压测流量(内网) │ ▼ │ │ │ gunicorn :8000 │ │ │ │ │ │ │ ▼ │ │ │ Flask app.py │ │ │ ├─ / 静态页│ │ │ ├─ /api/health 纯CPU│ │ │ ├─ /api/product MySQL │ │ └─ /api/product_cached Redis └────────────────────────┴────────────────────────┘ 内网 192.168.0.0/24(压测流量走这里)2.3 为什么必须走内网压测
这是基准场景的第一条铁律。压测本质是要排除一切外部干扰、只观察被测系统本身。如果压测流量走公网:
- 带宽瓶颈会掩盖应用瓶颈:公网带宽(几 Mbps~几百 Mbps)往往远小于 ECS 内网(Gbps 级),你测到的"上限"其实是公网带宽上限,而不是应用 TPS;
- 公网延迟与抖动不可控:跨地域、跨运营商的 RTT 与丢包会污染 P99/P999 延迟,让你无法区分"应用慢"还是"网络慢";
- 费用与合规:公网流量按量计费,且暴露攻击面。
因此本实验四台机器通过ping互验证内网互通(192.168.0.214/102/50/226全部 OK),wrk 直接打http://192.168.0.102/。公网 IP(如 113.44..)仅用于 SSH 登录运维,不参与压测链路。
三、场景设计(RESAR 七步法之"场景定义")
基准场景要回答三个问题:这个接口最多能跑多快?瓶颈卡在哪?调优后天花板在哪?
我们设计了四组基准子场景,按"从简到繁"逐步加链路:
| 子场景 | 接口 | 后端依赖 | 目的 |
|---|---|---|---|
| S1 静态页 | / | nginx 直接回静态页 | 测"纯转发 + 软中断"上限 |
| S2 纯 CPU | /api/health | 无(仅返回 JSON) | 测单进程计算上限,验证 worker 数 |
| S3 带 DB | /api/product | MySQL 查询 | 测"应用 + 数据库"链路上限 |
| S4 带缓存 | /api/product_cached | Redis 查询 | 与 S3 对比,量化"缓存跳"的价值 |
压测统一用wrk -d30s --latency,梯度提升线程-t与连接-c:-t2/-c50、-t4/-c100、-t8/-c200。
四、梯度执行与真实回显
4.1 静态页梯度:171k → 217k → 触顶
===== G1_T2C50 ===== ($ wrk -t2 -c50 -d30s --latency http://192.168.0.102/) 2 threads and 50 connections Latency Distribution 50% 240.00us 99% 412.00us 5154773 requests in 30.10s, 1.56GB read Requests/sec: 171255.54 Transfer/sec: 53.08MB ===== G2_T4C100 ===== ($ wrk -t4 -c100 -d30s --latency http://192.168.0.102/) 4 threads and 100 connections Latency Distribution 50% 443.00us 99% 622.00us 6527273 requests in 30.01s, 1.98GB read Requests/sec: 217472.98 Transfer/sec: 67.40MB ===== G3_T8C200 ===== ($ wrk -t8 -c200 -d30s --latency http://192.168.0.102/) 8 threads and 200 connections Latency Distribution 50% 0.90ms 99% 1.22ms 6528660 requests in 30.10s, 1.98GB read Requests/sec: 216901.42 Transfer/sec: 67.23MB关键观察:RPS 从 G1 的 171k 跃升到 G2 的 217k,但 G3(-t8 -c200)只到 216.9k ——并发翻倍,吞吐不再增长,延迟反而从 443us 升到 0.90ms。典型的"触顶"信号:资源已经打满,再加压力只会堆延迟。
4.2 纯 CPU 接口 /api/health:2 worker 只有 7.4k
===== API_HEALTH_C100 ===== ($ wrk -t4 -c100 -d30s --latency http://192.168.0.102/api/health) 4 threads and 100 connections Latency Distribution 50% 13.62ms 99% 14.52ms 221228 requests in 30.02s, 42.77MB read Requests/sec: 7369.924.3 带 DB 链路:/api/product 5.3k,/api/product_cached 12.6k
===== API_PRODUCT_C100 ===== ($ wrk -t4 -c100 -d30s --latency "http://192.168.0.102/api/product?id=77777") Latency Distribution 50% 18.85ms 99% 19.80ms 158901 requests in 30.01s, 32.88MB read Requests/sec: 5294.40 ===== API_CACHED_C100 ===== ($ wrk -t4 -c100 -d30s --latency "http://192.168.0.102/api/product_cached?id=77777") Latency Distribution 50% 7.87ms 99% 8.97ms 378389 requests in 30.01s, 81.91MB read Requests/sec: 12608.91五、瓶颈定位证据链(RESAR 七步法之"分析定位")
光看 RPS 不够,RESAR 强调证据链闭环。下面用四组现场回显,把"为什么触顶 / 为什么这么慢"钉死。
5.1 静态页:瓶颈在 M2 整体 CPU(含软中断)
压测中在 M2 跑mpstat -P ALL 5 5,avg 值直指 CPU 100%:
Average: all 14.57 0.00 59.17 0.00 0.00 25.94 0.00 0.00 0.00 0.32 Average: 0 16.67 0.00 67.85 0.00 0.00 15.15 0.00 0.00 0.00 0.32 Average: 1 12.63 0.00 51.18 0.00 0.00 35.83 0.00 0.00 0.00 0.36 Average: 2 16.31 0.00 67.49 0.00 0.00 15.87 0.00 0.00 0.00 0.32 Average: 3 12.57 0.00 50.82 0.00 0.00 36.29 0.00 0.00 0.00 0.32 Average: 4 12.52 0.00 50.56 0.00 0.00 36.60 0.00 0.00 0.00 0.32 Average: 5 16.41 0.00 67.43 0.00 0.00 15.85 0.00 0.00 0.00 0.32 Average: 6 12.72 0.00 50.76 0.00 0.00 36.24 0.00 0.00 0.00 0.28 Average: 7 16.72 0.00 67.24 0.00 0.00 15.72 0.00 0.00 0.00 0.32解读:%sys(59%)+%soft(26%)合计 85%,%idle仅 0.32。说明 CPU 几乎全部耗在内核态系统调用 + 软中断上,用户态(%usr 14%)反而很低——这是典型的"网络收包 + 协议栈处理"特征。且%soft在 8 核上均匀分布(15%~37%),说明软中断已被打散到所有核(后文 RPS=ff 印证)。
再看/proc/softirqs的 NET_RX 在压测前后 25 秒的增量:
=== softirqs BEFORE (t=0) === CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7 NET_RX: 2379559 4734462 2414284 4731764 4752505 2437732 4761922 2480083 === softirqs AFTER (t=25s) === CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7 NET_RX: 3117998 6113216 3157352 6109760 6146291 3184420 6137347 3234985读增量:CPU1 净增约 138 万、CPU4 净增约 139 万……NET_RX 软中断被均衡分散到全部 8 个核。这 25 秒内光 NET_RX 就累计处理了上千万次收包软中断——单台 8C 机器处理小包转发的软中断能力,正是 217k RPS 的天花板。
ss -s看连接状态,确认是大量短连接 TIME_WAIT 而非连接堆积:
Total: 390 TCP: 5159 (estab 204, closed 4947, orphaned 0, timewait 4947)5.2 纯 CPU 接口:瓶颈在"进程数不足"
/api/health只返回 JSON、不查库,TPS 却只有 7.4k,且延迟高达 13ms。在 M2 上pidstat抓 worker 进程:
02:29:52 PM 0 8045 75.25 24.75 0.00 0.00 100.00 2 gunicorn 02:29:52 PM 0 8046 76.05 23.95 0.00 0.00 100.00 6 gunicorn ... Average: 0 8045 76.36 23.59 0.00 0.00 99.95 - gunicorn Average: 0 8046 76.26 23.69 0.00 0.00 99.95 - gunicorn铁证:只有2 个 gunicorn worker(PID 8045/8046),每个都跑满 100% CPU,且只占用 2 个核(CPU 2 和 6/7)。再看此时mpstat all:%idle高达 66%——机器 8 核,只用了 2 核,另外 6 核在围观。
走 RESAR 决策树:
- 接口是纯计算、无外部依赖 → 不是 DB/缓存/网络外部依赖;
- 进程 CPU 打满、机器整体 idle 很高 →瓶颈是"并发处理能力 = worker 数"不足,而不是硬件不够;
- 决策:扩大 gunicorn worker 数到与 CPU 核数匹配。
5.3 调优:2 worker → 8 worker,TPS 2.37 倍
重启为 8 worker(注意--daemon,见踩坑章节):
===== RESTART ===== ($ pkill gunicorn; sleep 2; cd /opt/app && gunicorn -w 8 -b 127.0.0.1:8000 --daemon --access-logfile /dev/null app:app; sleep 1; echo "worker_count=$(pgrep gunicorn | wc -l)") worker_count=9重新压/api/health -t4 -c100:
===== API_HEALTH_C100 ===== ($ wrk -t4 -c100 -d30s --latency http://192.168.0.102/api/health) Latency Distribution 50% 5.59ms 75% 5.84ms 90% 6.43ms 99% 8.31ms 524841 requests in 30.01s, 101.47MB read Requests/sec: 17490.317,369 → 17,490,提升 2.37 倍,P99 从 14.5ms 降到 8.3ms。
再上强度-t8 -c300:
===== API_HEALTH_C300 ===== ($ wrk -t8 -c300 -d30s --latency http://192.168.0.102/api/health) Latency Distribution 50% 16.83ms 75% 17.27ms 90% 17.96ms 99% 19.85ms 524812 requests in 30.10s, 101.47MB read Requests/sec: 17436.95TPS 不增反微降(17,490 → 17,437),延迟却从 5.6ms 涨到 16.8ms。此刻mpstat -P ALL显示 8 核全部吃满:
Average: all 64.16 0.00 21.60 0.01 0.00 12.63 0.00 0.00 0.00 1.60 Average: 0 65.21 0.00 22.14 0.00 0.00 11.24 0.00 0.00 0.00 1.41 Average: 1 63.07 0.00 21.73 0.05 0.00 13.55 0.00 0.00 0.00 1.61 Average: 7 68.62 0.00 19.50 0.00 0.00 10.53 0.00 0.00 0.00 1.35pidstat也确认 8 个 worker 已铺满所有核(每个70%87% CPU,分布在 CPU 0~7)。结论:8 核 8 worker,CPU 100% 饱和,17.5k TPS 就是/api/health这台机器上的最大 TPS。
六、调优对比表(基准结论一览)
| 场景 | 配置 | 并发 | RPS | P50 | P99 | 瓶颈判定 |
|---|---|---|---|---|---|---|
| 静态页 G1 | nginx | -t2 -c50 | 171,256 | 240us | 412us | 未触顶 |
| 静态页 G2 | nginx | -t4 -c100 | 217,473 | 443us | 622us | 触顶(CPU+软中断) |
| 静态页 G3 | nginx | -t8 -c200 | 216,901 | 0.90ms | 1.22ms | 触顶、延迟升 |
| /api/health | 2 worker | -t4 -c100 | 7,370 | 13.6ms | 14.5ms | 进程数不足 |
| /api/health | 8 worker | -t4 -c100 | 17,490 | 5.6ms | 8.3ms | 调优后 |
| /api/health | 8 worker | -t8 -c300 | 17,437 | 16.8ms | 19.9ms | 8 核到顶=最大 TPS |
| /api/product | 8w+MySQL | -t4 -c100 | 5,294 | 18.9ms | 19.8ms | DB 链路拖累 |
| /api/product_cached | 8w+Redis | -t4 -c100 | 12,609 | 7.9ms | 9.0ms | 缓存提吞吐降延迟 |
链路递减规律一目了然:纯转发 217k > 纯 CPU 17.5k > 缓存 12.6k > 直查 DB 5.3k。每一跳外部依赖都会拉低整条链路的 TPS;直查 MySQL 相比 Redis 缓存,TPS 仅为其 42%(5.3k / 12.6k ≈ 0.42),延迟却高 2.4 倍——这就是缓存价值最硬的证据。
图:四类接口的 RPS/TPS 对比(对数轴),每多一跳外部依赖,吞吐量掉一个数量级
图:gunicorn worker 从 2 扩到 8,TPS 提升 2.37 倍、P99 延迟降低 43%
七、软中断专题(呼应课程第 14 讲:网络软中断)
基准场景里静态页的瓶颈直指软中断,这里把底层机理和真实配置讲透。
7.1 多队列网卡 + 中断亲和
/proc/interrupts显示 virtio 网卡开了 4 个接收队列,分别亲和到不同 CPU:
44: 201 5611393 0 0 0 0 1 0 ... virtio0-input.0 46: 1 0 36 0 0 0 5622686 0 ... virtio0-input.1 48: 0 0 2 0 5653719 1372 0 0 ... virtio0-input.2 50: 0 0 0 5598781 1 0 37 0 ... virtio0-input.3即virtio0-input.0→CPU1、virtio0-input.1→CPU6、virtio0-input.2→CPU4、virtio0-input.3→CPU3。硬件中断(%irq)被分散到 4 个核,避免单核被网卡中断打死。
7.2 RPS = ff:把 NET_RX 软中断再打散到全核
硬件中断只到 4 核,但收包后的协议栈处理(NET_RX 软中断)还可以用RPS(Receive Packet Steering)继续均衡。本机配置:
/sys/class/net/eth0/queues/rx-0/rps_cpus = ff /sys/class/net/eth0/queues/rx-1/rps_cpus = ff /sys/class/net/eth0/queues/rx-2/rps_cpus = ff /sys/class/net/eth0/queues/rx-3/rps_cpus = ff /sys/class/net/lo/queues/rx-0/rps_cpus = 00ff(十六进制)= 二进制 11111111,表示"允许把该队列的收包软中断派发到 CPU0~CPU7 全部 8 个核"。正因为开了 RPS=ff,我们在 5.1 节才看到 NET_RX 软中断均匀落在 8 个核上,而不是堆在某 4 核。这正是高 PPS 场景下消除"单核软中断瓶颈"的标准做法。
课程第 14 讲结论在此得到验证:软中断不是敌人,关键是把它均衡掉。均衡后,单台 8C 机器收包软中断上限(约 217k RPS 小包)就是静态页的天花板。
八、踩坑清单
| 序号 | 现象 | 根因 | 正确做法 |
|---|---|---|---|
| 1 | nohup gunicorn ... >log 2>&1 &执行后SSH 会话挂死 60s 超时([!! TIMEOUT 60s !!]) | nohup ... &后 shell 仍持有 gunicorn 的 stdout/stderr 文件描述符,SSH 会话要等子进程彻底脱离才返回;前台&在远程脚本里极易挂住 | 改用gunicorn --daemon(自带 daemonize,自动脱离终端、关闭继承的 fd),或setsid ... &/disown |
| 2 | 静态页 G3 并发翻倍但 RPS 不涨、延迟翻倍 | 误以为"加线程加连接就能加吞吐",实际 CPU 已 100% 饱和 | 触顶后应先看 mpstat 确认资源饱和,再决定调优方向,而非盲目加压 |
| 3 | 纯 CPU 接口 TPS 仅 7.4k 却以为是机器不行 | 没看 pidstat,不知道只起了 2 个 worker、只用了 2 核 | 任何"CPU 型接口"先pidstat确认 worker/线程是否铺满所有核 |
| 4 | 压测初期拿公网 IP 打 | 公网带宽/延迟会污染基线 | 一律走内网 IP 压测,公网仅用于 SSH 运维 |
第一条坑是真实踩出来的:boot_m2.log里那条nohup ... &命令最终[!! TIMEOUT 60s !!],而后续重启统一改用gunicorn -w 8 ... --daemon,worker_count=9立即正常返回。
九、总结与下篇预告
通过本篇基准场景,我们拿到一组可复现的基线:
- 静态页(nginx 纯转发 + 软中断)最大约217k RPS;
- 纯 CPU 接口
/api/health经 worker 调优后,8 核 8 worker 最大 17.5k TPS,再加压只涨延迟; - 链路递减:纯 CPU 17.5k > Redis 缓存 12.6k > MySQL 直查 5.3k,证明每一跳依赖都拉低整体 TPS;
- 软中断是静态页天花板的根因,RPS=ff 多队列均衡是标准解法。
这些数字,就是后续所有场景的"标尺"。
下篇预告(第 3 篇:容量场景):在基准场景基线上,引入混合业务模型(静态页 : 健康 : 商品 = 业务比例),用阶梯加压找到"业务最大容量"与"拐点",并用 RESAR 容量判定法区分"资源饱和型拐点"和"架构瓶颈型拐点"。
本文实验数据均来自真实环境实操,AI 辅助整理成文。
