解决mysql产生的大量time_wait以及其他

最近由于网站在大量访问后偶尔遇到数据库连接失败的情况,为了解决这个问题,做了一些分析。然后逐步挖掘,获得了一些有意思的东西。记录一下。

网站访问量大,在某些时刻提示数据库连接失败。但从后端看数据库状态一切正常,当前连接数也远小于最大连接数。

在日志中发现以下提示:

SQLSTATE[HY000] [2002] Cannot assign requested address

使用netstat 查看,发现大量到mysql的连接处于 TIME_WAIT 状态。

netstat -anlpt | grep 3306 |grep TIME_WAIT

按道理说,php使用pdo连接数据库后很快就完成了操作,并释放了连接。为什么还会有这么多等待呢。

后面发现,虽然pdo执行完操作,然后php页面也结束后,资源应该也释放了。这体现在mysql中的当前实际连接上是对的,页面执行完成后,当前实际连接会减少。但是该端口监听并不会马上断开,和mysql的 wait_timeout参数有关。而默认的wait_timeout 是28800,8小时。太长了。根据实际需要将 wait_timeout 调整至100秒足够业务使用。

这是第一步,减少了TIME_WAIT 的回收时间。

另外,网上提到,如果确实产生大量的TIME_WAIT,可以修改系统参数,启用端口重用。

修改 /etc/sysctl.conf  
net.ipv4.ip_local_port_range = 10000     61000   
net.ipv4.tcp_tw_recycle=1
net.ipv4.tcp_tw_reuse = 1

第一行增大本地端口可用范围,后面两行启用端口重用。sysctl -p 使修改生效。`

这样改后,确实效果明显,TIME_WAIT一下子少了很多。

不过这里有一个巨坑。真的是巨坑。网上几乎很少提及这个问题。那就是在nat网络环境下,如果使用了这个设置,会导致nat网络下无法访问该服务器。
看了几十篇文章,只有一篇提到了这个。 https://www.cnblogs.com/billyxp/p/3683559.html

我也遇到了这样的问题,修改生效后。公司内网的同事由于在一个网段内,都无法访问网站了,但是外面其他网络没有任何问题,手机也能正常访问。

于是将以下参数改回,然后内网访问恢复正常。

net.ipv4.tcp_tw_recycle=0
net.ipv4.tcp_tw_reuse = 0

这似乎又回到了原点。还是会有大量的TIME_WAIT产生,只是说给了更多的可用端口范围,和更快的回收时间而已。

思来想去,还是只有从根源上解决问题。

观察php连接Mysql的代码发现,代码都是使用短连接来完成,用完后系统自动回收资源。由于访问量巨大,所以产生了很多连接。

修改连接代码使用长连接连接Mysql,本次使用完毕后,下次连接数据库时该连接可以重用,避免建立新的连接。修改后观察,随着原来临时连接达到timeout时限,time_wait的情况越来越少,后面几乎没有了。恢复正常。目前只有由php-fpm创建的长连接稳定保持在一定数据,当然这和配置的php-fpm的线程数有关系。

自此,该问题基本解决。最终,还是要把代码写好才行啊,不然再好的硬件配置都是枉然。

解决掉这个,然后顺手看了下redis,似乎也存在同样的问题。

按照同样的思路,将redis改成长连接,不过似乎没有什么效果。不知道是不是我的phpredis版本问题。由于切换这个有可能会影响系统稳定性,暂时未作进一步挖掘。目前redis的TIME_WAIT只有几百个,还属于可接受范围,后续再做处理。

在这个过程中,发现了一个新的东西。用unix socket连接redis比用ip地址连接更快。由于redis本来就放在本机,所以用unix socket也是没什么问题的。立即做了一个简单测试。

redis-normal.php
$start_time = microtime(1);
$redis = new Redis();
$redis->connect('127.0.0.1');
for($i=1;$i<10000;$i++){
     $list_len = $redis->llen("visit_list");
     $data = $redis->lrange("visit_list",0,$list_len);
}
$redis->close();
echo microtime(1) - $start_time ;
0.69s
redis-socket.php
$start_time = microtime(1);
$redis = new Redis();
$redis->connect('/var/myredis.sock');
for($i=1;$i<10000;$i++){
     $list_len = $redis->llen("visit_list");
     $data = $redis->lrange("visit_list",0,$list_len);
}
$redis->close();
echo microtime(1) - $start_time ;
0.38s

使用socket连接的速度是完胜使用ip连接的,速度几乎快了一倍。

如果把redis的connect放在循环内,进行一万次的连接和关闭。也是socket方式更快,也是几乎快了一倍。不过整体会消耗更多的时间。

所以,socket连接看来确实比ip连接优秀不少,这在本机redis环境下很实用。

另外,顺手看了些关于unix socket的资料,对一些细节更了解了一些。本次查资料还涉及到了使用tcpdump,以及tcp的三次握手,四次再见等等,发现抽丝剥茧的感觉很爽,不过要花很多时间去一步步深入。

以下是一些参考:

What's the difference between Unix socket and TCP/IP socket?

A UNIX socket is an inter-process communication mechanism that allows bidirectional data exchange between processes running on the same machine.

IP sockets (especially TCP/IP sockets) are a mechanism allowing communication between processes over the network. In some cases, you can use TCP/IP sockets to talk with processes running on the same computer (by using the loopback interface).

UNIX domain sockets know that they’re executing on the same system, so they can avoid some checks and operations (like routing); which makes them faster and lighter than IP sockets. So if you plan to communicate with processes on the same host, this is a better option than IP sockets.

Edit: As per Nils Toedtmann's comment: UNIX domain sockets are subject to file system permissions, while TCP sockets can be controlled only on the packet filter level.

Windows 10 has support for Unix sockets. There are some limitations, but it's available: blogs.msdn.microsoft.com/commandline/2017/12/19/…Tyson Jul 30 '18 at 2:45

Unix Socket Tutorial

https://www.tutorialspoint.com/unix_sockets/index.htm

简单的tcpdump教程

https://danielmiessler.com/study/tcpdump/#protocol

推荐阅读更多精彩内容