当前位置: 首页 > 科技观察

高并发服务遇到redis瓶颈,引发时间等待事故_0

时间:2023-03-22 11:30:52 科技观察

摘要元旦期间,订单业务线通知推送系统无法正常收发消息。作为推送系统的维护者,我在外面,不能马上回去。我请操作人员帮助重新启动它。服务,一切正常,重启真是大杀器。由于推送系统本身是分布式部署的,所以对消息实施了各种可靠性策略,所以重启后消息事件不会丢失。后来通过日志分析,有大量的redis报错,十分钟之内就有16w个错误。日志中的错误是connect:cannotassignrequestedaddress。这个错误不是推送服务或者redis库返回的错误,而是系统返回的errno错误。该错误是由于无法申请可用地址,即无法申请可用套接字引起的。也就是说,元旦当天的在线人数和订单量确实增加了很多。平时推送系统的长连接客户端在35w左右,这次峰值飙升到50w左右。集群有6个节点,其中4个都是耐9w+长连接。此外,推送的消息量也翻了一番。分析下面是kibana日志的统计。错误时间间隔内报告了近160,000个redis错误。以下是问题节点的TCP连接状态。可以看到established在6w,time-waitconnection在2w多。为什么要等那么久?谁主动关闭就会有time-wait,但是推送系统除了协议解析失败外,不会主动关闭客户端,即使认证失败弱网客户端写缓冲区已满,也可以通过之后的log并不是推送系统自己生成的tw。另外,在ops下发linux主机的时候,要进行内核调优的初始化。开启tw_reuse参数后,time-wait可以被复用。难道是没有启用复用?查看sysctl.conf的内核参数,原来tcp_tw_reuse参数没有开启,还处于time-wait状态的地址无法快速重用,只能等待time-wait超时被关闭。rfc协议规定等待时间大约为2分钟。开启tw_reuse在1s后重用该地址。另外ip_local_port_range端口范围不大,缩短了可用连接范围。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:cannotassignrequestedaddress错误爆发,因为没有可用地址.Intrinsicproblems排查问题以上是一个表面问题,我们来看看为什么会有这么多time-waits?还是那句话,通常哪一端主动关闭fd,哪一端就会产生time-wait。后来通过netstat了解到time-wait连接基本是从redis主机来的。以下是推送代码中的连接池配置。空闲连接池只有50个,最多可以新建500个连接。这意味着当有大量请求时,首先尝试从连接池中获取大小为50的连接.如果无法获取连接,则会创建一个新连接。连接用完后,需要归还连接池。如果此时连接池已满,则连接会被主动关闭。MaxIdle=50MaxActive=500Wait=false另外发现一个问题。有几种redis处理逻辑是异步的。比如每收到一个心跳包,就会发送一个协程去更新redis,这样也加剧了对连接池的抢夺,改成了同步代码。这样在一个连接上下文中同时只操作一个redis连接。解决方法是增加golangredis客户端的maxIdle连接池大小,避免出现没有空闲连接却有新连接且池已满无法归还连接的尴尬情况。当poolwait为true时,表示如果空闲池中没有可用的连接,并且当前建立的连接数大于MaxActive的最大空闲数,就会阻塞等待别人返回连接。否则直接返回“连接池耗尽”错误。MaxIdle=300MaxActive=400Wait=trueredisqps性能瓶颈。redis的性能一直被大家称赞。没有redis6.0multiiothread,QPS一般可以在13w左右。如果使用多指令和流水线,最多可以做到40w条OPS指令,当然qps还是12w-13w左右。RedisQPS的高低与redis版本、cpuhz、缓存成正比。根据我的经验,在内网环境下,连接对象已经实例化,单条redis指令请求一般需要0.2ms左右,200us已经足够快了,但是为什么还会有大量新连接建立,因为redis客户端连接池没有空闲连接?通过grafana对redis集群的监控分析,发现有几个节点的QPS已经达到了Redis单实例性能的瓶颈,QPS达到了将近15w。难怪业务的redis请求不能快速处理。这个瓶颈必然会影响到请求的延迟。请求延迟高,连接池不能及时返回连接池,导致文章开头提到的问题。总之,业务流量的激增带来了一系列的问题。如果发现问题,则需要解决它。Redis的qps优化方案有两步:扩容redis节点,迁移slot共享流量。尝试将程序中的redis请求改为批处理方式。添加节点很容易,批处理也很容易。最开始优化push系统的时候,把同逻辑的redis操作改成了batch方式。但问题来了。很多redis的操作都在不同的逻辑块,不可能合成一个pipeline。然后进一步优化,将不同逻辑的redis请求合并到一个pipeline中。好处是提高了redis的吞吐量,减少了socket系统调用和网络中断开销。缺点是增加了逻辑复杂度,使用channelpipeline做Queues和notification增加了运行时调度的开销。pipelineworker的触发条件是满足3条命令或者5ms超时。定时器采用分段时间轮。与优化修改前相比,cpu开销降低了3%左右。压测下,redisqps平均下降了3w左右,最多可以降到7w左右。当然,消息延迟的概率会高几个ms。实现逻辑参考下图。调用者将redis命令和接收结果的chan推送到任务队列,然后一个worker消费。worker将多个rediscmd组装成一个pipeline,向redis发起请求并取回结果,拆解结果集后,将结果推送到每个command对应的resultchan中。调用者将任务推入队列后,一直监听发送结果的chan。这个方案来源于我之前公司推送系统的经验。有兴趣的朋友可以看看PPT,里面有很多golang高并发的经验。总结推送系统设计之初,预计长连接数为20w。稳定后没有优化调整,一直在线稳定运行。后来随着生意的暴涨,长连接的数量也是暴涨。现在平时稳定在35w,一出问题突然涨到50w。由于业务突然增长,我们没有对整个链路进行压测和优化。也就是说,如果你多关注推送系统,就不会出现这个问题。之前开发的是比较高标准的推送系统,现在接手了公司的推送系统。因为很一般,但是太商业化了,伤脑筋,所以没有拆下来重构。我一直在添加这个架子并做一些常规的性能优化。嗯,看来还是不能掉以轻心,免得自己的表演功亏一篑。