高并发服务遇Redis瓶颈引发的事故

元旦期间 订单业务线 告知 推送系统 无法正常收发消息,作为推送系统维护者的我正外面潇洒,无法第一时间回去,直接让 ops 帮忙重启服务,一切好了起来,重启果然是个大杀器 。由于推送系统本身是分布式部署,消息有做各种的可靠性策略,所以重启是不会丢失消息事件的 。
:sweat_smile: 事后通过日志分析有大量的 redis 的报错,十分钟内有 16w 次的错误 。日志的错误是 connect: cannot assign requested address  。该错误不是推送服务内部及 redis 库返回的 error,而是系统回馈的 errno 错误 。
这个错误是由于无法申请可用地址引起的,也就是无法申请到可用的 socket 。
话说,元旦当天在线数和订单量确实大了不少,往常推送系统的长连接客户端在 35w,这次峰值飙到 50w 左右, 集群共 6 个节点,其中有 4 个节点每个都扛了 9w+ 的长连接 。另外,推送的消息量也随之翻倍 。

高并发服务遇Redis瓶颈引发的事故

文章插图
 
分析下面是 kibana 日志的统计,出错的时间区间里有近 16w 次的 redis 报错 。
高并发服务遇Redis瓶颈引发的事故

文章插图
 
下面是出问题节点的 TCP 连接状况,可以看到 established 在 6w,而 time-wait 连接干到 2w 多个 。
高并发服务遇Redis瓶颈引发的事故

文章插图
 
为什么会产生这么多 time-wait?谁主动关闭就就有 time-wait,但推送系统除了协议解析失败之外,其余情况都不会主动 close 客户端,哪怕是鉴权失败和弱网络客户端写缓冲爆满,事后通过日志也确定了不是推送系统自身产生的 tw 。
另外,linux 主机被 ops 交付时应该有做内核调优初始化的,在开启 tw_reuse 参数后,time-wait 是可以复用的 。难道是没开启 reuse?
查看 sysctl.conf 的内核参数得知,果然 tcp_tw_reuse 参数没有打开,不能快速地复用还处在 time-wait 状态的地址,只能等待 time-wait 的超时关闭,rfc 协议里规定等待 2 分钟左右,开启 tw_reuse可在 1s 后复用该地址 。另外 ip_local_port_range 端口范围也不大,缩短了可用的连接范围 。
【高并发服务遇Redis瓶颈引发的事故】sysctl-a|egrep "tw_reuse|timestamp|local_port"net.ipv4.ip_local_port_range = 3576860999net.ipv4.tcp_timestamps = 1net.ipv4.tcp_tw_reuse = 0所以,由于没有可用地址才爆出了 connect: cannot assign requested address 错误 。
内在问题追究问题上面是表象问题,来查查为什么会有这么多的 time-wait ?再说一遍,通常哪一端主动 close fd,哪一端就会产生 time-wait 。事后通过 netstat 得知 time-wait 连接基本是来自 redis 主机 。
下面是推送代码中的连接池配置,空闲连接池只有 50,最大可以 new 的连接可以到 500 个 。这代表当有大量请求时,企图先从 size 为 50 的连接池里获取连接,如果拿不到连接则 new 一个新连接,连接用完了后需要归还连接池,如果这时候连接池已经满了,那么该连接会主动进行 close 关闭 。
MaxIdle= 50MaxActive = 500Wait= false除此之外,还发现一个问题 。有几处 redis 的处理逻辑是异步的,比如每次收到心跳包都会 go 一个协程去更新 redis, 这也加剧了连接池的抢夺,改为同步代码 。这样在一个连接上下文中同时只对一个 redis 连接操作 。
解决方法调大 golang redis client 的 maxIdle 连接池大小,避免了大并发下无空闲连接而新建连接和池子爆满又不能归还连接的尴尬场面 。当 pool wait 为 true 时,意味着如果空闲池中没有可用的连接,且当前已建立连接的连接数大于 MaxActive 最大空闲数,则一直阻塞等待其他人归还连接 。反之直接返回 "connection pool exhausted" 错误 。
MaxIdle= 300MaxActive = 400Wait= trueredis 的 qps 性能瓶颈redis 的性能一直是大家所称赞的,在不使用 redis 6.0 multi io thread 下,QPS 一般可以在 13w 左右,如果使用多指令和 pipeline 的话,可以干到 40w 的 OPS 命令数,当然 qps 还是在 12w-13w 左右 。
Redis QPS 高低跟 redis 版本和 cpu hz、cache 存在正比关系
根据我的经验,在内网环境下且已实例化连接对象,单条 redis 指令请求耗时通常在 0.2ms 左右,200us 已经够快了,但为什么还会有大量因 redis client 连接池无空闲连接而建立新连接的情况?
通过 grafana 监控分析 redis 集群,发现有几个节点 QPS 已经到了 Redis 单实例性能瓶颈,QPS 干到了近 15w 左右 。难怪不能快速处理来自业务的 redis 请求 。这个瓶颈必然会影响请求的时延 。请求的时延都高了,连接池不能及时返回连接池,所以就造成了文章开头说的问题 。总之,业务流量的暴增引起了一系列问题 。


推荐阅读