Kubernetes 的 kube-proxy 因为iptables性能问题 1.8 版本已经支持IPVS模式,在 1.12 中成为 kube-proxy 的默认代理模式。笔者在使用IPVS模式时应用莫名的出现connection reset错误信息,错误信息显示发生在 TCP 长连接,短连接没有出现,而且是隔一段时间才会出现。

应用部署在主机上没有这个问题,只在 Kubernetes 中出现,因为在 Kubernetes 中应用访问后端服务通过service域名负载,难道是这个原因?

问题原因

通过对应用请求使用 tcpdump 进行抓包、搜索IPVS相关资料等发现是IPVS默认 15 分钟在 TCP 连接 idle 的时候断开连接,而且这个断开连接不是IPVS主动发送 reset 包到客户端和服务端,客户端不知道连接已断开发请求到服务端的时候IPVS发送RST包客户端才发现连接已经断开。 ipvs timeout 配置:

$ ipvsadm -l --timeout
Timeout (tcp tcpfin udp): 900 120 300

900s(15 分钟)表示连接过期时间,在客户端与服务端建立 TCP 长连接后,过期时间逐渐递减,如果 900s(15 分钟)内没有数据通信,IPVS会断开连接;只要发送了数据,就会刷新过期时间重新设置为 900s(15 分钟)。

IPVS为什么要设置 900s 后断开连接呢?大家都知道IPVS是一个 4 层网络的负载均衡 工具,并且有多种负载算法,为了保证负载的平均,在 TCP 连接 idle 一段时间后断开连接以便下次连接的时候重新分配。

解决办法

一、TCP 开启 Keepalive

Linux 系统中默认的 TCP Keepalive 参数如下:

  • net.ipv4.tcp_keepalive_time = 7200:单位秒,7200s 为两个小时,表示连接 idle 多长后发送探测数据,如果探测没有回应则关闭连接
  • net.ipv4.tcp_keepalive_intvl = 75:单位秒,探测失败后离下次探测的时间间隔
  • net.ipv4.tcp_keepalive_probes = 9:单位次数,表示探测失败的时候共需要探测的次数 可以通过sysctl命令或配置修改。

有了以上的配置系统也并不会对所有的 TCP 都发送探测,需要在建立 TCP 连接时开启Keepalive,使用

int setsockopt(int s, int level, int optname,
                 const void *optval, socklen_t optlen)

函数设置SO_KEEPALIVE选项对socket连接开启Keepalive,默认使用系统的探测参数。 也可以针对socket单独设置探测参数(覆盖系统配置的参数):

TCP_KEEPCNT: overrides tcp_keepalive_probes
TCP_KEEPIDLE: overrides tcp_keepalive_time
TCP_KEEPINTVL: overrides tcp_keepalive_intvl

所以设置SO_KEEPALIVE选项开启Keepalive后,并配置TCP_KEEPIDLEtcp_keepalive_time小于 900s(15 分钟),如 600s(10 分钟),即可保证连接不会被IPVS断开。

二、应用协议层开启 Keepalive

大部分的 TCP 长连接型服务在应用协议层实现了Keepalive或者Heatbeat,有些客户端(库)也会实现Keepalive来保持连接,可以配置客户端或服务端Keepalive间隔时间小于 900s 保证连接不会被IPVS断开。

三、客户端重连/重试机制

客户端的自动重连/重试也很重要,现在微服务兴起,单组件的升级更新引起的连接断开需要客户端做自动重连,虽然问题不太一样,但是都可以用自动重连/重试来解决。

四、service 使用 Headless 类型

service有多种模式,出现该问题是在service分配了VIP并使用VIP做负载,如果创建 Headless 类型的service不使用IPVS,直接使用service的域名访问、用域名的方式来负载后端服务,避免使用IPVS也可以解决该问题。

五、使用注册中心/服务发现

同时,也可以使用注册中心,并使用注册中心发现服务来避免使用IPVS

IPVS Persistence 与 Kubernetes Service Session Affinity

在查找解决办法初期笔者了解到IPVS有 Persistence 选项,并且 Kubernetes Service Session Affinity使用了该选项,以为设置了 Persistence 选项后就能保持连接,经过实际测试和详细了解后发现并不是。

实际上该功能是会话保持,当一个请求写入了数据如 session,多个后端并没有同步或还未同步,如果客户端访问其他后端会访问失败,所以为了下次请求能够访问同一个后端服务,需要会话保持。

Session Affinity使用,把service.spec.sessionAffinity的值None改成ClientIP后,service 的VIP Persistence 时间默认配置了10800(3 小时,也可通过service.spec.sessionAffinityConfig.clientIP.timeoutSeconds配置),表示 3 个小时内一个客户端的请求都会发送到第一次连接到的后端,即使连接断开,重新建立的连接请求也会发送到该后端。

Reference

  1. IPVS connection timeout issue https://success.docker.com/article/ipvs-connection-timeout-issue
  2. 让 LVS 更持久 https://ieevee.com/tech/2017/08/15/lvs-persistence.html
  3. The setsockopt function call https://www.tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO/