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

运维和开发都陷入了使用Redis的误区,真的不是开玩笑的……

时间:2023-03-21 18:55:54 科技观察

炎炎夏日,一颗埋藏已久的大炸弹被引爆了。案发后,伊图格所在单位的产品线开发人员建立了庞大的价格存储系统。底层是关系型数据库,只用来处理一些事务性操作,存储一些基础数据。在关系型数据库之上还有一套MongoDB,因为MongoDB的文档型数据结构使得它们使用方便,也可以支持一定的并发量。大多数情况下,数据量大的计算结果可以复用,但详细数据会经常更新,所以他们在MongoDB上构建了一层Redis缓存。这样就形成了数据库→MongoDB→Redis的三级途径。不评估解决方案本身不是本文的重点。再来看Redis层的情况。由于数据量巨大,需要200GB的Redis,而在实际调用过程中,Redis是请求量最大的点。当然,如果Redis出现故障,也会有备份计划,将后续的MongoDB和数据库中的数据重新加载到Redis中。就是这样一个简单的解决方案,已经推出了。系统刚开始运行时,一切正常,运维同学却有些傻眼。如果使用200GB的Redis单机,失败的可能性太大了。所以大家建议还是分块。如果您不知道自己是否没有任何积分,您会大吃一惊。各种类型用的太多了,尤其是有一些类似消息队列使用的场景。因为开发同学对Redis的使用不够重视,一味的滥用,一锤子就搞定了,这就增加了难度。一些不死于运气的想法具有传染性。这时候大家就侥幸偷懒了,心想:“这个应该可以吧,以后再说吧,先做主从,挂了就拿到slave”,这种侥幸也是一种虚假的自信在Redis中,无知者无所畏惧。只可惜,怕的往往是事情。就在大家兴高采烈使用的时候,系统中的重要节点MongoDB却因为系统内核版本的bug,导致整个MongoDB集群挂掉!(MongoDB这里就不多说了,这也算是逗乐哭了)。当然这对于天天和故障为友的运维同学来说不是问题,对于整个系统来说也不是什么大问题,因为大部分的请求调用都是在顶层的Redis中完成的,只要需要一定的降级,拉起MongoDB集群后就可以了。但是这个时候不要忘了,Redis是一个200G的带slave的Redis。所以这个时候Redis肯定是没有问题的。一旦出现故障,所有的请求都会立即发送到最底层的关系数据库。在这么大的压力下,数据库会瞬间瘫痪。不过,还是有我怕的地方不对:Redis主从之间的网络有点乱。想想这么大一个东西的主从同步。一旦网络动荡会怎样?如果主从同步失败,如果同步失败,会直接开始全量同步,所以200GB的Redis瞬间开始全量同步,瞬间网卡就满了。为了保证Redis能够继续提供服务,运维同学直接关掉了从机,主从同步不存在,流量恢复正常。然而主从备份架构变成了单机版的Redis,我的心还是悬着。俗话说,福无双来,祸不单行。由于下层的退化,Redis的并发数已经增加到每秒4万多,AOF和RDB库显然无法应对。同样为了保证持续提供服务,运维同学还关闭了AOF和RDB的数据持久化,连最后的保护都没了(其实一开始这个保护也没用,200GBRedis的恢复太大了)。至此,这个Redis就变成了一个完整的单机内存类型,除了祈祷它不会挂掉,别无他法。这件事挂了很久,直到MongoDB集群搞定。这么侥幸,没有出什么大事,可是你心里会踏实吗?答案是不。问题分析本案例主要存在的问题是:对Redis的过度依赖,Redis看似给系统带来了简单方便的性能提升和稳定性,但在使用中没有对不同场景的数据进行分离,造成了逻辑单点问题。当然,我们可以通过更合理的应用架构设计来解决这个问题,但是这种方案不够优雅和彻底,同时也增加了应用层架构设计的麻烦。Redis的问题应该在基础缓存层解决,这样即使有类似情况也不会有问题。因为基础缓存层能够适应这样的使用方式,也会让应用层的设计变得更简单(简单一直是架构设计的追求,Redis本身的大量使用也是追求简单的副产品,那为什么我们不把这个简单的?变成真实的呢?)说完第二种情况,再来看第二种情况。某部门利用现有的Redis服务器搭建了一个日志系统,将日志数据先存储在Redis中,然后通过其他程序读取数据并进行处理。分析和计算用于制作数据报告。在他们完成这个项目后,这个日志组件让他们觉得用起来非常愉快。他们都认为这是一个好办法。他们可以轻松地记录日志并非常快速地对其进行分析。他们使用的是什么公司的分布式日志服务?随着时间的推移,这个Redis上已经悄悄挂载了上千个客户端,每秒上万个并发,系统单核CPU占用率接近90%。这个时候这个Redis已经开始不堪重负了。终于,压死骆驼的最后一根稻草来了。一个程序向这个日志组件写了一个7MB的日志(哈哈,这个容量可以写小说了,这是什么日志)。所以Redis被屏蔽了。一旦被阻止,数以千计的客户端将无法连接,所有日志记录操作都将失败。其实日志记录失败本身应该不会影响正常业务,只是因为这个日志服务不是公司标准的分布式日志服务,很少有人关注。一开始写的开发同学不知道会有这么大的使用量,运维同学更不知道有这个非法日志服务的存在。服务本身没有很好的容错设计,所以直接在记录日志的地方抛出异常。致使公司相当一部分业务系统出现故障,监控系统“5XX”错误直线上升。一群人欲哭无泪,排查问题压力巨大。但由于灾害范围广泛,排查压力可想而知。问题分析这种情况好像是一个日志服务没有做好或者开发过程管理不到位,而且很多日志服务也是使用Redis作为收集数据的缓冲区,貌似没有问题。事实上,这样一个大规模、高流量的日志系统,从收集到分析,都需要仔细考虑技术点,而不仅仅是简单的写入性能问题。在这种情况下,Redis为程序带来了一种超简单的性能解决方案,但这种简单是相对的,它有场景的局限性。在这里,这样的简单是毒药,无知的人无知地吃了会自杀。这就好比“小河沟里一条万能嚣张的小鱼,因为它没见过海,到了海里……”。本案还有一个问题:非法日志服务的存在,表面上是管理问题,本质上是技术问题。因为Redis的使用不能像关系型数据库那样由DBA监督,它的操作者无法管理和提前知道里面存储了哪些数据,开发者可以将数据写入Redis并在没有任何声明的情况下使用。所以这里我们发现如果不对这些场景进行管理,Redis的使用在长期使用中很容易失控。我们需要一个透明层来管理和控制Redis的使用。两个小例子,在Redis被乱用的年代,用过的兄弟想必都吃过苦头,饱受各种故障的轰炸:Redis被Keys命令阻塞;keepalived切换虚拟IP失败,虚拟IP被释放;使用Redis进行计算,Redis的CPU占用率变为100%;主从同步失败;Redis客户端连接数激增;…….如何改变Redis用不好的误区?这种乱象肯定是不可能再继续下去了,至少在涂哥工作的单位,已经不能再继续这种使用方式了,用户已经开始从喜欢变成痛苦。该怎么办?这是一件很沉重的事情:“乱成一锅粥的制度,就像一桌烧焦的菜,你很难再回到灶台上,让人拍手称快。”关键是已经这样用过了。不可能把所有系统都停掉,等新系统上线瞬间切换好吗?这是一份怎样的工作:“在高速公路上换轮胎。”然而,问题总是需要解决的。经过思考和讨论,我们可以总结出以下几点:必须建立完整的监控体系,在此之前必须进行预警。我们不能等到它发生了才发现问题;控制和引导Redis的使用,我们需要有一个自己开发的Redis客户端,在使用的时候开始控制和引导;Redis的一些角色需要改变,Redis应该从Storage的角色降低到Cache的角色;Redis的持久化方案需要重做,需要开发基于Redis协议的持久化方案,让用户可以把Redis当作DB使用;Redis的高可用应该根据场景分离,根据不同的场景采用不同的高可用方案。留给开发同学的时间不多了,只有两个月的时间来完成这些事情。这件事还是很有挑战性的,考验开发同学能不能换轮胎的时候到了。同学们开始开发自己的Redis缓存系统。我们先来看代号为Phoenix的第一版缓存系统:首先是监控系统。原来开源的Redis监控只是笼统的一些监控工具,并不能算是一个完整的监控系统。当然,这个监控是对从客户端到返回数据的整个链路的全方位监控;二是改造Redis客户端。目前广泛使用的Redis客户端有的过于简单,有的过于繁重,总之不是我们想要的。比如.Net下的BookSleeve和servicestack.Redis(同进程中也有一些老的.Net开发的应用),前者长期没有维护,后者直接收费。好吧,我们将只构建一个客户端并推动全公司的研发以用它替换当前的客户端。在这个客户端中,我们植入了日志记录,记录了代码对Redis的所有操作事件,比如耗时、key、value大小、网络断开等,我们在后台收集这些有问题的事件,并进行分析处理通过收集程序。同时取消IP端口直连方式,通过配置中心分配IP地址和端口。当Redis出现问题需要切换时,可以直接在配置中心修改,配置中心会将新的配置推送给客户端,省去了业务员需要修改配置文件的麻烦切换Redis。另外Redis的命令操作分为两部分:安全命令,可以直接使用安全命令;unsafecommands,开启前需要分析通过,同样由配置中心控制。解决了开发者使用Redis时的规范问题,将Redis定位为缓存角色。除非有特殊要求,否则将被视为缓存角色。最后,Redis的部署方式也进行了修改。以前是keepalived方式,现在换成了master-slave+sentinel方式。此外,我们自己实现了Redis分片。如果业务需要申请大容量的Redis数据库,Redis会被拆分成多块,通过Hash算法来平衡每块的大小。这种分片对应用层也是不敏感的。的。当然client-heavy的方式不好,我们要做的是缓存,而不仅仅是Redis,所以我们会做一个RedisProxy来提供统一的入口。Proxy可以多副本部署,客户端无论连接哪个Proxy都可以获得完整的集群数据。这样就基本完成了根据场景选择不同部署方式的问题。这样的Proxy也解决了多种开发语言的问题。比如运维系统是用Python开发的,也需要用到Redis,所以可以直接对接Proxy,再对接统一的Redis系统。无论是客户端还是代理,都不仅仅是为了代理请求,而是为了统一管理Redis缓存的使用,防止混乱。让缓存在可管可控的场景下稳定运行和维护,让开发者可以继续安全、肆无忌惮地使用Redis,但这种“乱”是虚拟化的乱,因为它的底层是可以管理的。系统架构图当然,以上修改需要在不影响业务的情况下进行。实现这一目标仍有许多挑战,尤其是碎片化。将一个Redis拆分成多个,也可以让客户端正确找到需要的Key。这需要非常小心,因为一不小心,内存中的数据就会全部消失。这段时间我们开发了多种同步工具,几乎实现了Redis的整个主从协议,终于可以顺利的将Redis过渡到新的模式。PS:大家可能会有这样的疑问,为什么不用Redis的集群模式呢?我们线上的情况多是2.X和3.X版本,2.X也在减少很多。proxy的加入不是为了简单的分片,而是为了更多的其他功能,比如单key的高人气等等,总的来说,我们做的是一个私有的缓存云,而不仅仅是一个缓存管理容器。