PHP ERROR_NO=7,TIME_WAIT占满可用端口

现象

业务收到一波急促的报警,第一眼看到的是大量curl第三方请求失败。响应ERROR_NO=7,追查一波发现异常日志大量集中在某一个业务节点,异常时节点CPU负载升高,从一些细节上看不像是网络链路或解析异常

手动curl -v ‘http://xxxx’ 能偶发复现得到下图的结果

排查

群里有个位司机提示此问题看上去比较像是端口耗尽,于是往端口线索追查

$ cat /proc/sys/net/ipv4/ip_local_port_range
32768	60999

看上去只开放了2万多个可用连接数(连接数的理解可以参见这位做的测试,比较简单明了 https://mozillazg.com/2019/05/linux-what-net.ipv4.ip_local_port_range-effect-or-mean.html)

The /proc/sys/net/ipv4/ip_local_port_range defines the local port range that is used by TCP and UDP traffic to choose the local port. You will see in the parameters of this file two numbers: The first number is the first local port allowed for TCP and UDP traffic on the server, the second is the last local port number. For high-usage systems you may change its default parameters to 32768-61000 -first-last.

使用 netstat -antp | grep TIME_WAIT | wc -l  发现待释放数量非常多,很有可能短时间耗尽可用连接数

(注意,这里只grep了TIME_WAIT,实际上ESTABLISHED状态数量也可能很多,实践时也要考虑进去)

使用 netstat -antp | grep TIME_WAIT |awk '{print $5}'|sort | uniq -c | sort -k1 -rn | head -n 20 看下哪些连接使用了大量待释放连接

tcpdump -A -i eth0 dst ***.11.76 查看第一条报文内容,很容易就发现是在调用哪个服务

解决

业务层面

改为长连接

定位到某些时段有部分高频接口调用,改为长连接,可以可以省下重复建连的时间

强制声明短连接

如果在请求头上未声明Connection:,则多数sever会默认为长连接,等待客户端主动socket_close(),即client先发F包

所以,如果是php-fpm这种无法实现跨请求维护curl长连接的,可以试着在curl头带上Connection:close 强制声明为短连接。多数server侧的实现都会在收到此参数时,响应完body后主动发起socket_close(),即先发F包

从而把time_wait问题留在server侧,因为对server来说,dst客户端的host和port都是不同的,在TCP四元组机制下,server侧不会出现端口消尽问题,又因为一般server系统的fd数量会开得非常大,可以是千万级别,所以基本不会出现问题(但还没测试过大量fd在time_wait时系统性能会不会下降太厉害)

声明ADDR可复用(待实践)

使用socket系列函数封装一份curl实现,关键之处在于加上这么一个参数

socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)

这种比较适合curl业务比较简单的场景,毕竟自己完善一套健壮的curl套件不是容易的事

未经验证的demo,仅参考

<?php
    $service_port = 9090;
    $address = '127.0.0.1';
    $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    if ($socket === false) {
        echo "socket_create() failed: reason: " . socket_strerror(socket_last_error()) . "\n";
    }

    $result = socket_connect($socket, $address, $service_port);
    if ($result === false) {
        echo "socket_connect() failed.\nReason: ($result) " . socket_strerror(socket_last_error($socket)) . "\n";
    }

    $data = 'data={"id":1}';
    $in = "POST /helloworld/say HTTP/1.1\r\n";
    $in .= "Host: localhost\r\n";
    $in .= "Content-Type: application/x-www-form-urlencoded\r\n";
    $in .= "Content-Length: ".strlen($data)."\r\n";
    //$in .= "Connection: Close\r\n";
    $in .= "\r\n";
    $in .= $data."\r\n\r\n";

    socket_write($socket, $in, strlen($in));

    $data = '';
    while ($out = socket_read($socket, 2048)) {
        $data.= $out;
    }

    socket_close($socket);

系统层面(待实践)

是由于linux分配的客户端连接端口用尽,无法建立socket连接所致,虽然socket正常关闭,但是端口不是立即释放,而是处于TIME_WAIT状态,默认等待60s后才释放。 可能解决方法

调低time_wait状态端口等待时间:

1. 调低端口释放后的等待时间,默认为60s,修改为15~30s sysctl -w net.ipv4.tcp_fin_timeout=30

2. 修改tcp/ip协议配置, 通过配置/proc/sys/net/ipv4/tcp_tw_resue, 默认为0,修改为1,释放TIME_WAIT端口给新连接使用 sysctl -w net.ipv4.tcp_timestamps=1

3. 修改tcp/ip协议配置,快速回收socket资源,默认为0,修改为1 sysctl -w net.ipv4.tcp_tw_recycle=1

增加可用端口:

$ vi /etc/sysctl.conf net.ipv4.ip_local_port_range = 10000     65000

发表评论

邮箱地址不会被公开。 必填项已用*标注