一、先破除一个概念:并发不是商家说了算

「这台机器支持 5000 并发」这句话在 VPS 的销售页面里几乎随处可见,但它既不是行业标准,也不是任何厂商的承诺。它通常指的是同时建立的 TCP 连接数,而不是同时处理的业务请求数。这两者之间可以差十倍甚至更多。
一个 2 核 2GB 的 VPS,Linux 内核允许的长连接数确实可以轻松设到几万——Nginx 的 worker_connections 调到 65535 也没人拦你。但一个用户打开一个页面,后台可能同时发起 25 个 HTTP 请求(1 个 HTML 加 20 个静态资源加几个接口),这 25 个请求在服务器上是串行或小批量地交给 PHP worker 处理的。真正决定「能同时服务多少人」的是四件事:
第一是 PHP-FPM 能同时跑多少个 worker 进程。 每个 worker 是一个独立的 PHP 解释器实例,各占一份内存。2GB 内存跑 15 个 worker 就到顶了,再多就 swap。
第二是数据库能同时开多少连接。 MySQL 默认 max_connections 是 151,但每个连接都要吃内存,连接数超过 CPU 能并行处理的数量后,数据库会从「并发处理」退化成「排队处理」。
第三是 CPU 核数决定的并行度。 一个核同一时刻只能执行一个线程的任务。PHP 是同步阻塞模型,一个请求不返回,这个 worker 就被占住。
第四是你的代码效率。 一个查了 20 条 SQL 的页面,和一个查 1 条 SQL 的页面,并发能力相差 20 倍。
所以「并发多少」这个问题没有通用答案,但有确定的算法。接下来三节就是这套算法。
在此之前,先做一个必要的区分:三种并发的成本完全不同。
静态资源并发(图片、CSS、JS)是最便宜的一种。Nginx 直接用 sendfile 把文件从磁盘推出去,几乎不消耗 CPU,1 个 worker 就能同时处理几百个静态请求。这类并发基本不用担心。
动态页面并发(PHP 渲染)是主流场景,CPU 和内存都要付钱。1 个请求 = 1 个 PHP worker 占着 + 1 到 5 条数据库查询。
长连接并发(WebSocket、SSE、实时推送)是最贵的一种。每条连接常驻内存、长期占用一个文件描述符,而且连接数有硬上限(受 ulimit -n 和内存共同限制)。1 万条连接在 2GB 内存的机器上根本放不下。
很多人在讨论「并发」时把这三种混在一起算,结果得出了一个看似很大、实际毫无意义的数字。先明确你要扛的是哪一种,再往下算。
# 先看你的请求构成:静态还是动态
sudo awk '{print $7}' /var/log/nginx/access.log | awk -F. '{print $NF}' | sort | uniq -c | sort -rn | head -10
# 看请求方法的分布(POST 通常意味着动态业务)
sudo awk '{print $6}' /var/log/nginx/access.log | tr -d '"' | sort | uniq -c | sort -rn
这条命令的输出直接告诉你,如果 80% 的请求是静态资源,那么你的并发瓶颈几乎一定在动态请求那一小部分上,而扩容静态资源的承载力是浪费钱。
二、算法一:算出你的 PHP 进程上限

公式:pm.max_children ≈ (物理内存 MB × 70%) ÷ 单个 PHP worker 常驻内存 MB
这个 70% 是留给操作系统、Nginx、数据库、文件缓存和安全余量的。剩下 30% 不是浪费——文件缓存要靠它,突发流量要靠它,swap 抖动要靠它。
第一步:测单个 worker 的真实内存占用。 不要靠猜。装好你的应用、跑起来、跑一段真实流量,然后量:
# 方法一:看 PHP-FPM 所有进程的平均 RSS
ps -o rss= -C php-fpm | awk '{s+=$1; n++} END {printf "worker 平均 RSS: %.0f MB,共 %d 个进程\n", s/n/1024, n}'
# 方法二:开 PHP-FPM 状态页,看更精确的数值
# 在 php-fpm pool 配置里加:
# pm.status_path = /fpm-status
RSS 包含共享内存的重复计数,作为估算够用了。如果你跑的是 WordPress 且装了 OPcache,每个 worker 的内存明显比裸 PHP 大——OPcache 的共享内存是所有 worker 共享的,计入第一个 worker 的 RSS 但不重复计入其他 worker,所以用平均值会有偏差,往上取整更安全。
第二步:套公式。 几个具体算例:
| 物理内存 | 可用内存(×70%) | 单 worker 占用 | max_children | 说明 |
|---|---|---|---|---|
| 1GB | 716MB | 60MB | 11 | 单站点够用,务必同时开 Opcache |
| 2GB | 1433MB | 60MB | 20 | 常见起点,留 2 到 3 个余量取 15 到 18 |
| 2GB | 1433MB | 90MB(带 Opcache 的 WP) | 15 | 装 WP 时按这一行算 |
| 4GB | 2867MB | 90MB | 28 | 可以同时跑两个站点加队列 |
| 8GB | 5725MB | 90MB | 55 | 留足余量给数据库和文件缓存 |
第三步:验证你配得对不对。 关键指标是「活跃进程数」和「空闲进程数」:
# 看 PHP-FPM 状态页的字段
# active processes:正在处理请求的进程
# idle processes:空闲待命
# max active processes:历史上同时活跃的峰值
# max children reached:达到过上限的次数(这个值 > 0 说明你配小了)
max children reached 大于 0 就是明确的信号:请求已经开始排队。排队就是并发的真实天花板,不管你的 worker_connections 配多大。
location /fpm-status {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
allow 127.0.0.1;
deny all;
}
; /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 18
pm.start_servers = 4
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.max_requests = 500
pm.max_requests = 500 是防内存泄漏的关键:worker 处理满 500 个请求就重启回收。不设这个值,机器跑上几天会越来越慢,这是「并发没问题但用久了就卡」的常见根因。
一个折中方案:改用 pm = ondemand。 它不预启动 worker,有请求才起,用完 pm.process_idle_timeout 秒后回收。适合流量波峰波谷明显的业务——白天高并发,晚上没流量。代价是每个新请求要等 worker 启动(约 10 到 30 毫秒),对首字节延迟不友好。如果你在意的就是打开速度,用 dynamic;如果你在意的是内存不被空转的 worker 占着,用 ondemand。
三、算法二:数据库连接数怎么定

数据库是并发系统里最容易先崩的一环。它的连接上限和内存直接挂钩,公式是:
max_connections ≈ (数据库可用内存 MB) ÷ 256MB
这个 256MB 是每个连接的常见平均开销(含连接私有内存、排序缓冲、临时表)。如果是 MariaDB 10.6+ 或 MySQL 8.0,每连接基础开销可以低到 32MB 左右;但如果应用里用了复杂的排序和 GROUP BY,实际会高得多。
给 2GB 机器算一遍。 假设物理内存 2GB,其中 PHP 需要 1GB(按 15 个 worker × 60MB 加余量算),系统和其他占 300MB,那么数据库可用大约 700MB。
- 保守算法:700 ÷ 256 ≈ 2 到 3 个连接上限?不对,这太少了。
- 实际做法:数据库的 max_connections 设成 100,但 PHP 侧用持久连接复用,实际同时活跃的连接数控制在 20 以内。
关键区别在这里:max_connections 是崩溃保护线,不是性能目标。设成 151(默认)或 100 是防止恶意连接耗尽内存,真正的并发能力取决于同时活跃的连接数。而这个数字由 PHP 侧的连接池控制。
; /etc/php/8.2/fpm/pool.d/www.conf
; 每个 worker 的数据库连接上限,18 个 worker × 5 = 90 个活跃连接上限
pm.max_children = 18
// WordPress 的 wp-config.php 里限制每个请求的数据库连接
define( 'DB_HOST', '127.0.0.1' );
define( 'DB_CHARSET', 'utf8mb4' );
[mysqld]
max_connections = 100
innodb_buffer_pool_size = 512M # 2GB 机器给数据库 25% 左右
innodb_log_buffer_size = 8M
table_open_cache = 400
怎么知道数据库是不是并发瓶颈? 看这三个指标:
-- 当前活跃连接数
SHOW STATUS LIKE 'Threads_connected';
-- 历史峰值
SHOW STATUS LIKE 'Threads_connected' ;
SHOW STATUS LIKE 'Max_used_connections';
-- 正在执行的查询(Threads_running 持续 > CPU 核数 = 已经过载)
SHOW STATUS LIKE 'Threads_running';
判断标准很明确:Threads_running 持续大于 CPU 核数,就说明数据库的查询处理已经排队了。 这时候再加 PHP worker 只会让更多查询堆积在数据库前面,响应更慢。正确的动作是优化查询、加索引,或者直接上读写分离。
-- 看有没有慢查询在堆积
SHOW GLOBAL STATUS LIKE 'Slow_queries';
SHOW VARIABLES LIKE 'long_query_time';
-- 看当前正在跑什么
SELECT id, user, db, command, time, state, LEFT(info, 150) AS query
FROM information_schema.processlist
WHERE command != 'Sleep' ORDER BY time DESC;
四、算账:并发能力与单价的换算关系

把前两节的公式和商品库里的真实价格放在一起,才能算清楚「为并发花多少钱最划算」。下面这张表是渠道库里的真实机型(数据来自渠道库快照,价格随商家调整而变化,此处只用于横向比较):
| 机型 | 折月价 | CPU | 内存 | 美元/GB内存 | 美元/GB盘 | 美元/TB流量 |
|---|---|---|---|---|---|---|
| RosCloud-01 | 19.99 | 2 | 1G | 19.99 | 0.999 | 19.99 |
| RosCloud-02 | 29.99 | 3 | 2G | 14.99 | 0.750 | 14.995 |
| RosCloud-03 | 39.99 | 4 | 4G | 10.00 | 0.500 | 13.33 |
| RosCloud-04 | 49.99 | 5 | 8G | 6.25 | 0.312 | 12.498 |
| RosCloud-05 | 59.99 | 6 | 16G | 3.75 | 0.250 | 11.998 |
| RosCloud-06 | 69.99 | 7 | 24G | 2.92 | 0.219 | 11.665 |
这张表能推出一个反直觉的结论:加内存比加 CPU 更划算。
用第二节的公式算一遍各档位的 PHP 进程上限(按单 worker 60MB 计):
| 机型 | 内存 | 可用内存(×70%) | max_children | 相对上一档的并发提升 | 折月价增量 |
|---|---|---|---|---|---|
| RosCloud-01 | 1G | 716MB | 11 | 基准 | 基准 |
| RosCloud-02 | 2G | 1433MB | 23 | +109% | +10.00 |
| RosCloud-03 | 4G | 2867MB | 47 | +104% | +10.00 |
| RosCloud-04 | 8G | 5725MB | 95 | +102% | +10.00 |
| RosCloud-05 | 16G | 11468MB | 191 | +101% | +10.00 |
| RosCloud-06 | 24G | 17203MB | 286 | +50% | +10.00 |
看这张表:每加 10 美元,PHP 并发能力大约翻一倍,而且从 RosCloud-01 到 RosCloud-05,每 GB 内存的单价从 19.99 美元降到 3.75 美元,降幅 81%。而如果只加 CPU 不加内存,并发能力完全不变——因为 PHP-FPM 的进程数被内存卡住了,多出来的 CPU 核只能让单个请求跑得快一点,不能让更多请求同时跑。
这就是并发场景下最核心的一条:并发能力的瓶颈是内存,不是 CPU。
再看流量这一列。并发数上去之后,流量消耗会同步上升:
月出站流量 ≈ 日 PV × 平均页面大小 × 30 × 1.3(缓存未命中与重传冗余系数)
举例:一个页面平均 500KB(未压缩),日 PV 10000:
- 10000 × 500KB × 30 = 150GB,加上 1.3 系数 = 195GB/月
- 如果并发是峰值 200 人,这 195GB 摊在 30 天里,日均 6.5GB,对应峰值带宽需求 = 6.5GB × 8 ÷ 86400 ≈ 0.6Mbps
这个结果说明一件事:静态内容为主的站点,带宽压力很小,瓶颈在 PHP 和数据库。而如果页面带视频或大图(页面平均 5MB),同样的 PV 会产生 1.95TB/月,峰值带宽需求直接跳到 6Mbps 以上,这时流量额度和带宽上限才成为真瓶颈。
对照上表:RosCloud-01 的流量是 1024GB,折合 19.99 美元每 TB;RosCloud-06 是 6144GB,11.665 美元每 TB。流量的单价降幅(42%)远小于内存单价的降幅(81%),所以并发密集、流量也大的业务,升配时优先看内存和流量额度两项,别只盯着带宽数字。
必须说清楚的边界:以上是横向比较配置性价比的框架。韩国节点的机型目前没有结构化套餐数据可查——justg、vpckr、zji、恒创科技、vollcloud 等商家都有首尔节点,但具体配置与报价以官网当日报价为准。你要做的是把自己的需求换算成「我要 8GB 内存能撑 95 个 PHP worker、每月 2000GB 流量」,然后拿这个需求去比,而不是抄别人的价格。
五、压测:怎么测出你自己机器的真实并发

公式算出来的只是理论值,真实并发必须压测出来。压测的目的不是测出「最高能撑多少」(那个数字会大得没有意义),而是找出响应时间开始明显恶化的拐点。
工具选择:wrk / ab / hey / k6。 前面三个只测 HTTP,k6 能写脚本模拟真实用户行为。对于 PHP 站点,先用 wrk 快速摸边界。
# 安装
apt install wrk
# 基础压测:2 核 4GB 机器,压 100 并发 30 秒
wrk -t4 -c100 -d30s --latency https://你的站点/
# 带脚本的 POST 接口压测
wrk -t4 -c50 -d30s -s post.lua https://你的站点/api
# 结果里最关键的三行:
# Latency 平均延迟
# 50%/90%/99% 分位延迟
# Requests/sec 吞吐量
关键在于分位延迟,不是平均延迟。 平均延迟 50ms 但 99 分位 3000ms,意味着每 100 个请求里有 1 个等 3 秒——用户感知到的就是「偶尔卡一下」。判断标准是:
合格线:99 分位延迟低于 1000ms,同时错误率低于 0.1%。
阶梯加压,找出拐点。 每次把并发翻倍,直到错误率超过 0.1% 或 99 分位超过 1 秒:
for c in 10 25 50 100 200 400 800; do
echo "=== 并发 $c ==="
wrk -t4 -c$c -d20s --latency https://你的站点/ 2>&1 | grep -E "Latency|Requests/sec|Socket errors|Non-2xx"
sleep 5
done
拐点出现时,看服务器当时的状态,就能知道瓶颈在哪:
# 另开一个终端,压测时实时观察
top -b -n1 | head -15
vmstat 1 10
瓶颈定位对照表:
| 现象 | 瓶颈 | 解法 |
|---|---|---|
| CPU 使用率接近 100%,load 很高 | CPU 不够 | 加 CPU 核数;检查慢 SQL 和正则回溯 |
CPU 不高但 Threads_running > 核数 |
数据库排队 | 加索引、改查询、读写分离 |
CPU 不高,worker 全忙,max children reached > 0 |
内存不够,PHP 进程数到顶 | 加内存,或减少单 worker 内存 |
| CPU 不高,io wait 高(vmstat 的 wa 列) | 磁盘 IO | 换 SSD、加缓存、把 swap 调小 |
| 错误全是 502 | PHP-FPM 崩溃或超时 | 看 PHP 错误日志,多半是 OOM |
# 内存不够的判定:看有没有 OOM 记录
dmesg -T | grep -i "out of memory" | tail -20
grep -i "allowed memory size exhausted" /var/log/php*-fpm.log | tail -20
压测时必须注意的两件事:一是压测机和网络不能成为瓶颈,最好压测机离服务器近或者同机房;二是压测前先关掉 Opcache 预热之外的干扰(比如 cron 任务),否则测出来的曲线会被后台任务带偏。
压测脚本要怎么写才接近真实流量。 用 wrk 的 -s 参数可以指定 Lua 脚本,让请求带上 Cookie、走不同的路径、按不同比例混合。这是让压测结果可信的关键:
-- mixed.lua:模拟真实流量构成,60% 首页、30% 文章页、10% 搜索
local paths = {"/", "/", "/", "/", "/", "/", "/news/1001", "/news/1002", "/news/1003", "/search?q=vps"}
request = function()
local path = paths[math.random(#paths)]
return wrk.format("GET", path, {["Accept-Encoding"] = "gzip"})
end
# 用这个脚本压,得到的 QPS 才有参考价值
wrk -t4 -c100 -d60s --latency -s mixed.lua https://你的站点/
几个常见的压测错误,会让结果完全失真:
错误一:压测时 PHP 的 Opcache 是冷的。 第一次运行时代码要现场编译,比生产环境慢得多。压测前先跑一遍 warmup:
# warmup:先跑 30 秒低并发,把 Opcache、Redis 连接池都热起来
wrk -t2 -c10 -d30s https://你的站点/ > /dev/null
# 然后再正式压
wrk -t4 -c100 -d60s --latency https://你的站点/
错误二:压测机本身是瓶颈。 wrk -t4 只有 4 个线程,在千兆内网里能压出几十万 QPS,但如果压测机和服务器之间跨公网,瓶颈就在压测机。判断方法是在服务器上直接压一次对比:如果本机压出来的 QPS 明显高于远程压,瓶颈在链路。
错误三:压测期间数据库在自动备份。 很多 VPS 商家或面板会在整点自动做数据库备份,那个瞬间的延迟会把 99 分位拉高好几倍。压测避开整点,或者临时关掉备份任务。
错误四:只看 QPS 不看错误。 wrk 会统计 Non-2xx 和 Socket errors。这两个数字必须一起看——很多机器在 QPS 冲高时已经在返回 502 了,只是这个数字看起来很漂亮。
六、峰值与突发:真正的杀手是尖峰不是均值

大部分人在压测时用平稳流量,然后发现线上还是崩——因为真实业务的流量不是平的。并发能力要按峰值算,而且要按「峰值持续多久」算。
真实场景里有三种典型形态:
形态一:阶梯型。 比如整点定时的对账任务、每天早上的定时推送。这种峰是可预测的,可以提前扩容或把任务错峰。
形态二:脉冲型。 持续时间只有几秒,但并发瞬间是平时的好几倍——秒杀、活动开抢、群发邮件的瞬间回调。这类峰值的杀伤力在于所有 PHP worker 同时被唤醒,数据库同时收到上百条查询。
形态三:持续爬坡型。 流量缓慢上升但从不下降,通常伴随内存泄漏或数据表膨胀,最终机器在某个平常的时刻被压垮。
针对脉冲型的三种缓解手段:
手段一:排队削峰。 在入口加一层限流,把瞬时并发压成队列:
# 限制同一 IP 的并发连接数(对付单机刷请求最有效)
limit_conn_zone $binary_remote_addr zone=perip:10m;
server {
limit_conn perip 20;
limit_req_zone $binary_remote_addr zone=reqzone:10m rate=30r/s;
limit_req zone=reqzone burst=50 nodelay;
}
limit_req 的 rate=30r/s 意思是每个 IP 每秒最多 30 个请求,超出的直接返回 503。这对登录接口、搜索接口这种容易被脚本打的接口特别有效。
手段二:让耗资源的操作异步化。 发邮件、生成 PDF、调用第三方 API,这些都不该在 HTTP 请求里同步做完。扔进队列,让 worker 慢慢处理。
# Laravel 队列 worker 数:按内存算,不能超过 PHP worker 的一半
php artisan queue:work --queue=default,emails --max-jobs=500 --max-time=3600 --sleep=3
手段三:用数据库连接池限流。 在应用侧限制同时活跃的数据库连接数,让排队的请求在应用层等待,而不是在数据库层堆积。
// 用 Redis 做信号量:限制同时执行的 DB 密集型任务数
function withDbSemaphore(callable $fn, int $limit = 20): mixed {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = 'db:slots';
$redis->set($key, $limit);
// 取令牌,失败则等待
while (!$redis->decr($key) > 0) {
usleep(20000);
$redis->incr($key);
}
try { return $fn(); }
finally { $redis->incr($key); }
}
对持续爬坡型的排查手段:内存泄漏看 pm.max_requests 是否设置、看 PHP-FPM 进程 RSS 是否随时间单调上升;数据表膨胀看慢查询里的 rows_examined 是否在涨。
# 观察 PHP 进程内存是否单调上升(隔 10 分钟看两次)
ps -o pid,rss,etime,cmd -C php-fpm | head -20
七、什么情况下不该用单台韩国VPS扛并发

这一节给出四类明确不该单台硬扛的场景,依据是并发模型的物理限制和机房位置,与具体商家的服务水平无关。
场景一:峰值并发要求超过 300,且业务不能容忍排队。 单台机器的 PHP 进程数被物理内存卡死,2 核 4GB 的机器满打满算也就 47 个 worker。替代方案:横向扩容——用负载均衡把流量分到 2 到 4 台同配置机器上,每台只承担四分之一到六分之一的并发。 这种架构下每台机器可以配更小的 max_children,数据库用主从读写分离,Redis 做会话共享。
场景二:你的并发主要来自长连接(WebSocket、实时推送、在线客服)。 长连接的特点是每个连接几乎不占 CPU 但常驻内存,一个 PHP 或 Node worker 处理一条 WebSocket 通常要留 20KB 到 100KB 内存,几千条连接就能吃掉几百 MB。这类场景的正解是专门的实时服务,用 Nginx 反代到 Node.js 或 Go 写的服务上,并按连接数单独规划内存,而不是套用 PHP 的并发公式。
场景三:业务需要长时间跑的任务(视频转码、大数据批处理、持续爬取)。 这些任务会长期占用 CPU 和磁盘 IO,和前台请求抢同一份资源,结果是前台变慢而不是后台变快。替代方案:把批处理放到独立的机器或独立的 CPU 配额上跑,前台机器只服务前台请求;更省的做法是直接买按小时计费的临时机器,用完销毁。
场景四:并发主要来自中国大陆用户,且是普通网站访问。 韩国节点到中国大陆的链路在晚高峰有物理层面的拥塞风险,你把 PHP 进程数从 15 提到 47 也没有用,因为请求卡在网络上而不是卡在服务器上。替代方案:静态资源全部上 CDN 并在边缘缓存,动态请求就近部署,或者干脆做两地部署分流。
# 判断你的并发瓶颈在服务器还是在网络:对比本地压测和远程压测
# 本地(服务器上自己压)
wrk -t2 -c50 -d20s --latency http://127.0.0.1/
# 远程(从客户端压,经过真实网络)
wrk -t2 -c50 -d20s --latency https://你的站点/
两个数字一比就清楚了:如果本地 50 并发的 99 分位是 60ms,远程是 1200ms,那瓶颈在网络链路,加服务器配置解决不了。
八、常见误区:这几个数字最容易把人带偏

误区一:把 worker_connections 当成并发数。 worker_connections 8192 是 Nginx 能同时持有的连接上限,包括 keepalive 的空闲连接,和你能服务的用户数之间隔着 PHP 进程数、数据库连接数、CPU 核数好几层。把它调大只是让排队更隐蔽,不会让处理能力变强。
误区二:用「同时在线人数」估算并发。 在线用户大部分时间在看页面,没有请求在处理。真实的并发请求数通常是在线人数的 5% 到 15%。估算并发时用「同时在线数 × 0.1」起步,再用压测校准。
误区三:只看峰值 QPS,不看单请求耗时。 QPS 高不代表健康——如果每个请求要 2 秒,那 5 QPS 的机器在 5 个并发下就已经饱和了。真正该盯的是「延迟 × QPS」这个乘积,它才等于实际负载。
误区四:以为加 CPU 能提升并发。 前面算过了,PHP-FPM 的进程数被内存卡死。加 CPU 核数只在单个请求很吃 CPU(比如带复杂正则、大量 JSON 序列化)时才有明显效果。如果你的瓶颈是排队,加 CPU 一点用没有。
误区五:把 max_connections 设得越大越安心。 数据库连接数设到 1000,然后每个连接吃 64MB,一旦被恶意连接或应用连接泄漏打满,机器会在几秒内 OOM。合理的做法是设一个够用但不夸张的上限(2 到 4GB 内存设 100),同时在应用层用连接池。
误区六:压测只用低并发。 用 10 并发压出来的 5000 QPS 毫无意义,因为你的机器根本没用满。压测必须一直加到你扛不住,那个拐点才是你的容量。
误区七:不做容量规划,等出事再升配。 升配要重启、要迁移数据、可能有几分钟到几十分钟的停机。提前把监控和告警建好:
# 简单的并发监控脚本,每分钟采一次样
cat > /usr/local/bin/conc_monitor.sh << 'EOF'
#!/bin/bash
php_fpm_active=$(curl -s http://127.0.0.1/fpm-status | grep -oP 'active processes: \K\d+')
php_fpm_max=$(curl -s http://127.0.0.1/fpm-status | grep -oP 'max children reached: \K\d+')
db_running=$(mysql -N -e "SHOW STATUS LIKE 'Threads_running'" 2>/dev/null | awk '{print $2}')
load=$(cat /proc/loadavg | awk '{print $1}')
echo "$(date '+%F %T') active=$php_fpm_active reached=$php_fpm_max db_running=$db_running load=$load"
EOF
chmod +x /usr/local/bin/conc_monitor.sh
九、怎么验收:并发合格的标准是什么

上一节给了监控手段,这一节给验收线。没有验收标准的容量规划就是猜。 下面是这套文章建议的一组硬指标:
| 指标 | 合格 | 优秀 | 说明 |
|---|---|---|---|
| TTFB(动态页) | < 300ms | < 100ms | curl -w "%{time_starttransfer}" |
| 99 分位延迟(压测中) | < 1000ms | < 300ms | wrk --latency 输出 |
| 错误率 | < 0.1% | 0 | 压测中统计 5xx |
| PHP worker 峰值占用 | < max_children 的 80% | < 50% | 状态页 active processes |
| max children reached 计数 | 0 | 0 | 一旦 > 0 就是排队过 |
| 数据库 Threads_running | ≤ CPU 核数 | ≤ 核数的一半 | 超过就是数据库排队 |
| 内存谷值(低峰) | < 15% | < 8% | free -m 看 available |
| CPU 使用率(压测中) | < 70% | < 50% | 留 30% 给突发 |
这里最关键的一条是「max children reached = 0」。这一个数字直接告诉你有没有发生过排队。如果它是 0,说明你的 PHP 进程数配置是够用的,瓶颈不在内存;如果它大于 0 且持续增长,那再高的并发压测结果也没有意义——那只是测出了排队队列的长度,不是你的处理能力。
验收的正确流程:
# 第一步:记录基线(压测前的空载状态)
free -m; vmstat 1 5; mysql -e "SHOW STATUS LIKE 'Threads_connected'"
# 第二步:阶梯压测到拐点
for c in 25 50 100 200 400; do
wrk -t4 -c$c -d20s --latency https://你的站点/ | grep -E "Latency|Requests/sec"
sleep 5
done
# 第三步:压测中采样服务器状态(在另一个终端)
vmstat 1 60 > /tmp/vmstat_$(date +%s).log
# 第四步:压测后检查是否有排队和错误
curl -s http://127.0.0.1/fpm-status | grep -E "max children reached|active processes"
记录结果用的表格模板。 每升一档并发就填一行,填到出现红线为止:
| 并发数 | QPS | 平均延迟 | 99分位延迟 | 错误率 | CPU% | 内存可用 | DB Threads_running | max children reached | |
|---|---|---|---|---|---|---|---|---|---|
| 25 | |||||||||
| 50 | |||||||||
| 100 | |||||||||
| 200 | |||||||||
| 400 |
填完之后,你的生产并发上限 = 出现「99 分位超过 1000ms」或「错误率超过 0.1%」那一档的 70%。留 30% 余量是因为生产环境的流量永远比压测更复杂——有爬虫、有缓存失效、有数据库自动维护,留够余量才不会在出问题时才发现。
最后一步是把验收线变成日常监控。 压测是一次性的,验收标准应该是持续生效的。把这几个告警接上:
# 1) 数据库连接告警:超过 CPU 核数 2 倍就该处理
mysql -N -e "SHOW STATUS LIKE 'Threads_running'" | awk '{if ($2 > 8) print "DB overloaded: " $2}'
# 2) 内存告警:available 低于 15%
free -m | awk '/Mem:/ {if ($7 < $2*0.15) print "MEM LOW: available="$7"MB total="$2"MB"}'
# 3) 磁盘告警:超过 80%
df -h / | awk 'NR==2 {if ($5+0 > 80) print "DISK HIGH: " $5}'
# 4) 5xx 错误率突增:从 Nginx 日志统计
sudo awk '$9 ~ /^5/ {c++} END {print "5xx count: " c}' /var/log/nginx/access.log
把这四条写进一个脚本,每 5 分钟跑一次并推送到你的告警渠道。 这样你不需要等用户投诉才发现容量到顶——并发问题的特点是「一旦崩就是全面崩」,因为排队会让所有请求一起超时,监控告警比事后救火便宜得多。
按你的情况,下一步该看哪家
上面讲的是判断标准,但多数人最后还是想问一句「那我该买谁」。按你的实际情况对照下面几条,直接去看对应的机型页比继续比价更快:
- 只想搭个站、预算优先:先看价格最低那档够不够用,别为用不上的配置付钱。看 justg 当前机型与价格
- 主要访客在国内、在意打开速度:先确认回程线路,再谈配置。看 vpckr 当前机型与价格
- 跑游戏服或实时业务:看上行抖动与防御能力,普通套餐往往不达标。看 zji 当前机型与价格
- 只是开发测试、临时用:按小时或月付,用完就删,别买年付。看 恒创科技 当前机型与价格
价格与库存以商家页面当日显示为准,下单前建议先确认退款窗口与是否支持按月付费。
十、常见问题

2 核 2GB 的韩国VPS大概能扛多少并发? 用这套公式估:2GB 内存可用 1433MB,按每个 PHP worker 60MB 算,max_children 约 23 个;数据库 max_connections 设 100 但活跃连接控制在 20 以内;CPU 2 核意味着同时只有 2 个请求在真正跑 CPU 密集部分。如果你的页面是缓存命中率高的静态内容,实测能稳定服务 100 到 300 个同时在线用户;如果每个请求都要查 3 到 5 条数据库,那并发请求数会掉到 20 到 40。 具体数字必须压测校准。
并发数和 PV 有什么区别? PV 是累计的页面浏览总量,是一段时间的统计值;并发是同一时刻正在处理的请求数,是瞬时值。一个 10000 PV/天的站,可能峰值只有 5 个并发,也可能集中在 10 分钟内产生 200 个并发。容量规划要用峰值并发,用 PV 算不出并发。
压测的时候把自己打挂了算谁的? 算你的——生产环境千万别直接压。第一,压测前确认有快照或备份;第二,压测要限制速率,不要一上来就 400 并发;第三,最好先在同配置的测试机上跑。最稳妥的做法是先在低并发下线性加压,找到「响应时间开始变缓」的拐点,在拐点以下留 30% 余量作为生产容量。
pm = ondemand 和 dynamic 选哪个? 看你的流量形态。流量平稳、要求低延迟,选 dynamic,它常驻一定数量的 worker,响应最快。流量波峰波谷明显、白天忙晚上闲,选 ondemand,它按需启停 worker 省内存,代价是每次冷启动多几十毫秒。访问量小但偶尔爆量的站,ondemand 加 pm.max_spare_servers 是很实用的组合。
数据库连接数比 PHP worker 多会有问题吗? 不会,这两个数本来就不该相等。max_children 限制的是能同时执行的 PHP 进程数,数据库连接数应该大于它(因为连接会因空闲而保留),但远小于 max_connections。判断标准是看 Threads_running:它持续大于 CPU 核数才说明数据库过载,单纯连接数高但 Threads_running 是 0,说明只是有空闲连接而已。
长连接(WebSocket)能套用这套并发公式吗? 不能,要单独算。长连接的瓶颈不是 CPU 而是内存和文件描述符:每条 WebSocket 连接通常占 20KB 到 100KB 内存,并且要一直占用一个 fd。2GB 内存的机器,安全的长连接上限大概在 3000 到 5000 条,且必须用 Nginx 反代并配好 proxy_read_timeout。这类场景应该用 Node.js 或 Go 的专门服务,而不是 PHP-FPM。
怎么知道自己的业务到底需要多少并发? 看历史数据的峰值,而不是估。用 Nginx 日志按分钟聚合请求数:
sudo awk '{split($4,t,":"); print t[1]" "t[2]":"t[3]}' /var/log/nginx/access.log | \
sort | uniq -c | sort -rn | head -20
找到峰值那一分钟,再乘以「这一分钟里同时在处理请求的比例」,就是你的历史峰值并发。新业务没有历史数据,就从「当前 PV × 0.1」起步,然后留足余量。

喜欢
高兴
鬼脸
呵呵
无聊
伤心