简介:Redis客户端库连接Redis服务器超时
差不多一两年前,我在阿里云上遇到一个奇怪的Redis连接问题。每隔十分钟,服务中的Redis客户端库报告与Redis服务器的连接超时。当时费了九牛二虎之力才发现,阿里云会断开闲置了很久的TCP连接,而且没有给两个FIN或者RST包。当时我们的Redis服务器没有开放tcp_keepalive选项,所以Redis服务器端的连接仍然存在于Linux conntrack表中,而Redis客户端因为连接池重用连接get和set发现连接断开而关闭。因此,客户端对应的本地端口被恢复,当Redis重用这个本地端口来发起与Redis服务器的连接时,因为<>
解决这个问题很简单,打开Redis服务器的tcp_keepalive选项就可以了。但是,当时没想到,这个问题的深层次原因影响之大,后果之严重!
恶债:”选择1 ″触发JDBC 4 . communication s exception
最近,生产环境中的Java服务几乎每分钟都会报告如下错误:
由于之前有阿里云调查Redis连接异常中断的先例,所以怀疑是类似问题。比较客户端和服务器的conntrack表花了很多时间,但是没有介绍中描述的问题。然后,比较了多个MySQL服务器的sysctl设置,研究了iptables TRACE,研究了tcpdump捕获的消息。试验tw_reuse、tw_recyle等参数,调整阿里云负载均衡器后面挂载的MySQL服务器数量。然而,我们* *出乎意料地发现了一个* *新问题。当使用下面这个命令不经过阿里云SLB直接连接数据库时,有的数据库600s就能返回,有的挂了半个多小时,也回不来了。即使按ctrl-c也不能中断。
当时查了一个正常数据库和一个异常数据库,发现两个数据库的wait_timeout和interactive_timeout都是600s。我想了好的坏的,但我不明白这是怎么回事。然后偶然发现了另一个数据库的wait_timeout=60s,却突然明白了原来的“选择1 ”有什么问题?
我们的服务使用光JDBC连接池[1],其idleTimeout默认为600s,最大生存期默认为1800s。前者是指空闲JDBC连接数超过minimumIdle数,空闲时间超过idleTimeout,那么这个空闲连接就会被关闭,后者是指连接池中的连接不能活得超过***xLifetime,到达点就会被关闭。
在“选择1 ”出问题后,我们认为这两个参数大于数据库的wait_timeout=600s,于是将这两个参数降为idle timeout = 570s,***xlifetime = 585s,并设置minimumIdle=5。但这两个时间设置还是大于其中一个数据库错误设置的wait_timeout=60s,所以60s后空闲连接被MySQL服务器主动关闭。但是JDBC没有任何事件触发的回调机制来关闭JDBC连接,时间也不够光触发idleTimeout和***xLifetime清理逻辑,于是光就拿这个“关闭”的连接发“选择1 ”SQL检查到服务器的连接的有效性,这触发了上面的异常。
解决方法很简单,只需将错误配置的数据库中的wait_timeout从60s改为600s即可。以下继续“选择睡眠(1000)”会挂掉退不掉的问题。
缘起:阿里云安全集团和TCP KeepAlive
最近看了一些佛教常识,对“一切法皆由因缘生”的缘起论印象很深。我正在调查“选择睡眠(1000)”在问题中,我真切地感受到了“从因缘”的含义
首先解释一下为什么有些数据库服务器对“选择睡眠(1000)”可以退,但有些退不了。实际上,wait_timeout和interactive_timeout参数只对“空闲”数据库连接有效,即没有SQL运行的连接。对于“选择睡眠(1000)”,有一个正在执行的SQL,其最大执行时间受限于MySQL服务器的***x_execution_time。该参数在我公司一般设置为600s,即“普通数据库”在600多岁时。选择睡眠(1000)”中断执行并退出。
不幸的是(另一个错误配置),我们的数据库***x_execution_time是6000s,所以“选择睡眠(1000)”在mysql服务器上,正常执行会在1000s结束——但问题是通过二分搜索法、tcpdump和iptables TRACE,阿里云会“默默”丢弃>:=910s的空闲TCP连接,没有向客户端和服务器发送FIN或RST强制断开连接,所以MySQL服务器在1000s结束时向客户端发送的ACK+PSH TCP包无法到达客户端,然后wait_timeout=600s,MySQL服务器断开了这个空闲连接——很不幸,MySQL客户端它持续等待MySQL服务器返回。Linux内核的conntrack表显示这个连接已经建立,尽管MySQL服务器已经关闭了相应的连接,但是这个关闭动作的FIN TCP包是无法到达客户端的!
以下是来自iptables TRACE log的这个问题的真锤证明。
mysql命令行所在机器的iptables跟踪日志显示,mysql客户端在23:58:25连接mysql服务器,开始执行SELECT sleep(1000),之后再也没有收到服务器消息。最后在00:41:20,我手动杀死mysql客户端的命令行进程。mysql客户端向mysql服务器发送FIN包,但是没有收到响应(此时mysql服务器的连接已经关闭)。
MySQL server 在 00:15:05 时执行 SELECT sleep(1000) 结束,给 mysql 客户端回送结果,但 mysql 客户端无响应(被阿里云丢包了,mysql 客户端压根收不到),在 00:25:05 时,由于 wait_timeout=600s,所以 MySQL server 给 mysql 客户端发 FIN 包以断开连接,自然,mysql 客户端收不到,所以也没有回应,结局是 MySQL server 一侧的 Linux 内核反正自行关闭 TCP 连接了,mysql client 一侧的 Linux 内核还在傻乎乎的在 conntrack table 维持着 ESTABLISHED 状态的 TCP 连接,mysql client 命令行还在傻乎乎的 recv 等着服务端返回或者关闭链接。00:15:05,服务器执行SELECT sleep(1000),将结果返回给mysql客户端,但是mysql客户端没有响应(被阿里云丢失,mysql客户端根本接收不到)。00:25:05,由于wait_timeout=600s,因此,mysql服务器向mysql客户端发送FIN包断开连接。MySQL客户端自然收不到,所以不响应。最终,MySQL服务器端的Linux内核还是自己关闭了TCP连接。mysql客户端的Linux内核还在傻乎乎的维护conntrack表中已经建立的TCP连接,mysql客户端命令行还在傻乎乎的等待服务器返回或者关闭recv中的链接。
好了,现在我知道是阿里云>:= 910s。如果虚拟机之间没有TCP包传输,空闲的TCP连接会“默默”丢包,那么有没有虚拟机之间呢?它是任何港口吗?服务器需要挂在负载均衡器后面吗?您是否需要到相应端口的一定数量的并发连接?
阿里云提交工单进行查询后,并没有得到任何有价值的信息。经过艰苦的实验——每个实验都要等将近二十分钟——一切都有了回报。我发现了一个稳定循环的规则:
两台虚拟机分别处于不同安全组,没有共同安全组;服务端的安全组开放端口 P 允许客户端的安全组连接,客户端不开放端口给服务端(按照一般有状态防火墙的配置规则,都是只开服务端端口,不用开客户端端口);客户端和服务端连接上后,闲置 >= 910s,不传输任何数据,也不传输有 keep alive 用途的 ack 包;然后服务端在此长连接上发给客户端的 TCP 包会在网络上丢弃,到不了客户端;但如果客户端此时给服务端发点数据,那么会重新“激活”这条长链接,但此时还是单工状态,客户端能给服务端发包,服务端的包还到不了客户端(大概是在服务端 OS 内核里重试中);激活后,服务端再给客户端发数据时,之前发送不出去的数据(如果还在内核里的 TCP/IP 协议栈重试中),加上新发的数据,会一起到达客户端,此后这条长连接进入正常的双工工作状态;
下图是nc测试的结果。
计算机网络服务器
和网友讨论后才知道,这应该是阿里云安全组实施“集中式防火墙”造成的。由于集中式防火墙处于网络的中枢,要应对海量的连接,其内存中的conntrack表需要相对较短的空闲超时(目前为910s)来清理长时间不活动的conntrack记录以节省内存,所以上述问题的根源很明显:
client 连接 server,安全组(其实是防火墙)发现规则允许,于是加入一个记录到 conntrack table;client 和 server 到了 910s 还没数据往来,所以安全组把 conntrack 里那条记录去掉了;server 在 910s 之后给 client 发数据,数据包到了安全组那里,它一看 conntrack table 里没记录,而 client 侧安全组又不允许这个端口的包通过,所以丢包了,于是 server -> client 不通;client 在同一个长连接上给 server 发点数据,安全组一看规则允许,于是加入 conntrack table 里;server 重试的数据包,或者新数据包,通过安全组时,由于已经有 conntrack record 了,所以放行,于是能到达客户端了。
我明白为什么了。我该如何解决这个问题?阿里云给了我两个不可接受的变通办法:
把 server、client 放进同一个安全组;修改 client 所在安全组,开放所有端口给 server 所在安全组;
想了想,通过netstat -o发现我们的Java服务使用的Jedis库和mysql JDBC库都在socket文件句柄上有SO_KEEPALIVE选项[2]:
MySQL服务器也为其open socket文件句柄开放了SO_KEEPALIVE选项,所以我只需要在下层服务器和客户端的至少一侧修改对应的sysctl选项即可。下面是我们服务器的默认配置,也就是说TCP连接空闲1800s后,每30s就会给对方发送一个ACK包。最多发3次。如果对方在此期间回复,定时器将重置,然后等待1800s的空闲状态。如果发送了3次对方都没有响应,那么它会向对方发送RST包,同时关闭本地socket文件句柄,也就是关闭这个长连接。
由于阿里云跨安全组的910s空闲超时限制,需要将
net . IP v4 . TCP _ keepalive _ time设置为小于910s,比如300s。
默认的tcp_keepalive_time特别大,这也解释了为什么Redis客户端在设置了SO_KEEPALIVE选项后,会被阿里云默默断开连接。
如果有些网络库封装后不提供调用setsockopt的机会,需要通过LD_PRELOAD等黑科技强制设置。只有当socket文件句柄的SO_KEEPALIVE选项开启时,以上三个sysctl才会在这个socket文件句柄上生效。当然也可以使用setsockopt函数在代码中进一步设置keep_alive_intvl和keepalive_probes,而不是Linux内核的全局默认设置。
最后,除了Java对SO_KEEPALIVE的处理比较好,使用netstat -o check,发现对门NodeJS著名的Redis客户端库已经开放了SO_KEEPALIVE但是其著名的mysql客户端库没有,而Golang的则严谨得多,两个库都开放了SO_KEEPALIVE。为什么介绍里说这个问题很严重?但是,如果服务器处理缓慢,如OLAP场景,并且服务器不通过阿里云SLB直连在910秒内不返回数据,则可能没有机会向客户端返回数据。来看看这个问题是不是死了!你可能会问我为什么不通过阿里云SLB中转。SLB不会无声无息地丢失数据包——但它的空闲超时限制是900秒!!!
图片来源:“没有虫子”野兔
高可用性架构
改变互联网的构建方式
本文来自又何必自找失落╮投稿,不代表舒华文档立场,如若转载,请注明出处:https://www.chinashuhua.cn/24/575799.html