今天遇到了一个问题,客户端psql在连接Greenplum时,静置一段时间后(大约15分钟),随便敲一条select * from xxx,psql没返回结果,而是返回了个提示信息:

psql: server closed the connection unexpectedly 
This probably means the server terminated abnormally
before or while processing the request.

看上去是server端把连接关闭了。出问题的时候抓了下包,可以看到client向server发了一个query,但是server直接回了个RST报文,说明的确是server端主动分手关闭的连接。

但是才15分钟,server不应该关闭连接。由于client到server的连接中间还经过了一层lvs,所以我查了下lvs的表项:

ipvs -l -c
IPVS connection entries
pro expire state       source             virtual               destination
TCP 13:46  ESTABLISHED [client ip]:60718  [server vip]:postgres [docker ip]:postgres
TCP 26:43  NONE        [client ip]:0      [server vip]:postgres [docker ip]:postgres

如果在client端再query一次,可以看到expire重新刷新为了15:00,query成功;如果client一直静置,可以看到expire一直递减,当expire递减为0时,该ESTABLISHED表项被清除,此时尝试query会失败,client弹出如上错误。

所以问题就比较明确了:LVS的会话表项老化删除后,psql client发过来的请求无人应答(并非连接建立阶段),直接被kernel RST掉。

那么上面的15分钟是怎么来的呢?我们可以通过如下命令查看。

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

单位是秒,900正好是15分钟。

怎么修改呢?

ipvsadm设置timeout

可以用ipvsadm设置timeout时间。

ipvsadm --set 7200 120 300

但ipvsadm有个问题是,如果机器重启了,这个配置也就丢了。当然可以再去写个service来保存,但又有点杀鸡用牛刀的感觉。有没有更好的方法呢?

keepalived设置lvs_timeouts

更好的做法是继续利用keepalived。读过不要开启tcp_tw_recycle的同学知道,我们的vip是通过kube-keepalived-vip这个容器来做的,内核的lvs表项也是该容器下发的。

那么keepalived有没有个参数可以配置下,让keepalived启动的时候,告诉内核设置下tcp timeout时间呢?

答案是有的,虽然你可能从来没见有人配置过。

keepalived支持配置lvs_timeouts(相信我,这可能是第一篇介绍如何配置该参数的blog),配置方法如下:

global_defs {
  vrrp_version 3
  vrrp_iptables KUBE-KEEPALIVED-VIP

  lvs_timeouts tcp 7200 tcpfin 120 udp 300
  lvs_sync_daemon <INTERFACE> <VRRP_INSTANCE>
}

注意一定要配置lvs_sync_daemon,否则lvs_timeouts不会真正下发给内核lvs。

lvs_timeouts 后面的tcpfin udp两个实际可以不配置。吐槽下,lvs_timeouts在keepalived中没文档介绍如何使用,我是从代码反推的。饶是如此,还是漏了lvs_sync_daemon

persistence_timeout的用途

读者可能会问一个问题,平时我们看到的keepalived的persistence_timeout参数是做什么用的?

想一下这个情景:http一般是短连接,客户端a发送请求给了lvs,lvs根据负载均衡策略将请求转发给了realserver 1(rs1);rs1也正确应答了,一个完整的流程。

紧接着,客户端a又发送了一个http请求给lvs,此时lvs应该转发给rs1还是rs2呢?如果给rs2,那么如果rs1和rs2的数据同步没有做好的话,极有可能会出现时序问题(例如你在rs1处付了款但是rs2不知道让你再付一遍)。

此时比较简单的做法,就是让lvs对于持续一定时间内的client过来的请求,均转发给同一个real server,这个就叫做lvs的persistence,这个时间就叫做persistence_timeout

默认kube-keepalived-vip容器配置的persistence_timeout时间为1800,即30分钟。细心的读者可能看到文章开始处查看lvs表项时,有一条NONE的表项,这条表项就是用来记录persistence的。NONE表项的expire总是不会超过30分钟,expire会随着时间的流逝逐步递减,如果expire减为0,NONE表项会被老化删除掉。

在NONE表项还没有老化之前,所有该client ip的报文,都会被转发到同一个real server;当NONE表项老化之后,再有新的clinet ip过来请求,报文会根据配置的lvs负载均衡策略,重新选择一个real server。

需要注意的是,如果persistence_timeout配置的时间比上面配置的lvs_timeouts时间小,当NONE表项的expire为0超时的时候,如果ESTABLISHED表项还没有超时(expire不为0),NONE表项会重新进入一个1分钟的老化阶段。这样,同一个client ip的报文还是会转发给同一个real server,防止连接还在的时候,报文被错误的转发给另一个后端。

老化与重刷

NONE表项和ESTABLISHED表项的expire时间是怎么重新刷新的呢?

ESTABLISHED表项的expire重刷机制比较简单,只要该链路上有新报文,expire就会重刷重新计时。NONE表项的expire重刷机制不同,它是通过新建连接刷新的。

所以如果ESTABLISHED的连接一直有报文刷新expire的话,如果此时没有新连接建立,NONE表项的expire不会刷新,无论persistence_timeoutlvs_timeouts谁大谁小,都会出现上面的NONE表项1分钟缓刑的情况。

应如何取值

那么应该如何配置persistence_timeout呢?你可能看过一些文章说这个超时时间最好配置成跟lvs_timeouts一致,当然这么配没什么问题,但其实二者没有必然的联系,lvs_timeouts想保护的是一个人(单一连接),而persistence_timeout想保护的是一类人(源自统一client ip的连接),根据实际情况将二者设置为不同值会更合理一些。

处于负载均衡的考虑,不建议将persistence_timeout配置的太大,这样可能会导致长时间client总是被转发到同一个real server。

lvs_timeouts建议配置为 >7200。为什么呢?TCP的保活定时器默认时间是2小时,当保活定时器超时时:

  • 如果client还正常跑着,TCP会发送探测报文,该报文刷新lvs ESTABLISHED表项,重置expire,TCP连接、lvs表项均继续存在
  • 如果client已经挂了,此时TCP保活定时器超时,即使没有lvs的存在也会关闭TCP连接;将lvs_timeouts配置为稍大于7200的值,会给用户相同的体验,避免lvs表项太早老化引起用户不满。