让LVS更持久
by 伊布
今天遇到了一个问题,客户端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_timeout
和lvs_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表项太早老化引起用户不满。
Subscribe via RSS