VPS上PHP高并发翻车实录:从100并发打满到5000并发稳如老狗的调优手册

1. 默认PHP-FPM配置就是定时炸弹

刚入坑时,我在一台1核1G的VPS上直接用了php-fpm默认配置,上线业务刚200并发就炸了——CPU打满、内存溢出、502满天飞。新手最常犯的错误:不做压力测试直接就当生产用

罪魁祸首清单

  • pm = dynamic + 默认pm.max_children = 5(实际上是5个动态子进程,但动态模式会在突发流量下疯狂fork,直接拉爆内存)
  • pm.max_requests = 0 (永不回收进程,内存泄漏累积)
  • request_terminate_timeout = 0 (死循环不超时,进程挂死)

实战调优方案(1核1G VPS示例)

# /etc/php/8.1/fpm/pool.d/www.conf
pm = static
pm.max_children = 4
pm.max_requests = 500
request_terminate_timeout = 30s
pm.process_idle_timeout = 10s

为什么用static?对于VPS这种固定资源,static模式避免动态fork的抖动。4个进程刚好占满1G内存(每个PHP进程约200MB,含业务代码库)。加上OPcache后单进程内存降到80MB,可以升到8个进程。

踩坑提醒:pm.max_children = 总内存 / 每个进程平均内存,用ps aux | grep php-fpm观察实际RSS值,别拍脑袋设。

2. OPcache不开启等于自残

新手常以为“小VPS没必要开OPcache”,真相是不开OPcache,PHP每次请求都要解析编译所有文件(包括vendor),1核CPU瞬间打爆。开启后CPU降低70%以上。

必配参数(在php.ini中)

opcache.enable=1
opcache.memory_consumption=64
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=0
opcache.fast_shutdown=1

revalidate_freq=0 是关键——生产环境不检查文件修改时间,避免每次请求都stat。部署代码后手动sudo systemctl reload php8.1-fpm或调接口清空缓存即可。

验证效果

php -r "print_r(opcache_get_status(true));"  | grep hits

若hits远大于misses,说明工作正常。misses暴增则需检查opcache.max_wasted_percentage

3. 数据库连接不复用,秒变雪崩

新手写PHP直连MySQL,每次请求new PDO,高并发下连接数迅速打满。VPS的MySQL默认max_connections=151,超过就报Too many connections

快速止损方案

  • 用持久连接:PDO::ATTR_PERSISTENT => true,但注意要手动检查连接有效性(mysql_ping),否则会出现“2006 MySQL server has gone away”。
  • 更推荐:配合Redis做查询缓存,扛住读多写少的场景。
class Db {
    private static $pdo = null;
    public static function getInstance() {
        if (self::$pdo === null) {
            self::$pdo = new PDO('mysql:host=127.0.0.1;dbname=xxx', 'user', 'pass', [
                PDO::ATTR_PERSISTENT => true,
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            ]);
        }
        // 检查连接是否有效
        try {
            self::$pdo->query('SELECT 1');
        } catch (PDOException $e) {
            self::$pdo = new PDO(...); // 重连
        }
        return self::$pdo;
    }
}

如果是轻云互联的VPS(比如香港BGP节点),自带MySQL 8.0优化版,建议启用performance_schema监控慢查询,及时加索引。

4. 慢查询堆死PHP-FPM池

高并发下,一个慢SQL耗时5秒,PHP-FPM进程就被占死,池子迅速耗尽。新手常忽略慢查询日志,直到502才查。

必开慢查询日志

# /etc/mysql/mysql.conf.d/mysqld.cnf
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.5
log_queries_not_using_indexes = 1

之后用pt-query-digest分析瓶颈:

pt-query-digest /var/log/mysql/slow.log | head -20

常见优化:无索引的LIKE %xxx%改成全文索引或搜索引擎;ORDER BY RAND()改用JOIN取随机结果。

5. 进程数不对:多核VPS的误区

很多教程说“CPU核心数 * 2”设置pm.max_children,但实测在VPS上会翻车。例如2核4G的VPS,设pm.max_children = 4(2*2),结果CPU不到50%内存却溢出了。

正确公式(面向VPS)

# 先测单进程内存
ps aux | grep php-fpm | awk '{print $6}' | head -1  # RSS in KB
# 假设平均180MB,4GB内存留给PHP 3GB(预留1GB给OS/MySQL)
max_children = 3GB * 1024 / 180 ≈ 17

考虑到内存碎片和突发,取12比较安全。同时配合pm.max_requests = 500防止内存泄漏累积。

6. 排错三板斧

当高并发系统突然变慢,新手别急着重启。按顺序检查:

  1. php-fpm slow log:设置request_slowlog_timeout = 5s,快速定位哪个API慢。
  2. netstat -anp | grep :80 看连接队列是否溢出(Recv-Q列)。
  3. ab -n 1000 -c 100 http://your-site/ 本地压测,观察Failed requestsRequests per second

真实案例

有一次线上502,查slow log发现一个file_get_contents超时等待上游API,直接阻塞了所有PHP进程。最终加上了stream_context_create设置超时参数:

$opts = [
    'http' => ['timeout' => 3, 'method' => 'GET'],
];
$context = stream_context_create($opts);
$result = file_get_contents('xxx', false, $context);

别怪VPS不行,很多问题都是代码里埋的雷。

7. 终极复盘:从100并发到5000并发的关键几步

  • 第一层:OPcache + 静态FPM进程 (杀CPU/内存)
  • 第二层:MySQL慢查询 + 连接池 (杀数据库)
  • 第三层:Redis缓存 + 异步队列 (降数据库压力)
  • 第四层:Nginx开启FastCGI缓存 (静态化扛流量)

如果你用的是轻云互联的VPS,他们默认预装了OPcache和MySQL参数调优,省去第一步的适配时间。但记住:没有任何一键配置能替代压测和日志分析