如何解决 PHP-FPM 与 Redis 连接数过多导致端口耗尽问题?
核心结论:在 php-fpm+phpredis 架构下,并发较大时处于 TIME-WAIT 状态的 TCP 连接过多会导致客户端无法分配新端口,报错"Cannot assign requested address",官方测试数据显示启用连接池后 Redis 操作吞吐量可提升 3-10 倍。
原因分析
PHP-FPM 与 Redis 连接数过多导致端口耗尽的根本原因是短连接模式下每次请求都建立新的 TCP 连接。在并发较大的情况下,处于 TIME-WAIT 状态的 TCP 连接较多,客户端无法分配出新的端口。典型报错信息包括:
- "Cannot assign requested address"——客户端端口耗尽
- "ERR max number of clients reached"——Redis 端连接数达到 maxclients 限制(默认 10000)
- "Redis server went away"——连接静默失效但 isConnected() 仍返回 true
根据阿里云开发者社区 2021 年 1 月 27 日的资料,出现"Cannot assign requested address"错误的应用程序使用的架构基本都是 php-fpm+phpredis。每次请求建立 TCP 连接需要 3 次握手和认证过程,在高并发场景下会额外增加 20-100ms 连接开销。
解决方案一:使用 pconnect 替换 connect
最推荐的方案是用长连接替代短连接,减少 TCP 连接数量。修改前代码:
$redis->connect('inst-name.redis.rds.aliyuncs.com',6379);$redis->auth('inst-password');修改为使用 persistent connection(phpredis >= 5.3.0 推荐方式):
$redis->pconnect('inst-name.redis.rds.aliyuncs.com',6379,0,NULL,0,0, ['auth'=> ['inst-password']]);关键是要显式传入稳定的 persistent_id 参数。根据 2026 年 4 月 23 日的技术资料,在 PHP 7.4 下 pconnect() 未指定稳定 persistent_id 时,PHP-FPM 各 worker 进程独立管理无名连接池,导致并发下重复建连。正确做法:
$redis->pconnect('127.0.0.1', 6379, 2.5, 'user_cache'); // ✅ 合法,命名清晰错误做法:
$redis->pconnect('127.0.0.1', 6379, 2.5, uniqid()); // ❌ 每次都新建池,比不用还糟解决方案二:修改客户端内核参数
对于业务代码牵涉过多组件不易变更的场景,可以修改内核参数 tcp_max_tw_buckets。查看当前参数:
sysctl net.ipv4.tcp_max_tw_buckets net.ipv4.ip_local_port_range
典型输出:
net.ipv4.tcp_max_tw_buckets = 262144 net.ipv4.ip_local_port_range = 32768 61000
修改 tcp_max_tw_buckets,保证比 ip_local_port_range 小:
sysctl -w net.ipv4.tcp_max_tw_buckets=10000
注意:请忽略所有修改 tcp_tw_reuse、tcp_tw_recycle 的方法,这些方法对于使用了 nat/lvs 的服务均不适用(tcp_tw_recycle 在 Linux 4.12 上已经被弃用)。
解决方案三:启用 PhpRedis 连接池
根据 2025 年 11 月 8 日收录的优化实践,PhpRedis 连接池基于持久连接和连接复用机制。配置 php.ini:
extension=redis.so redis.pconnect.pooling_enabled=1 redis.pconnect.connection_limit=50
生产环境推荐配置:
redis.pconnect.pooling_enabled=1 redis.pconnect.connection_limit=30 redis.pconnect.timeout=2.5 redis.pconnect.check_alive=1
连接池大小计算公式:连接池大小 = PHP-FPM Worker 数 × 平均 Redis 操作数/请求。启用连接池后,PHP 应用的 Redis 操作吞吐量可提升 3-10 倍,服务响应速度提升 50%。
解决方案四:连接健康检查
即使设置了超时,Redis 服务重启、网络抖动仍会导致连接"静默失效"。根据 2026 年 1 月 1 日的资料,每次关键操作前必须组合判断:
function safeGet($redis, $key) {
if (!$redis->isConnected() || $redis->ping() !== '+PONG') {
$redis->connect('127.0.0.1', 6379, 2.5, '', 0, 1.0);
}
return $redis->get($key);
}避免在循环里反复 ping,可加简单缓存(如 30 秒内跳过 ping),防止引入额外延迟。
注意事项
- ThinkPHP 的 think-redis 驱动默认不透传 connectTimeout 和 readTimeout,必须在 config/redis.php 中显式写死:'options' => ['timeout' => 2.5, 'read_timeout' => 1.0](2026 年 4 月 21 日)
- TP6 底层'persistent' => true 配置项仅对原生 Redis 扩展的 pconnect() 生效,对 Predis 完全无效(2026 年 4 月 23 日)
- Swoole/Workerman 等常驻框架中 pconnect() 容易引发连接泄漏,应改用协程连接池(如 Swoole\Coroutine\Redis)
- Redis 服务端需开启 tcp-keepalive 60,否则 SLB 或 NAT 网关可能静默丢弃空闲连接
- PHP-FPM 中限制每个 worker 的最大请求数(pm.max_requests),强制进程重启以清理残留连接。有案例将 pm.max_children 从 50 调小到 25,内存使用率从 92% 降至正常水平(2025 年 9 月 11 日)
参考来源
来源:阿里云开发者社区 - 解决 Redis 短连接导致端口耗尽报 Cannot assign requested address 错(2021 年 1 月 27 日)
来源:GitHub phpredis 官方文档 - pconnect/popen 参数说明(phpredis >= 5.3.0)
来源:开发者技术社区 - 解决高并发瓶颈:PhpRedis 连接池 3 步优化实践(2025 年 11 月 8 日收录)
来源:技术博客 - 为什么 PHP 7.4 下使用 Redis 扩展导致长连接过多(2026 年 4 月 23 日)