压测不是跑一条 ab:wrk 阶梯加压、拐点判读与四条纪律

一、压测要先有数,再去压

小站做压测最容易走两个极端:一种是人云亦云地跟着教程跑一条 ab -n 10000 -c 100,拿到一个「每秒多少请求」的数字就贴在文档里;另一种是觉得压测是「大厂才干的事」,自己一台两核四 G 的机器没什么好压的。两种做法都浪费时间。真正有用的压测只回答一个问题:我的机器在哪个点开始撑不住,撑不住的时候先坏的是哪一环。

回答这个问题之前,必须先从自己的访问日志里量出三个数:日常峰值 QPS、页面平均请求数和静态资源占比。这三个数决定了压测的目标区间。如果日志里日常峰值是 3 QPS,压测就从 5 QPS 开始阶梯上调,而不是从 500 开始——500 时的表现对你毫无意义,因为你永远不会到那里,而中间那段「甜蜜点到拐点」的区间反而被跳过了。

还有一个常被忽略的前提:压测一定要在独立的一台机器上跑客户端。压测工具本身吃 CPU,和被测服务在同一台机器上,测出来的数字至少偏低三成,而且根本分不清「是服务慢还是客户端跑不动」。租一个小时最便宜的云主机就够用,这笔钱比反复猜要划算得多。

二、先划清压测的四条纪律

压测不是普通的读操作,它会真实地消耗资源、写日志、可能触发自动封禁。上线前给自己定四条纪律,能避免九成事故。

第一,只压自己的站,并且提前在防火墙层面限定来源 IP。如果前面有 WAF、CDN 或 fail2ban,先确认压测客户端的 IP 在白名单里,否则第二轮就会被封,数据直接作废。第二,压测期间关掉日志落盘或改为采样。Nginx 的 access log 在几百 QPS 下能把磁盘 IO 打满,测出来的瓶颈会变成「磁盘写日志」而不是应用本身。第三,避免压测污染真实数据:评论、搜索、下单这类写接口不要压,或者用一份从生产导出的测试库。第四,压测前记录一份基线,包括 CPU 核数、内存、PHP-FPM 进程数、MySQL 关键参数,否则测完无法解释结果。

这四条都不是技术难题,但不做的话,你会在花了两个小时后发现所有数据都得扔掉。我自己的做法是在压测前写一个五分钟的准备脚本,把基线参数和「关闭日志」的配置变更一起做完,压完再一键还原。

三、wrk 的基本用法与三个必设参数

工具选择上,ab 太老、单线程、不支持长连接压测;JMeter 太重,需要图形界面;locust 适合写复杂场景但要写代码。对个人站长最合适的是 wrk:单机多线程、支持长连接、脚本能力够用,安装一条命令。

apt-get install -y wrk
# 或者
git clone https://github.com/wg/wrk && cd wrk && make && cp wrk /usr/local/bin/

基本用法是 wrk -t4 -c100 -d30s URL,四个参数分别代表线程数、连接数、持续时间和目标地址。三个必设项容易被忽略:一是 -t 线程数不要超过客户端机器的 CPU 核数,超了只会互相抢,吞吐反而下降;二是 -c 连接数要与服务端 worker_connections 和 PHP-FPM 进程池对得上,否则你测的是「连接排队」而不是「处理能力」;三是 -d 持续时间至少 30 秒,10 秒的结果波动很大,尤其是带缓存预热的场景前几秒是冷的。

另外一个细节:wrk 默认开启长连接(HTTP/1.1 keep-alive),这一点比 ab 更接近真实浏览器行为。但如果你的 Nginx 把 keepalive_timeout 设得很短(比如 5 秒),长连接会被频繁重建,测出来的 QPS 会明显低于真实场景。压测前先确认这个参数,或者干脆在压测时临时调大,测完还原。

四、阶梯加压:从日常峰值的三倍开始,每档翻倍

单点压测只能得到一个数,阶梯压测才能得到曲线。做法是从日常峰值的三倍开始,每档把并发翻一倍,每档跑 60 秒,记录 QPS、平均延迟、P99 延迟和错误率。翻倍的目的不是追求精细,而是让拐点足够明显——在拐点前后,P99 的跳变通常是指数级的,翻倍加压能一眼看出来。

for c in 10 20 40 80 160 320; do
  echo "=== concurrency $c ==="
  wrk -t4 -c$c -d60s --latency https://example.com/ \
    | grep -E 'Requests/sec|Latency|Non-2xx'
done

记录时要写全四项,缺一项都没法判断。这里最容易犯的错是只看 QPS:QPS 在饱和前几乎是一条直线,看不出任何问题;而 P99 延迟的走势才是最早的预警信号。当 P99 开始以每档两三倍的速度上涨,而 QPS 只涨了 10%,说明系统已经进入排队区,再往上压只会让延迟继续恶化。

还有一个必须盯住的数字是错误率。Non-2xx or 3xx 一旦不是 0,说明这一档已经越过了可用性红线,无论 QPS 多高都不算数。常见的错误是 502(PHP-FPM 进程池打满,Nginx 拿不到 worker)和 504(网关超时),两者的处理方向完全不同:502 要调进程池,504 要看后端的慢在哪里。

五、读结果:四个数字背后的四种瓶颈

拿到一档数据之后,怎么判断瓶颈在哪一层?方法是在压测的同一时刻,在服务端看四个指标,它们分别对应四个层面的问题。

第一个是 CPU 的 %us 与 %sy 比例。%sy 偏高说明大量时间花在内核态,通常意味着系统调用过多——连接建立太频繁、文件描述符操作多,或者是网络包的收发出了问题。%us 偏高才是应用自身在算。第二个是 %wa,如果它明显不为 0,说明有进程在等磁盘 IO,这时候要查是不是日志、session 文件或 MySQL 的临时表在拖后腿。第三个是 vmstat 1 里的 r 列(运行队列长度),如果它持续大于 CPU 核数,说明请求在排队等 CPU。第四个是 ss -s 里的 TIME_WAIT 与 ESTAB 数量,暴增的 TIME_WAIT 说明连接复用没做好。

把这四个指标和 wrk 的错误类型对上,基本就能定位了:CPU 满了且错误是 502,去看 FPM 进程池;CPU 没满但延迟暴涨且 %wa 高,去看磁盘和数据库;CPU 和磁盘都不高但延迟也高,多半是上游(CDN、负载均衡)或网络层的问题。最怕的是只看压测端数字不看服务端,那样只能得出「慢」这个结论,对优化毫无帮助。

六、一次真实的拐点复盘

说一个具体的例子。一台两核四 G 的机器,Nginx 加 PHP-FPM,日常峰值 6 QPS 左右,页面平均 42 个请求。阶梯压测从 20 并发开始,结果如下:20 并发 QPS 38、P99 210ms;40 并发 QPS 52、P99 480ms;80 并发 QPS 55、P99 1900ms;160 并发 QPS 54 且开始出现 502。

QPS 在 55 左右就平了,而 P99 从 480ms 一路涨到 1900ms。服务端看 vmstat,%us 到 92%,%wa 接近 0,r 列到 4~6。结论很清楚:CPU 打满,瓶颈在 PHP 执行而不是 IO。再往下查,PHP-FPM 的 pm.max_children 设的是 50,80 并发时连接数够,但 CPU 只有两核,50 个 PHP 进程互相抢占,上下文切换本身就是浪费。

调整方式是把 max_children 从 50 降到 12,同时打开 opcache 并把 pm.max_requests 设成 500。改完之后重跑,40 并发时 QPS 58、P99 260ms,80 并发时 QPS 62、P99 520ms,160 并发时 QPS 63、P99 900ms 且不再出现 502。数字提升不算夸张,但延迟曲线从「陡峭上升」变成了「缓慢上升」,这才是有意义的改善——它意味着系统在超载时有缓冲,不会一压就崩。

这个例子说明一个反直觉的结论:把进程数调小反而更抗压。进程数不是越大越好,它的上限由 CPU 核数与单请求的 CPU 占用共同决定,设过大只会增加上下文切换和内存占用,反而提前触发 OOM。

七、静态资源与动态页要分开压

很多教程直接用首页 URL 压测,这个数字其实混合了静态文件和动态请求,无法归因。正确的做法是分两轮:第一轮压一张图片或一个 CSS 文件,这测的是 Nginx 的静态文件处理能力、sendfile 与缓存的有效性;第二轮压一个纯动态页(比如一篇文章的详情页),这测的是 PHP、数据库和模板渲染。

两轮的数字对比很有信息量。如果静态文件能到几千 QPS 而动态页只有几十,说明瓶颈在应用层,这时候优化方向是缓存、减少 SQL、减少模板渲染次数,而不是换更强的服务器。如果静态文件的 QPS 也不高(比如只有三四百),那就要查 Nginx 自身配置和网络带宽——常见原因是没开 sendfile、开了 gzip 但压缩级别过高、或者带宽本身就跑满了。

八、把压测结论落成配置变更

压测的价值全部体现在「压完之后改了什么」。一份好的压测报告里,每一档数据都应该对应一条明确的动作建议,而不是一句「建议升级配置」。我把结论整理成三个层级,按需选择。

第一层是参数层:调整 FPM 进程池、Nginx 的 worker_processes 与 worker_connections、MySQL 的连接数与 buffer pool、系统的文件描述符上限与 TCP 参数。这一层成本最低,通常能把拐点往后推 30%~80%。第二层是缓存层:给动态页加整页缓存或片段缓存,把重复计算消掉。这一层的效果往往是指数级的,但要注意缓存失效逻辑,否则会引入新问题。第三层是架构层:拆分静态与动态、加 CDN 分流、读写分离。对小站来说这一层通常不需要,除非用户量真的上了一个台阶。

顺序不能颠倒。先改参数再看缓存,是因为参数问题会干扰缓存效果的评估;先做缓存再改参数,很可能在缓存命中率变化后又要重测一遍。每次只改一层,改完立刻重跑同一档压测,用同一个指标对比。这个纪律听起来死板,但它能避免「改了一大堆也不知道是哪个生效」的常见困境。

九、压测之后必须做的一件事:还原

压测会留下很多痕迹:临时关闭的日志、调大的连接参数、白名单里的 IP、脏掉的缓存、写入数据库的测试数据。这些如果不还原,会在几天后以莫名其妙的形式爆发——比如日志一直没开,出了事故没有记录;比如白名单里的 IP 被扫到,成为后门入口。

我的做法是压测脚本里带一个 trap,无论正常结束还是中途中断,都执行还原逻辑;同时在压测报告最后附一份「变更清单」,逐条写明改了什么、还原成什么值、什么时候还原的。这份清单比压测数字本身更重要,因为它是下次压测的起点,也是出事时唯一能追溯的依据。

最后说一句实话:对大多数个人站来说,压测的正确产出不是「我的站能扛多少并发」,而是「我知道它在什么情况下会先坏、坏在哪个环节、我该按什么顺序去救」。这个认知在真实流量突然上涨的那一天,比任何一个 QPS 数字都有用。

Last modification:November 13th, 2026 at 10:24 pm

Leave a Comment