此文基本是翻译aloha的一篇文档,本人实际使用情况遇到的问题类似,但不是MySQL。
[2017.01.12 增补] 1.7版的haproxy开启了IP_BIND_ADDRESS_NO_PORT支持 ,即可以复用source port,这样可以从更基础的内核层面解决这个问题,唯一不足是需要将内核升级到4.2以上版本才可以。
参考:
http://www.haproxy.org/download/1.7/src/CHANGELOG
https://kernelnewbies.org/Linux_4.2#head-8ccffc90738ffcb0c20caa96bae6799694b8ba3a
环境描述
小公司,一个比较繁忙的PHP/MySQL构建的站点。
前端使用haproxy做负载均衡,后端web server连接MySQL数据库。
MySQL做了主从复制,前端PHP代码做了读写分离。
MySQL Master负责写入请求和一小部分读请求,MySQL Slave负责响应读请求。
另一个haproxy作为MySQL的反向代理。
拓扑图
问题描述
在请求很少的时候,工作得非常好。但当MySQL上的请求压力增大(2~3K次/秒)的时候,haproxy的本地端口耗尽。日志中报大量health check SOCKERR错误。
原因分析
haproxy作为反向代理,会使用自己的IP地址作为源地址连接后端的MySQL服务器。
根据TCP协议,无论任何类型操作系统都只能拥有64K个左右的源TCP端口,用于向外发起TCP连接。
一旦"srcIP:port => dstIP:port"建立,这个源端口将不能被重用于其它连接。
当前的MySQL Client Lib关闭连接时的操作序列如下:
Mysql Client ==> "QUIT" sequence ==> Mysql Server
Mysql Client ==> FIN ==> MySQL Server
Mysql Client <== FIN ACK <== MySQL Server
Mysql Client ==> ACK ==> MySQL Server
这时候MySQL Client会进入2MSL状态,时间2分钟。
那么什么是2MSL状态呢?请看下图,这是关于TCP关闭连接时的握手序列:
上边TCP状态图中有一个TIME_WAIT状态,就是所谓的2MSL状态。
MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。
MSL在RFC 1122上建议是2分钟,而源自berkeley的TCP实现传统上使用30秒。
因而,TIME_WAIT状态一般维持在1-4分钟。
该状态是为了可靠地实现TCP全双工连接的终止,保证在tcp客户端发给tcp服务端的最后一个ACK能顺利到达。
若没有TIME_WAIT状态,tcp客户端将直接进入CLOSED状态。
如果tcp客户端直接进入CLOSED状态,那么由于IP协议的不可靠性或者是其它网络原因,导致tcp服务端没有收到tcp客户端最后回复的ACK。那么tcp服务端就会在超时之后继续发送FIN,此时由于tcp客户端已经CLOSED了,就找不到与重发的FIN对应的连接,最后tcp服务端就会收到 RST而不是ACK,tcp服务端就会以为是连接错误把问题报告给高层协议。
这样的情况虽然不会造成数据丢失,但是却导致TCP协议不符合可靠连接的要求。
所以,tcp客户端不是直接进入CLOSED状态,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。
这里有2点需要强调一下:
1、对于tcp请求来说,tcp的客户端服务端概念和http的不同,请求双方,哪边关闭请求,哪边就是tcp客户端,另一边就为服务端。请不要与MySQL Client和MySQL Server混淆起来。
2、tcp的一个链接由4个值确定,源ip、源端口、目标ip、目标地址。
参考:http://omnitraining.net/networking-101/98-networking-101-understanding-tcp-part-2
“There is no way for the person who sent the first FIN to get an ACK back for that last ACK. You might want to reread that now. The person that initially closed the connection enters the TIME_WAIT state; in case the other person didn’t really get the ACK and thinks the connection is still open. Typically, this lasts one to two minutes.”
根据上述的论述,如果一个源端口在2分钟内不能再次使用,则超过534个/秒的MySQL Client请求将会耗尽其本地TCP源端口。
64000 (可用端口) / 120 (2分钟,即120秒) = 533.333.
因为haproxy作为反向代理,会将所有MySQL请求转发给MySQL Server,因此haproxy会比MySQL Client更快的耗尽本地TCP源端口!!
但是如果MySQL Client和MySQL Server在同一台主机上,使用looback接口通信,则MySQL关闭序列是一个相对"干净"的序列:
Mysql Client ==> "QUIT" sequence ==> Mysql Server
Mysql Client <== FIN <== MySQL Server
Mysql Client ==> FIN ACK ==> MySQL Server
Mysql Client <== ACK <== MySQL Server
但在非loopback接口上则不是!因此如果要解决这个问题,需要MySQL的开发者修改他们的代码......
那么是不是完全没有办法呢?也不是,请向下看。
解决方案
1.增加本地端口范围
对于单一的dstIP:port,可用的源端口默认是28K左右,可以用如下命令查看当前值:
[haproxy ~]# sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 61000
增加到64K个源端口
[haproxy ~]# vi /etc/sysctl.conf
net.ipv4.ip_local_port_range = 1025 65000
2.允许处于TIME_WAIT状态的源端口重用
[haproxy ~]# vi /etc/sysctl.conf
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle =
1
3.使用多个IP连接单一dstIP:port,并让haproxy来管理源端口
配置示例:
....
server mysql1 10.0.0.1:3306 check source 10.0.0.100:1025-65000
server mysql1_bis 10.0.0.1:3306 check source 10.0.0.101:1025-65000
....
经过测试,如果不让haproxy管理源端口,则4个源IP地址最多管理不超过80K个TIME_WAIT连接。
但是haproxy管理源端口可以达到170K+个TIME_WAIT连接!!!
4.使用memcached和MySQL persistant connections
参考文档:
http://blog.exceliance.fr/2012/12/12/haproxy-high-mysql-request-rate-and-tcp-source-port-exhaustion/ http://go12345.iteye.com/blog/1798119
http://ganquan.org/blog/2009/09/tcp协议的time_wait状态详解/
http://kerry.blog.51cto.com/172631/105233/
-----
Haproxy作为MySQL中间层如何避免TCP端口耗尽
Haproxy作为MySQL中间层是很成熟的方案,特别是解决从库的负载均衡和故障切换,在生产环境中有着广泛的应用。
在实际使用过程中,有两个问题比较容易发生:
1. TCP端口耗尽
2. 网卡带宽跑满
本文重点讲讲如何优化问题1,问题2暂不讨论。
优化一: 使用尽可能多的端口
Linux系统默认提供了65K个端口,每当Haproxy建立了一个到MySQL的连接,就会消耗一个端口;当Haproxy断开和MySQL的连接时,该端口并不会立即释放,而是会处于TIME_WAIT状态(2*MSL),超时后才会释放此端口供新的连接使用。
我的环境中,tcp_fin_timeout为15秒,也就是说如果我环境中的haproxy可以承载的最大并发连接数为64K/(15*2)=2.1K,可实际上达不到这个上限,原因如下:
$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 15000 65000
linux会保留一段端口,实际能参与分配的端口数只有50K,为了获得尽可能多的可分配端口,做如下调整:
#sysctl net.ipv4.ip_local_port_range="1025 65000"
记得修改/etc/sysctl.conf中对应的内容
优化二: 复用处于TIME_WAIT的端口
调整两个参数:
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
第一个参数很安全,可以不用过多关注。需要注意的是第二个参数,某些情况下会导致数据包被丢弃。
例如:client通过NAT连接haproxy,并且haproxy端打开了tcp_tw_recycle,同时saw_tstamp也没有关闭,当第一个连接建立并关闭后,此端口(句柄)处于TIME_WAIT状态,在2*MSL时间内又一个client(相同IP,如果打开了xfrm还要相同PORT)发一个syn包,此时linux内核就会认为这个数据包异常,从而丢掉这个包,并发送rst包.
不过通常情况下,client都是通过内网直接连接haproxy,所以可以认为tcp_tw_recycle是安全的,只是需要记住此坑。
优化三: 缩短TIME_WAIT时间
Linux系统默认MSL为60秒,也就是正常情况下,120秒后处于TIME_WAIT的端口(句柄)才会释放,可以将MSL的时间缩小,缩短端口的释放周期。
#cat /proc/sys/net/ipv4/tcp_fin_timeout 60
#echo 15 > /proc/sys/net/ipv4/tcp_fin_timeout
这是一个折中的数值,太小也会导致其它问题
优化四: 使用多IP
如优化一中所说,我们已经尽可能多的使用了系统提供的端口范围。但最多依然不超过65K。
Haproxy提供了内建的端口管理方法,可以充分利用以扩大我们的端口范围。
server mysql0 10.0.3.1:3306 check source 10.0.3.100:1025-65000
server mysql1 10.0.3.1:3306 check source 10.0.3.101:1025-65000
如果使用两个ip,我们可用的端口数就接近130K。扩展多个IP,就可以不断增加端口数。
优化五: 使用长连接
服务最好使用长连接,一是避免频繁的申请连接,导致端口耗尽;二是避免创建连接带来的时间消耗。
##评论
复用TIME_WAIT端口的坑我也遇到了。客服收到反馈,有少量用户访问游戏异常。最终定位到和TIME_WAIT有关。关闭timestamps机制可解决这个问题。
===========================================================================
关闭tw_recycle和tw_reuse可屏蔽NAT用户丢包的问题。但失去了快速回收的意义。
关于timestamps机制特性:
An additional mechanism could be added to the TCP, a per-hostcache of
the last timestamp received from any connection.This value could then be
used in the PAWS mechanism to rejectold duplicate segments from earlier
incarnations of theconnection, if the timestamp clock can be guaranteed
to haveticked at least once since the old connection was open. Thiswould
require that the TIME-WAIT delay plus the RTT togethermust be at least
one tick of the sender’s timestamp clock.Such an extension is not part
of the proposal of this RFC.
机制会缓存每个连接的最新时间戳。下一次收到的连接如果小于最新时间戳,则判
断为无效,丢弃数据。(副作用)
timestamps 和 tw_recycle 同时开启会有副作用产生。如关闭timestamps,则无
副作用产生了。
net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1