haproxy TCP源端口耗尽问题

此文基本是翻译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 发布于:2019-10-28 07:03:07