新上线的应用连接数据库总是偶发性连接拒绝,开始以为是数据库连接池(使用的Druid)空闲连接配置有误导致,经调整后依然没有解决问题。
后偶然发现,使用Telnet 探测连接,也存在类似问题,后开始在调用链路各节点开始抓包,根据抓包结果,多方在网络上查看各位大神的帖子和咨询资深同事,终于确认问题原因并解决。现记录该问题如下,以便后续类似问题参考。(文章中的知识点来源于与网上各种资料,记录该文章为自己记录以及有相同问题的朋友多个思路,如有侵权,可联系删除)
先来说一下此次问题的系统调用链路:应用-->f5--->数据库。
抓包结果:在F5上看到偶有请求,在三次握手建立之初,F5发了SYN 包给到后端数据库中间件,后端无应答,重发三次后,连接被F5重置。
问题原因:数据库中间件开启了net.ipv4.tcp_tw_recycle,导致部分TCP连接失败。(开启参数为:net.ipv4.tcp_tw_recycle=1;net.ipv4.tcp_timestamps=1。F5 通过SNAT 地址池访问数据库中间件)
以下为该问题分析记录,涉及实际应用,未贴出抓包截图。
先来解释下两个Linux 内核参数:
1)net.ipv4.tcp_tw_recycle:用于快速回收处于TIME_WAIT 状态的socket 连接;(通常在开启net.ipv4.tcp_timestamps 时有效,1代表开启,0代表关闭)
2)net.ipv4.tcp_timestamps:启用时间戳,默认缺省值为1,开启;该值必须为单调递增,否则接受到的包可能会被丢掉。
TCP协议中有一种机制,缓存了每个主机(即ip)过来的连接最新的timestamp值。这个缓存的值
可以用于PAWS(Protect Against Wrapped Sequence numbers,是一个简单的防止重复报文的机制)中,来丢弃当前连
接中可能的旧的重复报文。而Linux实现这个机制的方法就是同时启用net.ipv4.tcp_timestamps和net.ipv4.tcp_tw_recycle
这两个选项。
这种机制在 客户端-服务器 一对一的时候,没有任何问题,但是当服务器在负载均衡器后面时,由于负载均衡器不会修改
包内部的timestamp值,而互联网上的机器又不可能保持时间的一致性,再加上负载均衡是会重复多次使用同一个tcp端口
向内部服务器发起连接的,就会导致什么情况呢:
负载均衡通过某个端口向内部的某台服务器发起连接,源地址为负载均衡的内部地址——同一ip
假如恰巧先后两次连接源端口相同,这台服务器先后收到两个包,第一个包的timestamp被服务器保存着,第二个包又来了,
一对比,发现第二个包的timestamp比第一个还老——客户端时间不一致。服务器基于PAWS,判断第二个包是重复报文,
丢弃之
反映出来的情况就是在服务器上抓包,发现有SYN包,但服务器就是不回ACK包,因为SYN包已经被丢弃了。为了验证这
一结果,可以执行netstat -s | grep timestamp 命令,看输出里面passive connections rejected by timestamp 一项的数字变化。
假如启用net.ipv4.tcp_tw_recycle,可以用于快速减少在TIME-WAIT状态TCP连接数。,尤其是在普通用户家中,有多台设备,或者网吧、公司等多台设备,共用同一个NAT设备环境下,TW回收选项是很有问题的面向公共服务器作为它不会把手连接两台不同的计算机上,这问题很难发现,无从下手。
* Connection table slot连接表槽
处于TIME-WAIT状态的TCP连接,在链接表槽中存活1分钟,意味着另一个相同四元组(源地址,源端口,目标地址,目标端口)的连接不能出现,也就是说新的TCP(相同四元组)连接无法建立。对于web服务器来说,目标地址、目标端口都是固定值。如果web服务器是在L7层的负载均衡后面,那么源地址更是固定值。在LINUX上,作为客户端时,客户端端口默认可分配的数量是3W个(可以在参数net.ipv4.up_local_port_range上调整)。
这意味着,在web服务器跟负载均衡服务器之间,每分钟只有3W个端口是处于established状态,也就大约500连接每秒。如果TIME-WAIT状态的socket出现在客户端,那这个问题很容易被发现。调用connect()函数会返回EADDRNOTAVAIL,程序也会记录相关的错误到日志。如果TIME-WATI状态的socket出现在服务端,问题会非常复杂,因为这里并没有日志记录,也没有计数器参考。不过,可以列出服务器上当前所有四元组连接的数量来确认
* 解决办法:增加四元组的范围,这有很多方法去实现。(以下建议的顺序,实施难度从小到大排列)
修改net.ipv4.ip_local_port_range参数,增加客户端端口可用范围。
增加服务端端口,多监听一些端口,比如81、82、83这些,web服务器前有负载均衡,对用户友好。
增加客户端IP,尤其是作为负载均衡服务器时,使用更多IP去跟后端的web服务器通讯。
增加服务端IP。
当然了,最后的办法是调整net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle。
最合适的解决方案是增加更多的四元组数目,比如,服务器可用端口,或服务器IP,让服务器能容纳足够多的TIME-WAIT状态连接。在我们常见的互联网架构中(NGINX反代跟NGINX,NGINX跟FPM,FPM跟redis、mysql、memcache等),减少TIME-WAIT状态的TCP连接,最有效的是使用长连接,不要用短连接,尤其是负载均衡跟web服务器之间。尤其是链家事件中的PHP连不上redis。
* 在服务端,不要启用net.ipv4.tcp_tw_recycle,除非你能确保你的服务器网络环境不是NAT。在服务端上启用net.ipv4.tw_reuse对于连接进来的TCP连接来说,并没有任何卵用。
* 在客户端(尤其是服务器上,某服务以客户端形式运行时,比如上面提到的nginx反代,连接着redis、mysql的FPM等等)上启用net.ipv4.tcp_tw_reuse,还算稍微安全的解决TIME-WAIT的方案。再开启net.ipv4.tcp_tw_recycle的话,对客户端(或以客户端形式)的回收,也没有什么卵用,反而会发生很多诡异的事情(尤其是FPM这种服务器上,相对nginx是服务端,相对redis是客户端)。
主要解释参考转载自如下博客,感谢
https://blog.51cto.com/legehappy/2095539