大带宽服务器PHP高并发调优:从FPM静态模型到内核断舍离

前言

很多人在大带宽服务器上跑PHP业务,结果带宽跑不满,CPU先撑爆了。别被“大带宽”三个字骗了,高并发下PHP的瓶颈往往不在网络,而在进程调度、内核参数和应用层IO模型。本文不讲套话,直接上手一个真实场景:轻云互联的1Gbps大带宽服务器(8核16G),压测5000并发API请求,最终稳定响应时间<50ms。关键步骤全盘托出。

1. 刨掉FPM的“温柔陷阱”

默认的 pm = dynamic 在大并发下会频繁创建销毁进程,加上 pm.max_spare_servers 的滞后性,直接导致请求排队。必须换静态模型,并计算出最优进程数。

先确定服务器能扛多少PHP-FPM进程(按单进程30MB RSS计算):

free -m
# 可用内存约15000MB
# 留2GB给系统+页缓存,剩余13000MB
# 进程数 = 13000 / 30 ≈ 433
# 但CPU只有8核,Linux调度会变慢,实测取 CPU核数 * 4 = 32 更稳

修改 www.conf

pm = static
pm.max_children = 32
pm.start_servers = 32
pm.min_spare_servers = 32
pm.max_spare_servers = 32
request_terminate_timeout = 30s
rlimit_files = 65535

别问我为什么不用dynamic——你去压测 ulimit -n 和进程上下文切换的数据,自然懂。

2. 内核参数断舍离——别让系统拖PHP后腿

大带宽服务器吞吐量大,默认的 net.core.somaxconn = 128 会直接让Nginx反向代理排队溢出一堆Connection refused。一步到位:

cat >> /etc/sysctl.conf << EOF
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0   # 注意,NAT环境开这个会断连
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_notsent_lowat = 16384
EOF
sysctl -p

关键参数 tcp_notsent_lowat:允许应用层更早写入数据,配合Nginx的sendfile低延迟。实测在轻云互联大带宽服务器上峰值吞吐提升约12%。

3. Nginx-FPM的连接池与惊群消除

默认Nginx用 accept_mutex on 防止惊群,但在高并发下反而成了锁竞争。使用SO_REUSEPORT直接绕过互斥锁:

events {
    use epoll;
    worker_connections 65535;
    multi_accept on;
    accept_mutex off;   # 配合reuseport
}
http {
    ...
    upstream phpfpm {
        server unix:/var/run/php7.4-fpm.sock;
        # 或者用TCP,避免UNIX socket受文件系统影响
        # server 127.0.0.1:9000;
        keepalive 128;                # 关键!启用长连接
        keepalive_requests 10000;
        keepalive_timeout 60s;
    }
    server {
        listen 80 reuseport backlog=65535;
        ...
        location ~ \.php$ {
            fastcgi_pass phpfpm;
            fastcgi_keep_conn on;     # 保持下游连接
            fastcgi_buffering off;    # 实时传输,配合大带宽
        }
    }
}

同时升级PHP-FPM的listen队列为 so_reuseport 模式(PHP 7.4+):

; /etc/php/7.4/fpm/pool.d/www.conf
listen = 127.0.0.1:9000
listen.backlog = 65535
listen.allowed_clients = 127.0.0.1
; 启用SO_REUSEPORT
; 注意:PHP-FPM本身不支持reuseport,需借助systemd socket激活
; 或者改用Swoole替代FPM

4. 代码层面的真·高并发——异步连接池

FPM的同步阻塞模型决定了每个进程同时只能处理一个请求。要突破必须上PHP扩展:SwooleWorkerman。以Swoole为例,构建一个协程化的API网关:

// server.php
use Swoole\Coroutine\Channel;
use Swoole\Database\PDOPool;
use Swoole\Database\RedisPool;

$config = [
    'pdo' => [
        'dsn' => 'mysql:host=127.0.0.1;dbname=test;charset=utf8mb4',
        'username' => 'root',
        'password' => '',
        'pool_size' => 64,  // 充分利用大带宽服务器的内存
    ],
    'redis' => [
        'host' => '127.0.0.1',
        'port' => 6379,
        'pool_size' => 64,
    ],
];

$http = new Swoole\Http\Server('0.0.0.0', 9501, SWOOLE_BASE);
$http->set([
    'worker_num' => 8,      // 等于CPU核数
    'enable_coroutine' => true,
    'max_coroutine' => 100000,
    'backlog' => 65535,
    'socket_buffer_size' => 8 * 1024 * 1024, // 8MB,适配大带宽
]);

$http->on('start', function () use ($config) {
    // 初始化连接池
    \Swoole\Runtime::enableCoroutine();
});

$http->on('request', function ($req, $resp) use ($config) {
    // 协程化DB查询
    go(function () use ($req, $resp, $config) {
        $pdoPool = new PDOPool($config['pdo']);
        $pdo = $pdoPool->get();
        $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([$req->get['id'] ?? 1]);
        $user = $stmt->fetch();

        $redisPool = new RedisPool($config['redis']);
        $redis = $redisPool->get();
        $redis->set('last_query', time());

        $resp->header('Content-Type', 'application/json');
        $resp->end(json_encode($user));
    });
});

$http->start();

配合supervisor启动,记得调大 ulimit -n 65535pm.max_requests 无关了,因为Swoole常驻内存不回收。

5. 压测与翻车排坑

使用 wrk -t8 -c1000 -d30s http://YOUR_IP:9501/ 压测,发现大量 Connection reset by peer。排查步骤:

  • 检查Swoole的 max_connection 设置:默认8192,改成 max_connection = 100000
  • ulimit -n:确认shell、systemd服务都要改,别只改了/etc/security/limits.conf忘了systemctl daemon-reexec
  • TCP的 tcp_tw_reuse:如果客户端是短连接,TIME_WAIT溢出,要保证已开启。但注意别开tcp_tw_recycle(NAT场景必坑)
  • 内存页大小:大带宽服务器网卡队列可能默认关闭,ethtool -l eth0 发现只有1个队列,开启多队列并绑定CPU(这步在轻云互联的大带宽机器上默认已优化,省去手动操作)

最终压测数据:wrk -t8 -c5000 -d60s,平均延时45ms,99分位98ms,CPU占用70%,带宽跑满980Mbps。

总结

大带宽服务器的价值在于同时处理大量连接而网络不成为瓶颈,但PHP传统FPM的同步模型反成短板。本文从FPM静态模型、内核参数、Nginx reuseport到Swoole协程池,层层递进给出了全链路调优方案。别再去翻网上的“性能优化十大条”了,动手改配置跑压测才是真本事。