服务器压测实战:用 ab 与 wrk 找到个人站点的性能拐点,Nginx/PHP-FPM/MySQL 三层瓶颈定位

为什么单台 VPS 也需要压测

很多个人站长有一个误解:压测是大厂才需要做的事,单台 VPS 跑一个小站,几万 PV 一天,根本用不上。这个想法在网站没火之前看起来没问题,一旦某篇文章被推荐、某个搜索结果突然排到首页第一页,流量在几个小时内从几百 IP 涨到几千 IP,服务器很可能就在那个时间点倒下——而你不知道它到底能扛多少,也不知道它是在哪一层先撑不住。

压测的价值不在于「测出服务器能扛多少」这个数字,而在于主动暴露瓶颈。一台 2 核 4G 的 VPS,瓶颈可能在 Nginx 的连接数、可能在 PHP-FPM 的进程池、可能在 MySQL 的连接上限、也可能在网络带宽。不压测,你永远只能等它真的挂了才知道;压测一次,你能在十分钟内把这几层全部排出来。

本文面向个人站长,用最基础的 ab 和 wrk 两个工具走完一整套流程:本地装工具、安全地设计并发阶梯、读懂输出里的关键数字、把 Nginx、PHP-FPM、MySQL 三层逐个压到瓶颈、最后用 htop 和 ss 在压测中观察现场。全文假设你的站点是典型的 LNMP 结构。

压测之前必须搞清楚的三件事

直接对线上站点开炮是新手最容易犯的错误,后果可能包括:被机房判断为攻击而封 IP、把自己网站压挂导致真实用户无法访问、触发云厂商的流量计费。所以开压之前,请先确认三件事。

第一,压测目标要对。 压测的目标应该是「某个具体接口或页面」,而不是整个网站。首页通常静态内容多、缓存命中高,压出来数字很好看,但它不代表你站点的真实压力点。真正该压的是那些会走 PHP、会查数据库的动态页面,比如文章详情页、搜索页、评论提交接口。

第二,压测时间要避开高峰。 如果你的站有真实访问,压测请安排在凌晨。更稳妥的做法是先在测试环境或同机房的另一台机器上压,确认参数合理后再对线上做一次小规模验证。

第三,搞清楚压测机在哪。 压测机到服务器的网络路径会极大影响结果。如果压测机走的是公网,你测出来的可能是「公网带宽瓶颈」而不是「服务器处理能力」。理想的压测机应该和被测服务器在同一内网或同机房,此时带宽接近 1Gbps,能真实反映应用的 CPU 和内存极限。

# 确认压测机到服务器的延迟与丢包
ping -c 20 your-domain.com

# 确认带宽上限(粗略)
curl -o /dev/null -w "%{speed_download}\n" https://your-domain.com/static/bigfile.zip

安装 ab:最简单的起点

ab(ApacheBench)属于 apache2-utils 包,几乎所有发行版都有,它只能做 GET/POST,但胜在零配置、输出直观,非常适合做第一轮摸底。

# Debian / Ubuntu
apt-get update
apt-get install -y apache2-utils

# RHEL / CentOS
yum install -y httpd-tools

ab -V

一次典型调用如下:

ab -n 2000 -c 50 -k \
   -H "Accept-Encoding: gzip, deflate" \
   "https://your-domain.com/post/123.html"

# -n 总请求数
# -c 并发数(同时进行的请求)
# -k 启用 HTTP Keep-Alive,避免每次重建 TCP 连接
# -H 追加请求头,这里模拟浏览器声明支持 gzip

注意 -k。默认情况下 ab 每次请求都新建 TCP 连接,测出来的是「每秒能建立多少连接」,这个数字在真实场景里没有意义,因为浏览器一定会复用连接。加上 -k 后测的才是应用的处理吞吐。

读懂 ab 输出:五个真正重要的数字

ab 的输出很长,但绝大部分可以忽略。真正需要盯住的是下面这几个。

Document Path:          /post/123.html
Document Length:        28541 bytes

Concurrency Level:      50
Time taken for tests:   12.418 seconds
Complete requests:      2000
Failed requests:        0
Keep-Alive requests:    2000
Total transferred:      58100000 bytes
HTML transferred:       57082000 bytes
Requests per second:    161.06 [#/sec] (mean)
Time per request:       310.45 [ms] (mean)
Time per request:       6.209 [ms] (mean, across all concurrent requests)
Transfer rate:          4569.10 [Kbytes/sec] received

Percentage of the requests served within a certain time (ms)
  50%    281
  66%    330
  75%    366
  90%    470
  95%    552
  98%    741
  99%    918
 100%   1420 (longest request)
  • Failed requests:必须为 0。只要非 0,先别管性能,去查日志——通常是 PHP 报错、502 或超时,性能数字全部作废。
  • Requests per second:吞吐量,压测的核心指标。注意它和并发数是联动的,单独看没有意义。
  • Time per request(第一行):单次请求的平均耗时,单位毫秒。这个数字直接对应真实用户的等待感受。
  • 90% / 95% / 99% 分位:比平均值重要得多。平均值会被大量快速请求拉低,掩盖掉慢请求。用户感受到的是 95 分位甚至 99 分位,不是平均值。
  • 100% (longest request):最长的一次请求耗时。如果它远高于 99 分位,说明存在偶发的长尾,通常和锁、GC、磁盘 IO 抖动有关。

一个健康的判断标准:在你的目标并发下,Failed requests = 0,且 95 分位耗时低于 500ms,这基本就是一个体验良好的站点。

用 wrk 做更接近真实的压测

ab 的模型是「固定并发、每个连接发完一个请求再发下一个」,比较呆板,而且单线程实现,在高并发下它自己会先成为瓶颈。换成 wrk,用多线程 + 事件驱动(epoll),几千并发都能压出来,并且支持 Lua 脚本定制请求。

# 装 wrk(需要从源码编译,依赖很少)
apt-get install -y build-essential libssl-dev git
git clone https://github.com/wg/wrk.git
cd wrk
make -j$(nproc)
cp wrk /usr/local/bin/

wrk -v

一次典型调用:

wrk -t4 -c200 -d30s --latency \
    -H "Accept-Encoding: gzip" \
    "https://your-domain.com/post/123.html"

# -t4    4 个线程
# -c200  200 个并发连接
# -d30s  持续 30 秒
# --latency 打印延迟分布(等价于 ab 的分位表,但更细)

输出样例:

Running 30s test @ https://your-domain.com/post/123.html
  4 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   118.42ms   64.10ms 812.00ms   71.20%
    Req/Sec   428.15     96.44   640.00     65.10%
  Latency Distribution
     50%  102.00ms
     75%  148.00ms
     90%  212.00ms
     99%  398.00ms
  51012 requests in 30.07s, 1.42GB read
Requests/sec:   1696.31
Transfer/sec:     48.31MB

wrk 和 ab 结果对不上的时候,以 wrk 为准。 因为 ab 自身单线程在高并发下会拖后腿,经常出现 ab 测出来 150 QPS、wrk 测出来 1600 QPS 的情况。不是服务器变快了,是压测工具不再拖后腿了。

用并发阶梯找出拐点在哪

压测最常见的错误是「只测一个并发数」。正确答案是压一组阶梯,找出吞吐量停止增长、延迟开始暴涨的那个点,也就是拐点(knee)。拐点之前的并发是你系统的舒适区,拐点之后就是雪崩区。

#!/bin/bash
# /root/bench_ladder.sh —— 并发阶梯压测
URL="https://your-domain.com/post/123.html"
for c in 10 30 50 100 200 400 800; do
    echo "=== concurrency=$c ==="
    wrk -t4 -c${c} -d15s --latency "$URL" 2>&1 \
      | grep -E "Requests/sec|Latency |50%|99%"
    sleep 5
done

把结果抄进一张表,你会看到典型的三段式:

并发    QPS      平均延迟   99分位
10      890      11ms       28ms
30      1420     21ms       55ms
50      1680     30ms       92ms     <-- 线性区
100     1900     52ms       180ms
200     2050     97ms       410ms    <-- 拐点附近,QPS 涨不动了
400     1980     201ms      980ms    <-- 过载,QPS 反降
800     1640     487ms      2400ms   <-- 雪崩

规律很清楚:并发从 10 加到 100,QPS 一路涨;到 200 时 QPS 几乎不再增长,但延迟翻倍——这就是拐点。再到 400、800,QPS 反而下降,延迟成倍上升,说明请求开始排队、超时、重试,系统已经过载。

拐点对应的 QPS 才是你系统真实的能力上限。 那个 2050 不是,1900 也不是,真正该记住的是「在延迟可接受的前提下能稳定输出多少」,通常取拐点前一级,也就是 100 并发 / 1900 QPS。

压测的同时看现场:htop 与 ss

光看 QPS 数字不够,必须一边压一边看机器上到底发生了什么。开两个 SSH 窗口,一边跑压测,另一边执行下面的观察命令。

# 窗口 1:压测
wrk -t4 -c200 -d60s --latency "https://your-domain.com/post/123.html"

# 窗口 2:实时观察
htop                            # 看 CPU 各核占用、load average、内存
vmstat 1                        # 看 r(运行队列)、si/so(swap)、wa(IO 等待)
ss -s                           # 连接状态总览:TIME-WAIT、ESTAB
ss -lnt                         # 监听队列,看 Send-Q 是否长期不为 0
iostat -x 1                     # 看磁盘 %util 和 await

几个关键的判读信号:

  • CPU 单核跑满、其余空闲:说明有单线程瓶颈。PHP 是单进程处理请求的,多核要靠 PHP-FPM 的多进程分摊;如果只有一核满,通常是没配好进程池,或者有全局锁。
  • load average 远大于核数:说明大量任务在排队。2 核机器 load 到 8 就是严重过载。
  • vmstat 里 r 列持续大于核数:同样是排队信号。
  • wa(IO 等待)很高:磁盘成瓶颈了。对博客类站点,通常是日志写得太多或 MySQL 在刷盘。
  • ss -lnt 的 Send-Q 长期非 0:监听队列堆积,说明 Nginx 处理不过来新连接,通常是 backlog 或 worker 数量不够。
  • ss -s 里 TIME-WAIT 上万:连接被大量短连接消耗,考虑启用 keepalive 或 tcp_tw_reuse。

把三层瓶颈逐个压出来

LNMP 站点的瓶颈永远落在三层之一:Nginx、PHP-FPM、MySQL。逐层定位的方法很直接。

第一层:先压纯静态资源,确认 Nginx 和网络本身的能力。

# 放一个 100KB 的静态文件,直接压它
dd if=/dev/urandom of=/var/www/html/bench.bin bs=1k count=100
wrk -t4 -c500 -d20s --latency "https://your-domain.com/bench.bin" | grep -E "Requests/sec|Transfer"

静态文件能跑出几千 QPS 且 CPU 很闲,说明 Nginx 和带宽没问题,瓶颈在后面。如果连静态都跑不动,先检查网卡带宽和 worker_connections。

第二层:压动态页面,看 PHP-FPM 是不是瓶颈。

# 观察 PHP-FPM 状态(需要先开启 status 页)
curl -s http://127.0.0.1/phpfpm_status?full

# 关键字段
# pool:                 www
# active processes:     45     <-- 正在干活的进程
# idle processes:        5     <-- 空闲进程
# listen queue:          0     <-- 排队等待的请求,持续非 0 就是不够用
# max children reached:  0     <-- 是否触顶,>0 说明进程池被打满

如果 active processes 长期等于 pm.max_children、listen queue 持续堆积、max children reached 不为 0,那么 PHP-FPM 就是瓶颈。解决办法是提高 pm.max_children,但要先算清楚内存:每个 PHP 进程大约占用 30–60MB,4G 内存的机器最多留 2.5G 给 PHP-FPM,也就是 40–60 个进程。

; /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 8
pm.max_spare_servers = 20
pm.max_requests = 800          ; 每个进程处理 800 次后重启,防内存泄漏
pm.status_path = /phpfpm_status

第三层:如果 PHP-FPM 很闲但页面还是慢,问题在 MySQL。

# 压测进行中,在 MySQL 里看正在跑的语句
mysql -e "SHOW FULL PROCESSLIST\G" | grep -A2 "State: Sending data"

# 看 InnoDB 的行锁等待
mysql -e "SELECT * FROM information_schema.innodb_trx\G" | head -40

# 看谁在占用连接
mysql -e "SHOW STATUS LIKE 'Threads_connected'; SHOW STATUS LIKE 'Max_used_connections';"

压测中如果出现大量 State: Sending data 或 Copying to tmp table,说明查询本身需要优化,跟服务器配置无关——去加索引、去改 SQL。如果只是连接数被打满,调 max_connections 并排查是否有连接泄漏。

个人站长的压测清单与常见坑

最后把整套流程浓缩成一张可执行的清单,每次上线新版本或调优后照着跑一遍。

  1. 基线:先测静态文件,记下 Nginx + 带宽的上限。
  2. 阶梯:动态页面做 10/30/50/100/200/400 并发阶梯,找出拐点。
  3. 观察:每次压测同时开 htop、vmstat、ss,记录 CPU、load、连接状态。
  4. 定位:静态正常 → PHP-FPM 状态页 → MySQL processlist,逐层排除。
  5. 记录:把每次的 QPS、延迟、拐点写进一个表格,调优前后对比。
  6. 复测:任何配置变更后重跑阶梯,确认拐点右移或延迟下降。

几个必须避开的坑:

  • 不要在压测机上同时跑压测和被压的服务,CPU 会互相抢,结果完全不可信。
  • 不要只信一次结果。压测波动很大,同一组参数至少跑三次取中间值。
  • 不要忘了压测机本身的限制。 单台压测机跑 wrk -t4 -c800 时,它自己的网卡和 CPU 也可能饱和,这时候数字反映的是压测机的能力。要更高必须在多台机器上分布式压。
  • 不要拿压测数字当 SLA 承诺。 压测是理想条件:无真实网络抖动、无爬虫、无恶意流量。真实流量下能跑出压测的 60%–70% 就该满意了。
  • 压完之后一定要把测试文件删掉,bench.bin 留在网站根目录既是垃圾也可能被搜索引擎抓取。

压测这件事,对个人站长来说投入产出比极高:花一个晚上把上面这套流程跑通,你对自己服务器的认识会从「大概能扛点流量」变成「清楚知道拐点在 200 并发、瓶颈在 PHP-FPM、加 4G 内存就能把拐点推高一倍」。这比盲买更高配的机器划算得多。

Last modification:October 1st, 2026 at 08:24 pm

Leave a Comment