Loading

Redis连接报错套接字地址只允许使用一次

Redis连接报错套接字地址只允许使用一次

报错信息:

[2025-08-17 07:57:38] custom.ERROR: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379] {"exception":"[object] (Predis\Connection\ConnectionException(code: 10048): 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379] at D:\path\to\project\server\vendor\predis\predis\src\Connection\AbstractConnection.php:160)"}

从错误信息看,应该是四元组信息(客户端的IP、客户端使用的端口、服务器的IP、服务器使用的端口)重复了,四元组信息中只有“客户端使用的端口”是动态的,

四元组信息:

客户端的IP:127.0.0.1

客户端使用的端口:创建连接时随机取一个未使用的动态端口

服务器的IP:127.0.0.1

服务器使用的端口:6379

通过 netstat -ano | findstr 6379 检查端口使用情况,有很多状态是 TIME_WAIT 的连接:

  TCP    127.0.0.1:50436        127.0.0.1:6379         TIME_WAIT       0

查了下 TIME_WAIT 状态的主要作用:Windows 上的连接断开之后,其本地端口(尤其是动态端口)处于 TIME_WAIT 状态,并且持续一段时间,

  • 确保最后一个 ACK 被可靠接收(防止迷失的 ACK 导致问题)
  • 允许网络中旧的、延迟的数据包“过期”(防止旧连接的数据包污染新连接)

TIME_WAIT 状态会导致端口的消耗增加,特别是在并发比较高的时候,会在短时间内快速占用大量的动态端口,Windows Server 默认的动态端口数是 16384:

D:\zhpj\Desktop>netsh int ipv4 show dynamicportrange tcp

协议 tcp 动态端口范围
---------------------------------
启动端口        : 49152
端口数          : 16384


D:\zhpj\Desktop>

考虑到可能是动态端口耗尽导致的,就需要再具体排查一下了,在 Redis 连接中,将四元组信息中的客户端信息打印出来,具体位置:vendor/predis/predis/src/Connection/StreamConnection.phpcreateStreamSocket 方法中,连接建立之后,获取一下客户端的连接信息:

protected function createStreamSocket(ParametersInterface $parameters, $address, $flags)
{
    $timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0);

    if (!$resource = @stream_socket_client($address, $errno, $errstr, $timeout, $flags)) {
        $this->onConnectionError(trim($errstr), $errno);
    }

    if (isset($parameters->read_write_timeout)) {
        $rwtimeout = (float) $parameters->read_write_timeout;
        $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1;
        $timeoutSeconds = floor($rwtimeout);
        $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000;
        stream_set_timeout($resource, $timeoutSeconds, $timeoutUSeconds);
    }

    if (isset($parameters->tcp_nodelay) && function_exists('socket_import_stream')) {
        $socket = socket_import_stream($resource);
        socket_set_option($socket, SOL_TCP, TCP_NODELAY, (int) $parameters->tcp_nodelay);
    }

    // 获取客户端的四元组信息
    $localAddress  = stream_socket_get_name($resource, false);
    \Log::info('localAddress = ' . $localAddress);

    return $resource;
}

编写一个压测的文件:

<?php

require __DIR__ . '/bootstrap/app.php';

use \Illuminate\Support\Facades\Cache;

$processId = getmypid();
$requestsPerProcess = 20; // 每个线程请求次数
for ($i = 0; $i < $requestsPerProcess; $i++) {
    $key = "test_key_{$processId}_{$i}";
    try {
        Cache::has($key);
    } catch (Exception $e) {
        echo "进程 {$processId} 出错: " . $e->getMessage() . "\n";
    }
    usleep(10000); // 10ms
}
echo "进程 {$processId} 完成\n";

先打印下当前 redis 连接的端口状态:

D:\zhpj\Desktop>netstat -anob | findstr 6379
  TCP    127.0.0.1:6379         0.0.0.0:0              LISTENING       6428

D:\zhpj\Desktop>

执行测试脚本:

D:\path\to\project\server> php redis_stress.php
进程 4896 完成

D:\path\to\project\server>

再次打印 redis 连接的端口状态:

D:\zhpj\Desktop>netstat -anob | findstr 6379
  TCP    127.0.0.1:6379         0.0.0.0:0              LISTENING       6428
  TCP    127.0.0.1:55700        127.0.0.1:6379         TIME_WAIT       0

D:\zhpj\Desktop>

日志文件中打印出来的客户端连接信息:

[2025-08-18 13:46:10] custom.INFO: localAddress = 127.0.0.1:55700

连接确实使用的是 55700,脚本执行完之后,端口的连接状态变成 TIME_WAIT。

PHP 中没有多线程,多进程也仅在 Linux 环境下可以支持。Windows 环境下使用批处理文件来模拟多个PHP进程:

@echo off
set THREAD_COUNT=10
set PHP_SCRIPT="redis_stress.php"
for /l %%i in (1,1,%THREAD_COUNT%) do (
   start /B php %PHP_SCRIPT%
)

执行批处理文件后,检查 redis 连接的端口状态:

D:\zhpj\Desktop>netstat -anob | findstr 6379
  TCP    127.0.0.1:6379         0.0.0.0:0              LISTENING       6428
  TCP    127.0.0.1:56205        127.0.0.1:6379         TIME_WAIT       0
  TCP    127.0.0.1:56206        127.0.0.1:6379         TIME_WAIT       0
  TCP    127.0.0.1:56207        127.0.0.1:6379         TIME_WAIT       0
  TCP    127.0.0.1:56208        127.0.0.1:6379         TIME_WAIT       0
  TCP    127.0.0.1:56209        127.0.0.1:6379         TIME_WAIT       0
  TCP    127.0.0.1:56210        127.0.0.1:6379         TIME_WAIT       0
  TCP    127.0.0.1:56211        127.0.0.1:6379         TIME_WAIT       0
  TCP    127.0.0.1:56212        127.0.0.1:6379         TIME_WAIT       0
  TCP    127.0.0.1:56213        127.0.0.1:6379         TIME_WAIT       0
  TCP    127.0.0.1:56221        127.0.0.1:6379         TIME_WAIT       0

D:\zhpj\Desktop>

[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56205  
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56206  
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56207  
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56208  
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56209  
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56210  
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56212  
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56213  
[2025-08-18 13:50:17] custom.INFO: localAddress = 127.0.0.1:56211  
[2025-08-18 13:50:18] custom.INFO: localAddress = 127.0.0.1:56221  

默认的动态端口数 16384 个,打印太多日志不太方便观察,考虑将动态端口数调整小一点:

D:\zhpj\Desktop>netsh int ipv4 set dynamicport tcp start=49152 num=100
参数错误。

D:\zhpj\Desktop>
D:\zhpj\Desktop>
D:\zhpj\Desktop>netsh int ipv4 set dynamicport tcp start=49152 num=255
确定。

D:\zhpj\Desktop>
D:\zhpj\Desktop>netsh int ipv4 show dynamicportrange tcp

协议 tcp 动态端口范围
---------------------------------
启动端口        : 49152
端口数          : 255


D:\zhpj\Desktop>

起初想将动态端口数改为 100,执行时报错了。改为设置成最小值 255 个。

根据微软文档:TCP/IP 的默认动态端口范围,动态端口范围的起始端口(start)必须在1025到65535之间,而端口数量(num)必须至少是255。此外,起始端口加上端口数量不能超过65535。

将批处理脚本中的 THREAD_COUNT 改为 500:

@echo off
set THREAD_COUNT=500
set PHP_SCRIPT="redis_stress.php"
for /l %%i in (1,1,%THREAD_COUNT%) do (
   start /B php %PHP_SCRIPT%
)

执行批处理脚本后,日志中复现了错误:通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]

脚本中的 echo "进程 {$processId} 出错: " . $e->getMessage() . "\n"; 改成了 Log::info("进程 {$processId} 出错: " . $e->getMessage());

echo 直接输出在终端里,批处理脚本执行之后直接退出了。

[2025-08-18 14:01:13] custom.INFO: localAddress = 127.0.0.1:49158  
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49194  
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49213  
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49214  
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49216  
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49217  
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49218  
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49219  
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49227  
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49274  
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: 进程 33864 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379]  
[2025-08-18 14:01:14] custom.INFO: localAddress = 127.0.0.1:49292  
[2025-08-18 14:01:14] custom.INFO: 进程 26708 出错: 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 [tcp://127.0.0.1:6379] 

将动态端口数恢复为默认值:

D:\zhpj\Desktop>netsh int ipv4 set dynamicport tcp start=49152 num=16384
确定。


D:\zhpj\Desktop>
D:\zhpj\Desktop>netsh int ipv4 show dynamicportrange tcp

协议 tcp 动态端口范围
---------------------------------
启动端口        : 49152
端口数          : 16384


D:\zhpj\Desktop>

恢复后再执行批处理脚本是正常的,日志文件中没有产生报错。

方案一:调整动态端口数:(cmd 管理员身份执行,不需要重启服务器)

# 常规应用服务器,使用默认的 16384
netsh int ipv4 set dynamicport tcp start=49152 num=16384 

# 高并发服务器,可以考虑设置到 30000+
netsh int ipv4 set dynamicport tcp start=35535 num=30000

# 极端高负载,最大 55535
netsh int ipv4 set dynamicport tcp start=10000 num=55535

start 默认值是 49152,num 的默认值是16384,这里的 start + num <= 65535

方案二:调整 TIME_WAIT 时间(通过修改注册表,将时间从默认 120 秒缩短到 30 秒),可能存在潜在风险,且必须重启服务器才能生效,最好是阶梯式调整:

# 先改为 60 秒
reg add HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters /v TcpTimedWaitDelay /t REG_DWORD /d 60 /f

# 观察 1 周后改为 30 秒
reg add HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters /v TcpTimedWaitDelay /t REG_DWORD /d 60 /f

posted @ 2025-08-18 15:44  zhpj  阅读(33)  评论(0)    收藏  举报