4层负载均衡服务偶发1S请求的故障分析

4层负载均衡平台上线后,在公司推广比较顺利,有很多业务使用。我们支持两种服务:静态负载均衡与动态负载均衡,静态负载均衡采用了LVS的Tunnel模式,而动态负载均衡由于需要动态实时与K8S中发布的服务同步,所以采用的是NAT的模式。今天要说的问题,就是业务方反馈他们部署在K8S中的服务,上了4层负载均衡,会出现偶发1S的长延时请求。调查过程有些曲折,所以这里记录下来,供后续学习和参考。

系统环境与问题说明

为了避免公司敏感信息,这里列举的IP地址都是虚构的。

  • LVS环境: IP:10.1.1.1, PORT:7012
  • 客户端在K8S容器中的环境: IP地址:10.2.1.1
  • 服务端在K8S容器中的环境: IP地址:10.2.2.2 PORT:8012

业务访问流程为:
客户端->LVS(10.1.1.1:7012)->服务端(10.2.2.2:8012)

在LVS主机我们查看一下LVS的规则

root@ubuntu:/home/yuxianbing# ipvsadm -ln 
IP Virtual Server version 1.2.1 (size=1048576)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  10.2.1.1:7012 wrr
  -> 10.2.2.2:8012            Masq    10     0          0         

抓包分析

业务调用的过程我们在三端都进行了抓包,分别如下:

  • 客户端

从上面客户端的TCP的抓包结果来看,主要过程如下:

  1. 客户端发起请求包括两个过程:获取连接、执行请求;
  2. 服务器端11:33分主动发起了Close操作,并且客户端连接没有设置tcp_keepalive;
  3. 客户端11:35:04发起Close操作,发送FIN包到LVS;
  4. 客户端11:35:05秒发起FIN包的重发操作;
  5. 客户端11:35:05发送SYN包到LVS,建立连接;
    .......
  • 看了一下业务方的代码,业务方代码的逻辑大致为:
  1. 从连接池获取连接;
  2. 调用http rpc请求;
  3. 记录1,2两个步骤的延迟时间;

从问题看来,业务请求发起后,没有主动发起连接的Close操作;
服务端超时了,主动发起了Close操作后;
等待了2分钟左右,客户端发起请求逻辑,进入到第一步操作,从连接池获取连接,发现连接已经被close,于是被动调用Close,但是这一步触发了FIN包多次重传;(整个重传周期很长)
1秒后Close操作完成,客户端连接池于是发起新的连接,并发起HTTP请求。
于是整个过程超过1S。
FIN重传了很多次,但是从结果来看,重传两次左右,系统就开始发起新的SYN来建立连接了。

LVS为什么没有响应的客户端的FIN包

上面抛出了一个问题,为什么FIN包会重传,没有收到LVS端的ACK包?
K8S环境下的4层负载均衡服务,我们是采用的是LVS的NAT模式,我们是基于LVS的DNAT加上linux系统上iptables规则实现SNAT,来达到FULLNAT的功能。所以这里要用到系统的连接跟踪(conntrack),连接跟踪的概念的如下:

连接跟踪(CONNTRACK),顾名思义,就是跟踪并且记录连接状态。Linux为每一个经过网络堆栈的数据包,生成一个新的连接记录项 (Connection entry)。此后,所有属于此连接的数据包都被唯一地分配给这个连接,并标识连接的状态。连接跟踪是防火墙模块的状态检测的基础,同时也是地址转换中实 现SNAT和DNAT的前提。那么Netfilter又是如何生成连接记录项的呢?每一个数据,都有“来源”与“目的”主机,发起连接的主机称为“来源”,响应“来源”的请求的主机即为目的,所谓生成记录项,就是对每一个这样的连接的产生、传输及终止进行跟踪记录。由所有记录项产生的表,即称为连接跟踪表。

跟这个故障相关的conntrack配置项为:
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
在我们的LVS机器上,配置的是120秒,也就是2分钟,所以在该CASE中,服务器端主动发起连接操作后,两分钟后连接跟踪表中对应的表项会被删除。这时候客户端的FIN请求,LVS由于连接表项中不存在对应的记录,由于发现包是FIN控制包,所以不会再去创建对应的连接记录项,直接把包转发到服务器后端。

而在服务器后端收到包后,没有对应的连接,就会直接回一个RST包,所以客户端会不断的发起FIN重传。

补充另外一种现象以及处理方案

抓包发现情况2:

  1. 15:38:46.198336 CLIENT->LVS SYN包(TCP Port numbers reused)
  2. 15:38:47.201539 CLIENT->LVS SYN包(TCP Retransmission)
  3. 15:38:47.201657 LVS->CLIENT SYN/ACK包
  4. 15:38:47.201703 CLIENT->LVS ACK
    从这种情况来看,建立连接用了1秒钟来完成,造成业务请求超过1秒。
    通过分析,我们发现我们的有一项向内核参数设置如下:

net.ipv4.vs.conn_reuse_mode = 1

这个参数可以让我们对tcp端口及时的重复利用,但是由于在k8s环境下,我们利用了LVS的NAT以及主机的SNAT功能(conntrack=1),客户端与LVS发起的连接请求,在与服务端建立连接后,我们会通过连接记录项进行记录,连接记录项也有状态,而当TCP Close阶段,连接记录项处于TIMEWAIT状态时。这时候,如果客户端再次发起连接(使用重用端口),IPVS由于发现了该记录项,就会把第一个SYN包DROP掉,进行保护,这样导致一秒后的SYN包重传。

为了解决这个问题,我们把参数进行了调整。设置为

net.ipv4.vs.conn_reuse_mode = 0
为就能够得到解决了。