为什么单台 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.31MBwrk 和 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 并排查是否有连接泄漏。
个人站长的压测清单与常见坑
最后把整套流程浓缩成一张可执行的清单,每次上线新版本或调优后照着跑一遍。
- 基线:先测静态文件,记下 Nginx + 带宽的上限。
- 阶梯:动态页面做 10/30/50/100/200/400 并发阶梯,找出拐点。
- 观察:每次压测同时开 htop、vmstat、ss,记录 CPU、load、连接状态。
- 定位:静态正常 → PHP-FPM 状态页 → MySQL processlist,逐层排除。
- 记录:把每次的 QPS、延迟、拐点写进一个表格,调优前后对比。
- 复测:任何配置变更后重跑阶梯,确认拐点右移或延迟下降。
几个必须避开的坑:
- 不要在压测机上同时跑压测和被压的服务,CPU 会互相抢,结果完全不可信。
- 不要只信一次结果。压测波动很大,同一组参数至少跑三次取中间值。
- 不要忘了压测机本身的限制。 单台压测机跑
wrk -t4 -c800时,它自己的网卡和 CPU 也可能饱和,这时候数字反映的是压测机的能力。要更高必须在多台机器上分布式压。 - 不要拿压测数字当 SLA 承诺。 压测是理想条件:无真实网络抖动、无爬虫、无恶意流量。真实流量下能跑出压测的 60%–70% 就该满意了。
- 压完之后一定要把测试文件删掉,
bench.bin留在网站根目录既是垃圾也可能被搜索引擎抓取。
压测这件事,对个人站长来说投入产出比极高:花一个晚上把上面这套流程跑通,你对自己服务器的认识会从「大概能扛点流量」变成「清楚知道拐点在 200 并发、瓶颈在 PHP-FPM、加 4G 内存就能把拐点推高一倍」。这比盲买更高配的机器划算得多。