前段时间我们把生产环境的Redis从5.0升级到了6.0,开启了多线程IO功能,本来想提升性能,没想到却引发了一次惊心动魄的线上故障,折腾了好几个小时才恢复。今天来复盘一下这次故障的经过、原因和解决方法,以及从中得到的经验教训,希望能给大家一些参考。
一、背景
先聊聊这次故障的背景。
我们公司的业务用Redis做缓存和部分数据的存储,有一个Redis集群大概10个节点,每个节点都是主从架构,QPS大概在5万左右,延迟一直比较稳定在1ms以内。
Redis 6.0发布之后我们很关注,因为Redis 6.0最大的新特性就是支持多线程IO了。之前Redis一直是单线程的,虽然性能已经很强了,但是在高并发的场景下网络IO会成为瓶颈,多线程IO能大幅提升网络IO的处理能力,提升吞吐量。
我们做了一些调研和测试,发现开启多线程IO之后Redis的吞吐量能提升一倍以上,延迟也更稳定,所以我们决定把生产环境的Redis升级到6.0,开启多线程IO提升性能。
我们先在测试环境做了充分的测试,包括功能测试、性能测试、稳定性测试,都没有问题,然后选择了一个业务低峰的时间开始灰度升级,先升级了几个节点观察了一天没有问题,然后就把所有节点都升级了,开启了多线程IO。
升级之后前几个小时一切正常,性能确实有提升,我们还很高兴,觉得这次升级很成功,但是没想到到了晚上业务高峰的时候故障就发生了。
二、故障经过
故障发生在一个周五的晚上8点左右,正是业务高峰的时候。
首先是监控系统告警,说Redis集群的延迟升高了,从原来的1ms以内升到了10ms以上,而且还在继续升高。紧接着业务方也反馈说网站和APP变慢了很多,用户投诉说页面打不开或者加载很慢。
我们赶紧登录Redis节点查看情况,发现几个Redis节点的CPU使用率非常高,达到了100%,而且主要是用户态CPU高,不是系统态也不是IO等待。同时Redis的连接数也很高,达到了几万,比平时高很多,而且有很多连接在等待响应。
我们一开始以为是业务量突然增大导致的,但是看了业务的QPS和平时差不多,没有明显增长,所以不是业务量的问题。然后我们怀疑是多线程IO的问题,因为之前用Redis 5.0单线程的时候从来没出现过这种情况,升级到6.0开启多线程之后才出现的。
我们赶紧先把几个CPU高的节点的多线程IO关掉,改成单线程模式,看看会不会好一些,但是关掉之后CPU还是很高,延迟也没有降下来,情况没有好转。
这时候故障已经持续了一个多小时了,业务影响很大,老板也在催,我们压力很大,但是还是没找到根本原因。
后来我们仔细看了Redis的慢查询日志,发现有很多慢查询都是一些大key的操作,比如hgetall、smembers等操作,一个key里有几十万甚至上百万的元素,操作一次要几十毫秒甚至上百毫秒。
我们突然意识到可能是大key的问题。在单线程模式下,大key的操作虽然也慢,但是因为是单线程一个一个处理,影响相对可控,但是在多线程IO模式下,虽然网络IO是多线程的,但是命令执行还是单线程的,大key的操作会阻塞主线程,导致其他命令都在等待,而且多线程IO会同时读入更多的命令堆积在队列里,反而加重了阻塞的影响。
而且我们发现有一个业务在高峰的时候会频繁地对几个大key做hgetall操作,这几个key每个都有几十万的field,一次hgetall要返回大量的数据,在多线程IO模式下这些大数据的网络发送也会占用大量的IO线程资源,导致其他连接的IO处理不过来。
找到原因之后我们赶紧采取了措施,首先把这几个大key的操作临时禁掉,让业务方改用其他方式获取数据,然后把所有节点的多线程IO都关掉回退到单线程模式,同时清理了一些过期的大key。
做完这些操作之后大概过了十几分钟,Redis的CPU慢慢降下来了,延迟也恢复到了正常水平,业务也恢复了正常。这时候故障已经持续了快三个小时了,真的是一次惊心动魄的经历。
三、故障原因分析
故障恢复之后我们做了详细的原因分析,总结了这次故障的根本原因和诱因。
根本原因是大key操作阻塞主线程。这次故障的根本原因是存在几个大key,业务在高峰时频繁对这些大key做hgetall等操作,这些操作耗时长,阻塞了Redis的主线程,导致其他命令都在等待,延迟升高。
Redis虽然6.0支持多线程IO,但是多线程只是处理网络IO的读写,命令的执行还是单线程的,所以大key的操作依然会阻塞主线程,影响所有命令的执行。而且多线程IO反而加重了这个问题,因为多线程IO能同时处理更多的连接,读入更多的命令,这些命令都堆积在命令队列里等待主线程执行,一旦主线程被大key操作阻塞,队列里的命令就会越积越多,延迟越来越高,形成恶性循环。
诱因一是多线程IO开启后连接数增加。开启多线程IO之后Redis能处理更多的连接,所以连接数比之前高了很多,这些连接在高峰时都在发送命令,导致命令队列堆积更严重。
诱因二是大key的数据量大,网络IO压力大。大key的hgetall操作会返回大量的数据,这些数据的网络发送需要占用IO线程的资源,在多线程IO模式下虽然有多个IO线程,但是大量的数据发送还是会占满IO线程,导致其他连接的IO处理不过来,延迟升高。
诱因三是测试环境没有覆盖这种场景。我们在测试环境做测试的时候没有这种大key频繁操作的场景,所以没有发现这个问题,到了生产环境业务高峰的时候才暴露出来。
诱因四是对Redis 6.0多线程的理解不够。我们之前对Redis 6.0多线程的理解不够,以为开启了多线程性能就一定会提升,所有场景都适用,但是实际上多线程IO只是提升网络IO的处理能力,对于大key操作这种CPU密集型的命令没有帮助,反而可能有反作用。
四、解决和优化措施
故障恢复之后我们采取了一系列的解决和优化措施,避免类似故障再次发生。
第一是清理和拆分大key。首先我们对整个Redis集群做了大key扫描,找出了所有大于10KB的key,然后逐个分析和处理。对于不需要的大key直接删除,对于需要的大key做拆分,比如把一个有几十万field的hash拆成多个小的hash,或者改成其他数据结构,避免单次操作返回大量数据。同时我们也制定了规范,禁止再创建大key,单个key的大小不能超过10KB,单个集合的元素不能超过5000个,业务开发的时候要遵守这个规范。
第二是优化大key的操作。对于确实需要操作大key的场景我们做了优化,比如不用hgetall一次性取所有数据,而是用hscan分批取,或者只取需要的field,避免一次性返回大量数据。同时我们也把一些大key的数据迁移到了其他存储,比如MySQL或者MongoDB,不放在Redis里,避免影响Redis的性能。
第三是合理配置多线程IO。我们重新评估了多线程IO的配置,根据每个节点的业务情况决定是否开启多线程IO以及IO线程的数量。对于网络IO密集型的节点,比如连接数多、数据量小的场景,开启多线程IO效果好;对于CPU密集型的节点,比如有很多复杂计算或者大key操作的场景,不开启多线程IO,用单线程更稳定。同时IO线程的数量也不是越多越好,一般设置为CPU核数的一半左右比较合适,太多了线程切换的开销会增大反而影响性能。
第四是加强监控和告警。我们加强了Redis的监控和告警,除了原来的延迟、QPS、内存等指标,还增加了大key监控、慢查询监控、命令队列长度监控等,一旦出现异常及时告警及时处理。同时我们也设置了更合理的告警阈值,避免告警太频繁或者太滞后,保证能及时发现问题。
第五是完善上线流程。我们完善了上线流程,对于Redis版本升级、配置变更等操作,要求必须在测试环境充分测试,覆盖各种场景,包括高峰场景、大key场景等,然后灰度上线,观察足够的时间没有问题才能全量上线。同时上线的时候必须有回滚方案,一旦出现问题能快速回滚,减少影响。
第六是加强团队培训。我们也加强了团队的培训,让大家更深入地理解Redis的原理和最佳实践,尤其是Redis 6.0多线程的原理和适用场景,避免因为理解不够而导致问题。
五、经验教训
这次故障给了我们很多经验教训,这里总结一下分享给大家。
第一,不要盲目追新,新版本不一定适合所有场景。Redis 6.0的多线程IO是一个很好的新特性,但是不是所有场景都适合开启,之前一定要充分测试,了解它的原理和适用场景,不要盲目追新,看到新版本就升级,看到新特性就开启。
第二,大key是Redis的大忌,一定要避免。大key是Redis的大忌,不管是单线程还是多线程,大key的操作都会阻塞主线程影响性能,所以一定要避免大key,定期扫描和清理大key,制定规范禁止创建大key。
第三,多线程不是银弹,要理解原理。多线程IO只是提升网络IO的处理能力,命令执行还是单线程的,所以对于CPU密集型的命令没有帮助,反而可能有反作用,要理解原理,根据场景合理使用。
第四,测试环境要尽量模拟生产场景。测试环境不能只是简单测一下功能,要尽量模拟生产的场景,包括数据量、业务量、高峰场景、异常场景等,这样才能发现潜在的问题,避免到了生产才出问题。
第五,上线一定要有回滚方案。任何上线操作都要有回滚方案,一旦出现问题能快速回滚减少影响,这次故障我们就是因为能快速关掉多线程IO回退到单线程,才没有造成更大的影响。
第六,监控要全面,告警要及时。监控一定要全面,不仅要监控基础指标,还要监控关键的业务指标和异常指标,告警要及时,能在问题发生的初期就发现,及时处理,避免问题扩大。
第七,故障发生时要冷静,先恢复再排查。故障发生的时候不要慌,要冷静,先想办法恢复业务,再慢慢排查原因,不要为了找根本原因而耽误了恢复的时间,业务恢复是第一位的。
六、写在最后
好了,关于这次Redis 6.0多线程故障的复盘就聊这么多。
总结一下,这次故障是因为大key操作阻塞主线程,加上多线程IO开启后连接数增加、数据发送量大,加重了阻塞,导致Redis延迟升高影响业务,经过三个小时的排查和处理最终恢复正常。
这次故障给了我们很多教训,也让我们对Redis的理解更深入了,后来我们做了一系列的优化和改进,现在Redis集群运行很稳定,没有再出现类似的问题。
最后想说,技术升级是好事,能带来性能提升和新功能,但是也有风险,一定要谨慎对待,充分测试,合理使用,才能发挥新技术的优势,避免带来问题。希望这次故障复盘能给大家一些参考和启发,如果你也遇到过类似的Redis故障或者有其他的经验,欢迎在评论区交流讨论。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录