转载请备注来源: 《Cannot assign requested address报错分析和四次挥手》 | shuwoom.com

1.问题

今天通过php-fpm记录的错误日志,发现了很多下面这种重复的错误信息:

PHP Fatal error:  Call to a member function query() on a non-object in /xxxxx/xxxx.php on line 80

根据错误提示信息,打开对应的行代码:

......
$query = $this->db->query($sql);
......
    这里是$this->db对象为null,也就是说连接没有成功,那么我在连接处try...catch捕获的异常应该有记录错误信息。
日志里也确实有很多下面这个错误记录,时间上也对的上:
exception: SQLSTATE[HY000] [2002] Cannot assign requested address

2.解决方法

问题确认了,那么就要找对应的解决方法了。
“Cannot assign requested address.” 报错是因为客户端频繁连接服务器,每次连接都在很短的时间内结束,导致出现大量的TIME_WAIT,最终导致linux分配的客户端连接端口用尽,无法建立socket连接导致的。
虽然socket可以正常关闭,但是端口并不是马上就释放,而是处于TIME_WAIT状态,默认等待60s后才释放。

解决方法:
sysctl -w net.ipv4.tcp_timestamps=1  开启对于TCP时间戳的支持,若该项设置为0,则下面一项设置不起作用
sysctl -w net.ipv4.tcp_tw_recycle=1  表示开启TCP连接中TIME-WAIT sockets的快速回收

那么,大家肯定会好奇,为什么会有TIME_WAIT这个状态呢?这里就需要讲解下TCP/IP协议的设计,特别是四次挥手关闭连接的原理。

3.TIME_WAIT状态原理(四次挥手、关闭连接)

tcp/ip四次挥手过程示意图

过程说明:
(1)首先,客户端主动要关闭连接,发送FIN包,然后进入FIN_WAIT_1状态,等待服务端返回ACK包。这时客户端不能再向服务端发送数据,但是可以读取数据。
(2)服务端收到FIN包后向客户端发送ACK包,然后进入CLOSE_WAIT状态。此时服务端不能再读取数据,但可以继续向客户端发送数据。
(3)客户端收到服务端发送的ACK包后就进入FIN_WAIT_2状态,等到接收服务端发送的FIN包。
(4)服务端发完成发送数据后,继续发送FIN包给客户端,然后进入LAST_ACK状态,等待客户端返回ACK包。此时,服务端既不能发送数据,也不能读取数据。
(5)客户端收到FIN包后向服务端发送ACK包,然后进入TIME_WAIT状态,等待足够长的时间(2MSL)以确保服务端能接收到ACK包,最后进入CLOSED状态,释放网络资源。
(6)服务端收到客户端发送的ACK包后进入CLOSED状态,释放网络资源。

主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。MSL在RFC 1122上建议是2分钟,而源自berkeley的TCP实现传统上使用30秒,因而,TIME_WAIT状态一般维持在1-4分钟。
而在这TIME_WAIT状态期间,该socket所占用的本地端口号将一直无法释放,因此在高并发高负载下运行一段时间后,就有可能出现本文所说的错误导致无法创建新的socket连接。

我们看上图的流程,客户端在发送最后一个ACK包以后就跟服务端没有通信,为什么这里还要进入TIME_WAIT状态呢?

4.TIME_WAIT状态存在的理由

(1)可靠地实现TCP全双工连接的终止

确保最后的ACK能让被关闭方接收。 假如主动关闭的一方的发出ACK包,如果这个ACK包丢失,那么服务端将会重新发送FIN包,这时客户端就需要去维护TIME_WAIT状态允许它重发ACK包。如果这时客户端不维持TIME_WAIT状态直接进入CLOSED状态,那么客户端将响应RST分节,服务端收到后会将此分节解释成一个错误(connect reset by peer,即对端已经关闭)。

(2)允许老的重复分节在网络中消逝

TCP是可靠的服务,当数据包丢失会重传,当有数据包迷路的情况下,如果不等待2MSL时,当客户端以同样地方式重新和服务建立连接后,上一次迷路的数据包这时可能会到达服务,这时会造成旧包被重新读取

打赏

发表评论

电子邮件地址不会被公开。