记一次连接超时问题

过去半年里参与了阿里云Redis数据库的开发,随着用户数的逐渐增多,遇到了许许多多的技术上的问题,本篇文章讲系统遇到的连接超时问题,希望日后对他人起到一点借鉴作用。

系统介绍

在说问题之前,首先简单介绍一下我们系统在数据层的架构,它主要包含4个组件,负载均衡服务器SLB,代理服务器Proxy,后端Redis服务器,以及一个探测系统Detector负责诊断数据链路的各个部分的网络联通状态,架构如下图。
Aliyun Redis数据链路

每个Redis实例对应一个域名xx.region.kvstore.aliyuncs.com,端口都为6379,用户通过该域名进行访问。每个域名将通过DNS解析成一个唯一的VIP。

每个VIP对应一组后端的Proxy,通过SLB将请求进行转发。分组的原因是为了避免问题的影响范围(我们限制每个组里的Proxy负责的后端的Redis实例数目(200个为一组),防止因为某个客户触发了Proxy的BUG影响所有客户。

VIP到Proxy的链路由负责均衡服务器SLB控制,它是四层的负载均衡器,采用透传模式,负责用户请求的接入并把用户的VIP放在TCP包的Option字段中,请求的回复直接通过Proxy返回。
Proxy通过TCP包的Option字段,获取用户需要访问的VIP,接着将请求转发到后端Redis实例。由于需要全兼容redis协议,支持事务和Pub/Sub, Proxy采用的是一对一的连接方式,用户建立多少连接,Proxy也随即建立多少连接。
Redis是直接部署在物理机之上,每台物理机平均部署超过200个Redis实例。

问题现象

这个问题最初是由少数几个php客户提出,它们在单台客户机访问>1个Redis实例过程中,使用短连接偶尔出现连接超时问题。这个问题在起初并没有得到充分的重视,我们没有能有效复现出该问题。只是建议用户将短连接改成长连接,这样之后问题得以很大成都缓解。而客户也并不愿意继续追查,不了了之。直到在我们开发探测系统的过程中,问题才得以发现。
探测系统的基本原理是通过一台探测机,定期向所有的Redis实例发出连接请求和PING命令,检查链路的联通状况。

探测系统在线下测试一切正常,但是当系统在线上部署时在透过SLB探测实例链路时出现了问题。探测过程中频繁出现Redis连接超时问题,且随着探测实例数目的增多,该问题出现的概率越高。当超过1000个实例的时候,几乎每次探测必现。每次报出的实例都不一样,且进行重试之后一般又能连接上,其Log日志如下。

1
2
3
4
5
I1028 18:50:08.678743 client(10.101.72.139:20426) connect to instance 0f4b7c2496714dff(10.102.46.53:6379) timeout, retry times 1
I1028 18:50:08.678749 client(10.101.72.139:19124) connect to instance 4c4b7c25552714dee(10.102.46.53:6379) timeout, retry times 1
I1028 18:50:08.683749 client(10.101.72.139:51714) connect to instance d123a4351ecd1256(10.102.46.53:6379) timeout, retry times 1
I1028 18:50:08.683749 client(10.101.72.139:36471) connect to instance ea1e21345c559s9a(10.102.46.53:6379) timeout, retry times 2
I1028 18:50:08.678821 client(10.101.72.139:36271) connect to instance d5a1e4652259484e(10.102.46.53:6379) timeout, retry times 1

但同时直接探测Proxy和直接探测Redis实例链路则没有问题。

问题分析

通过在Client和Proxy分别抓包发现。超时请求在在客户端看起来是SYN丢包,而在Proxy服务器端的行为是诡异的直接不响应该SYN连接请求。

为了避免干扰,将Proxy单独拿出进行测试,该问题仍然稳定复现,且连接请求不需要太多,有时低于100/s请求时问题都能复现。

而如果将4层负载均衡SLB换成7层的负载均衡该问题将消失。那么问题来了,为什么7层负载均衡没有问题,而4层负载均衡存在问题呢?

答案出在源目标地址!对于7层负载均衡服务器,它负责请求的流入与流出,所以此时在Proxy看来请求的源地址是负载均衡服务器,而4层则是用户的地址。正是这个地址的不同造成了BUG。为解释该现象,此我们将需要先介绍TCP连接相关的内容。

TCP连接4元组

每个TCP连接通过4元组(源地址,源端口,目的地址,目的端口)来唯一标识。

对于一个只有一个IP的客户端来说,当它在应用层发起Connect操作时,它的源地址、目的地址、目的端口是确定的,而源端口是由操作系统指定(也可以事先通过bind来指定,稍后描述),操作系统将为了避免连接重用(相同的四元组),系统将保证对于相同的目的地址和端口将使用不同的本地端口,再依赖于net.ipv4.ip_local_port_range的限制,最多使用65535-1024个连接。如果采用主动bind的方式建立连接,出现了端口相同也将主动报错。

但是值得注意的是系统只保证了对于同一个目的地址和端口,将不重用本地端口,但对于不同的目的地址和端口将可能使用相同的端口访问。这也是为什么一台机器能建立几十万、上百万甚至更多的连接。

问题原因

说完了TCP4元组,再回过头来看我们的问题。我们的系统每个Redis实例拥有不同的VIP,探测机在探测实例时,由于TCP的规则,将存在一定概率使用相同的本地端口。但是我们的系统由于Proxy的存在,所有连接请求将首先经过它。
同时我们的后端Redis实例数目远多于Proxy数目(根据负载情况大约为(50-100):1),这将导致Proxy无法区分在客户端看来的不同连接,从而造成此次BUG。举个例子:
Client的地址为10.101.72.139,它需要连接两个后端实例 (10.102.46.53:6379), (10.102.42.10:6379),这两个实例恰好经由同一个Proxy(10.103.40.50:6379)
Aliyun Redis数据链路

在一个时间点可能出现如下情况:
Client使用了相同的端口10141去连接两个后端实例,建立了如下两个四元组:
(10.101.72.139:10141) -> (10.102.46.53:6379)
(10.101.72.139:10141) -> (10.102.42.10:6379)
这条连接经过SLB到了Proxy,这两条连接的TCP4元组变成了如下:
(10.101.72.139:10141) -> (10.103.40.50:6379)
(10.101.72.139:10141) -> (10.103.40.50:6379)

这两条连接的四元组一致了!这时操作系统只会处理先到来的SYN请求,而直接丢弃后续的连接请求,因为它认为后续的请求可能是由于客户端丢包造成的!而这就是造成了此次BUG!

我们回过头来再看看为什么7层负载均衡没有该问题。这时因为7层负载均衡器是采用与后端建立连接的方式来实现的,而负载均衡服务器由于知道它在访问同一个Proxy IP从而不会复用端口,从而不会造成BUG!

问题总结

解决这个问题前前后后花了超过两周的时间,从最初的怀疑探测器实现问题、到Proxy问题、再到SLB问题,最后才回过头来分析网络TCP问题,走了很多弯路,其实问题的根源在于对于网络的理解不够。合理的做法应该还是从应该从问题现象出发,以分析SYN包为什么会丢为依据,仔细思考,而不要上来就怀疑代码是不是BUG了。希望大家共勉。