大带宽服务器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扩展:Swoole 或 Workerman。以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 65535 和 pm.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协程池,层层递进给出了全链路调优方案。别再去翻网上的“性能优化十大条”了,动手改配置跑压测才是真本事。