前言最近上线的一台服务器的nginx在连接上游的时候总会报一些请求(不是所有请求)到upstreamtimedout(110:Connectiontimedout)的错误,貌似说是后端的phpcgi进程有问题,但是如果phpcgi进程有问题,应该不是所有的请求都报错,所以开始排查。排查原因在我们的服务器上,PHP使用9006端口进行监控。执行netstat-an|grep9006命令查看相关连接的网络状态,可以看到部分连接处于CLOSE_WAIT状态:tcp0010.0.0.188:900610.0.0.52:37316CLOSE_WAITtcp0010.0.0.188:900610.0.0.52:37292CLOSE_WAITtcp0010.0.0.188:900610.0.0.52:37300CLOSE_WAITtcp0010.0.0.188:900610.0.0.52:37302CLOSE_WAITtcp0010.0.0.188:900610.0.0.52:37234CLOSE_WAIT奇怪的是,这些连接一直卡在CLOSE_WAIT的状态,不会改变,所以这些连接应该会导致PHP进程卡住,无法处理新的请求,导致一些报告连接超时的请求是错误的。根据TCP连接的四次挥手过程,CLOSE_WAIT状态只有在连接关闭时才会出现。Nginx调用PHP,PHP响应太慢,导致nginx超时,然后nginx主动关闭与PHP的TCP连接,所以PHP进程是被关闭的,这没有问题。但是PHP进程收到nginx的shutdown请求后,也应该关闭,但是没有。而是一直停留在CLOSE_WAIT状态,说明PHP被什么东西卡住了,无法关闭连接。所以使用strace-pPHP进程ID来查看PHP卡在哪里。等待一两分钟后,strace命令没有任何输出,说明PHP进程卡在了某个系统调用:然后使用lsof-npPHP进程ID命令查看进程打开了哪些文件资源,以及发现有一个状态为ESTABLISHED的mysql连接:猜测是不是mysql导致PHP卡顿,于是根据显示的连接端口号10465,在mysql服务器上查看该端口的连接情况,但是发现有没有到此端口的TCP连接。太神奇了,这里我只能猜测:PHP成功与mysql服务器建立TCP连接,但是由于丢包或者防火墙拦截等奇怪原因,PHP没有收到mysql的问候包,所以PHP一直在这里等待空的。后来mysql服务器主动断开了TCP连接,但是发送的FIN包也被拦截,导致收不到PHP的ACK响应,于是mysql继续释放连接。所以在PHP端看起来连接是ESTABLISHED状态,但是在mysql端连接不存在。确认与模拟测试PHP中为了确认mysql连接是否卡住,使用gdb-pPHP进程ID进行调试。上述lsof命令显示mysql连接对应的文件描述符为5u,在gdb中输入命令callclose(5)强制关闭连接,关闭后PHP进程的CLOSE_WAIT状态立即消失,证明了连接确实卡在了PHP中。然后我尝试模拟“成功建立TCP连接,但收不到mysql问候包”的情况,看PHP会不会卡死。测试代码如下:$mysql=newmysqli();$mysql->real_connect('45.113.192.102','root','xxx','xxx',80);45.113.192.102是百度的web服务器,肯定可以和80端口建立TCP连接,但不是mysql服务器,所以建立连接后肯定不会向PHP发送问候包,符合我要测试的场景。执行这段代码后,PHP确实会卡在real_connect,不会超时,和网上的情况一模一样。解决方案如何为这种情况设置超时时间?这里我走了弯路,尝试使用set_time_limit、default_socket_timeout、MYSQLI_OPT_CONNECT_TIMEOUT等参数来设置超时时间,但是都没有生效,让我很苦恼。后来才知道这个场景的超时是读写超时,不是连接超时,所以用MYSQLI_OPT_CONNECT_TIMEOUT设置是无效的。此参数控制连接超时。而set_time_limit只控制PHP脚本本身的执行时间,不包括系统调用和数据库操作消耗的时间,所以不会生效。这个在PHP文档中也有解释:下面是正确答案,有两种方法:1)修改mysqlnd.net_read_timeout配置项,在php.ini中增加一个mysqlnd.net_read_timeout配置项。例如mysqlnd.net_read_timeout=60表示mysqlnd.net_read_timeout配置项设置为60秒。mysqlnd.net_read_timeout配置项在PHP7.2(含)之后的版本可以通过ini_set函数在代码中设置,旧版本只能在php中设置。要求PHP版本大于等于7.2,代码示例:$mysql=newmysqli();$mysql->options(MYSQLI_OPT_CONNECT_TIMEOUT,5);//将连接超时设置为小于5秒$mysql->options(MYSQLI_OPT_READ_TIMEOUT,60);//设置读写超时为60秒$mysql->real_connect('45.113.192.102','root','xxx','xxx',80);这种方法的影响范围比第一种小,它只影响当前连接,而第一种是通过修改配置文件实现的,所以会影响所有其他使用同一个配置文件的PHP程序。设置读写超时后,重新执行测试代码,确实没有卡住,60秒后超时报错。总结对于mysql连接,一个完整的超时设置应该是连接超时和读写超时同时设置。如果只设置连接超时,那么在某些特殊情况下,PHP进程可能会卡住。
