一,问题排查
测试说,网站页面突然很卡。
首先kubernates集群没有报警,集群资源cpu,内存使用也还算正常。
然后查看jvm,发现并没有大量的full gc。确切的说,近期连一次full gc都没有发生。那么说明,不是pod问题,不是jvm的gc引起的。
再然后排查数据库连接,通过druid查看,发现sql也没有什么问题。
看起来一切都是这么安好。。。
转念一想,既然服务都如此正常,会不会是网络资源问题?
查看集群资源,网络连接情况。
发现tcp连接数在5000,按说呢这个量并不大。
但是奇怪的是,这个5000的瓶颈,这个拐点就很可疑。看起来是收到了限制。
经查,果然是slb设置了最大tcp连接数为5000,导致更多的请求进不来,导致的此次问题。
将该阈值改为5w,再次压测,问题解决。
二,根本原因处理
不过这只是解决了问题的表象,这么多time_wait tcp连接说明什么呢?
time_wait 说明client已经向服务端发送最后的关闭请求,自己等待2msl的过程中。
2MSL有多久呢?
RFC793定义了MSL为2分钟,Linux设置成了30s。所以2MSL一般就是1分钟。那么一分钟内的大量tcp请求就会在time_wait状态积压,从而给后续的请求带来问题。
为什么要规定2MSL的等待时间呢?
主要是保证最后一次ACK能够到达服务端,和一次丢失重传的时间,从而保证server端也正常关闭。
如果第一次ACK服务端没有收到,会重新发送FIN信息,从而client会知道数据丢失,从而重发。避免客户端自己关闭,服务端没有收到ACK,重发,仍然收不到,从而无法关闭的情况。
还可以保证数据同样的四元组(收发IP/PORT),旧的已经关闭,从而防止数据错乱。
回顾自己的应用,推测是由于大量的grpc连接造成的,那么需要对grpc client进行优化,将每次请求创建一个client,改为所有相同的请求使用同一个client后,tcp中time_wait数量,和inuse数量都大幅减少,从而解决该问题。
三,单例的grpc client是否会引起性能问题?
按说,grpc使用http2,而http2是支持多路复用的,这样说来或许不需要额外的连接池。
真的是这样吗?
通过压测,我发现单个连接的client可以支持最大到9w的qps,并发量再大,cpu占用率会上去,但吞吐量会下来。
为什么会这样的?
猜测大概是由于tcp头阻塞和client锁竞争引起的。
应用层的头阻塞:在http1.1时各个request和response需要排队进行,在没有收到response报文之前,该tcp不能被复用。
虽然http2避免了应用层的头阻塞,但依然存在tcp层的头阻塞。
tcp头阻塞:tcp在并发发包时,接收方可能会先收到后面的包,那么这个包就不能放到socket buf上,只能先放到内核协议栈上,这就是tcp头阻塞。
那么怎么解决呢?
一个成熟的方案是,池化。
通过使用自建连接池,经测qps可以达到40w左右。
总结,如果服务的并发量不高,使用单个client既可以避免大量的tcp TIME_WAIT问题,也可以支撑数万的qps。
当并发量远远超过这个数量级时,可以考虑使用池化方案进行优化。
滴滴在使用grpc时就使用了自建的连接池
具体的池化代码,在我这篇文章:grpc连接池实现。
四,另外,linux也可以设置time_wait相关属性,从而从另一个纬度上解决这个问题。
编辑内核文件/etc/sysctl.conf,加入以下内容:
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAITsockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系默认的 TIMEOUT 时间
然后执行 /sbin/sysctl -p 让参数生效.
/etc/sysctl.conf是一个允许改变正在运行中的Linux系统的接口,它包含一些TCP/IP堆栈和虚拟内存系统的高级选项,修改内核参数永久生效。
简单来说,就是打开系统的TIMEWAIT重用和快速回收。
当然还可以调大time_wait的上限
vi /etc/sysctl.conf
net.ipv4.tcp_keepalive_time = 1200
#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟。
net.ipv4.ip_local_port_range = 1024 65000
#表示用于向外连接的端口范围。缺省情况下很小:32768到61000,改为1024到65000。
net.ipv4.tcp_max_syn_backlog = 8192
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_tw_buckets = 5000
#表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。
默认为180000,改为5000。对于Apache、Nginx等服务器,上几行的参数可以很好地减少TIME_WAIT套接字数量,但是对于 Squid,效果却不大。此项参数可以控制TIME_WAIT套接字的最大数量,避免Squid服务器被大量的TIME_WAIT套接字拖死。