王晓波
同程旅行机票事业群 CTO
读完需要
12
分钟速读仅需 4 分钟
本章和大家分享一下同程凤凰缓存系统在基于 Redis 方面的设计与实践。在本章中除了会列举我们工作过程中遇到各种问题和误区外,还会给出我们相应的解决办法,希望能够抛砖引玉为大家带来一定的启示。
本文节选自中生代技术社区出品图书《深入分布式缓存》
1
同程凤凰缓存系统遇到的问题
2012 年~2014 年,我们的业务开始使用一种新的互联网销售模式——秒杀抢购,一时间,各个产品线开始纷纷加人,今天秒杀门票,明天秒杀酒店。各种活动轮番登场,用户在不亦乐乎地玩着秒杀活动的同时,也对后端技术的支撑提出了一波又一波的挑战。
在第一个秒杀抢购系统上线后不久,流量越来越大,发现不对了:只要秒杀抢购一开始,卡顿、打不开的故障就会此起彼伏。一旦出现故障,所有人都急得直跳脚,因为秒杀抢购流量一下而过,没有机会补救。其实问题也很简单,一个有点经验的兄弟就很快就将问题定位出来:抢购那一下太耗费服务器资源,在同一时间段内涌入的人数大大超过了服务器的负载,服务器根本承受不了,CPU 占用率很多时候都接近了 100%,请求的积压也很严重,从请求接人到数据的读取都有问题,尤以数据的读取最为严重。在原来的设计方式中虽然也考虑了大并发量下的数据读取,但是因为数据相对分散,读取时间相对拉长,不像秒杀抢购是对同一批或同一条数据进行超高并发的读取。当然秒杀抢购不仅仅是数据读取的集中并发,同时也是数据写入的集中并发。
问题是发现了,表面上看起来解决没那么简单。应用层的问题解决起来相对容易,实在不行多加点机器也能解决;但数据的问题就不是那么简单了,靠增加机器来解决是不行的。大部分关系型数据库没有真正的分布式解决方案,最多做一个主从分离或多加从库分担读取的压力。但因为秒杀抢购是数据集中式超高并发的读,所以一般的关系型数据库因为它本身局限性很难支撑这样瞬间突发的高并发,就算勉强顶上,也会因为秒杀抢购还有写的高并发,影响到读节点的数据同步问题。当然也可以拼命提升一下服务器的硬件性能,比如换最好的 CPU、把硬盘换成 SSD 等等,但效果应该不会太显著,没有解决本质的问题,还比较费钱。
其实寻找新的解决方案也很简单,因为在当时那个年代的开源社区中有很多的 NoSQL 明星产品(如 Redis 等等)方案,这些方案也都提供了丰富的数据类型,拥有原子性操作和强大的并发性能特性,感觉简直就是为抢购量身定做的。于是我们也基于此做了一些方案,例如:数据在抢购活动开始前被先放到 NoSQL 数据库里,产生的订单数据先被放到队列中,
然后通过队列慢慢消化……这一系列的操作解决了抢购的问题,这里主要不是讲抢购技术方案,我们不再细化下去。
其实这样的解决方案在技术蛮荒时代还是相对靠谱的,在我们技术强壮的今天,这个方案还是单薄和弱小了一些,但是所有的技术点都是这样一路走来的。下面我们来看一下,从弱小走向长大,经历了哪些?
1.1
Redis 用法的凌乱
从运维角度来想,Redis 是很简单的东西,安装一下,配置一下,就轻松上线了,再加上 Redis 的一些单进程、单线程等特性,可以很稳定地给到应用层去随便使用。就像早期的我们,在很短的时间内,Redis 实例部署达到了千个以上,用得多了真正的问题开始出现。什么问题?乱的问题。Redis 从使用的角度来讲是需要像应用服务一样去治理的。为什么是需要治理的?我们先来看一些常见的运维与开发的聊天记录,大家会不会有一些风趣的感觉:
开发:“Redis 为啥不能访问了?”
运维:“刚刚服务器内存坏了,服务器自动重启了。”
开发:“为什么 Redis 延迟这么久?”
运维:“大哥,不要在 Zset 里面放几万条数据,插入排序的后果很严重啊!”
开发:“我写进去的 key 呢,为什么不见了?”
运维:“你的 Redis 超过最大大小了,不常用的 key 都丢了呀!”
开发:“刚刚为啥读取全部失败了?”
运维:“刚刚网络临时中断了一下,slave 全同步了,在全同步完成之前,slave 的读取全部失败。”
开发:“我刚刚想到一个好方案,我需要 800GB 的 Redis,什么时候能准备好呢?”
运维:“大哥,我们线上的服务器最大也就 256GB,别玩这么大好吗?”
光看这么一小点就感觉问题很多了,开发和运维都疲于奔命地解决这些看上去很无聊的问题。这些问题从本质上来讲还只是麻烦,谈不上困难。但是每当这些麻烦演变成一次 Redis 的故障时,哪怕是小故障,有时也会造成大痛苦,因为毕竟保存在内存里的数据太脆弱了,一不小心数据就会全部消失了。为此,当时也是绞尽脑汁,想了很多种办法
单机不是不安全吗?那么就开启主从+Keepalived,用虚 IP 地址在 master 和 slave 两边漂移,master 挂了直接切换到 slave。
数据放内存不是不安全吗?可以开启数据落盘,根据业务需要决定落盘规则,有 AOF 的,也有 RDB 的。
使用上不是有问题吗?那么多开几场培训,跟大家讲讲 Redis 的用法和规范。
以上策略在当时似乎很完美,但是没多久,均宣告失败,这是必然的。
为什么呢?先看那个主从+Keepalived 的方案,这本来是个很好的方案,但是忽略了主数据节点挂掉的情况。我们在前面说过,Redis 的单进程、单线程设计是其简单和稳定的基石,只要不是服务器发生了故障,在一般情况下是不会挂的。
但同时,单进程、单线程的设计会导致 Redis 接收到复杂指令时会忙于计算而停止响应,可能就因为一个 Zset 或者 keys 之类的指令,Redis 计算时间稍长,Keepalived 就认为其停止了响应,直接更改虚 IP 的指向,然后做一次主从切换。过不了多久,Zset 和 keys 之类的指令又会从客户端发送过来,于是从机上又开始堵塞,Keepalived 就一直在主从机之间不断地切换 IP。终于主节点和从节点都堵了,Keepalived 发现后,居然直接将虚 IP 释放了,然后所有的客户端都无法连接 Redis 了,只能等运维到线上手工绑定才行。
数据落盘也引起了很大的问题,RDB 属于非阻塞式的持久化,它会创建一个子进程来专门把内存中的数据写人 RDB 文件里,同时主进程可以处理来自客户端的命令请求。但子进程内的数据相当于是父进程的一个拷贝,这相当于两个相同大小的 Redis 进程在系统上运行,会造成内存使用率的大幅增加。如果在服务器内存本身就比较紧张的情况下再进行 RDB 配置,内存占用率就会很容易达到 100%,继而开启虚拟内存和进行磁盘交换,然后整个 Redis 的服务性能就直线下降了。
另外,Zset、发布订阅、消息队列、Redis 的各种功能不断被介绍,开发者们也在利用这些特性,开发各种应用,但从来没想过这么一个小小的 Redis 有这么多新奇的功能,它的缺点在什么地方,什么样的场景是不合适用的?
这时 Redis 在大部分的开发者手上就是像是一把锤子,看什么都是钉子,随时都一锤了事。同时也会渐渐地淡忘了开发的一些细节点和规范,因为用它解决性能的问题是那么轻松简单,于是一些基于 Redis 的新奇功能就接连不断地出现了:基于 Redis 的分布式锁、日志系统、消息队列、数据清洗,等等,各种各样的功能不断上线使用,从而引发了各种各样的问题。这时候原来那个救火神器就会变成
四处点火的神器,Redis 堵塞、网卡打爆、连接数爆表等问题层出不穷,经过这么多折腾,Redis 终于也变成了大家的噩梦了。
1.2
从实际案例再看 Redis 的使用
第一个案例
在一个炎热的夏天,引爆了埋藏已久的大炸弹。首先是一个产品线开发人员搭建起了一套庞大的价格存储系统,底层是关系型数据库,只用来处理一些事务性的操作和存放一些基础数据;在关系型数据库的上面还有一套 MongoDB,因为 MongoDB 的文档型数据结构,让他们用起来很顺手,同时也可以支撑一定量的并发。在大部分情况下,一次大数据量的计算后结果可以重用但会出现细节数据的频繁更新,所以他们又在 MongoDB 上搭建了一层 Redis 的缓存,这样就形成了数据库→ MongoDB → Redis 三级的方式,对方案本身不评价,因为这不是本文重点,我们来看 Redis 这层的情况。由于数据量巨大,所以需要 200GB 的 Redis。并且在真实的调用过程中,Redis 是请求量最大的点,当然如果 Redis 有故障时,也会有备用方案,从后面的 MongoDB 和数据库中重新加载数据到 Redis,就是这么一套简单的方案上线了。
当这个系统刚开始运行的时候,一切都还安好,只是运维同学有点傻眼了,用 200GB 的 Redis 单服务器去做,它的故障可能性太大了,所以大家建议将它分片,没分不知道,一分吓一跳,各种类型用得太多了,特别是里面还有一些类似消息队列使用的场景。由于开发同学对 Redis 使用的注意点关注不够,一味地滥用,一锤了事,所以让事情变得困难了。
有些侥幸不死的想法是会传染的,这时的每个人都心存侥幸、懒惰心理,都想着:“这个应该没事,以后再说吧,先做个主从,挂了就起从”,这种侥幸也是对 Redis 的虚伪的信心,无知者无畏。可惜事情往往就是怕什么来什么,在大家快乐并放肆地使用时,系统中重要的节点 MongoDB 由于系统内核版本的 BUG,造成整个 Mongodb 集群挂了!(这里不多说 MongoDB 的事情,这也是一个好玩的“哭器”)。当然对天天与故障为朋友的运维同学来说这个没什么,对整个系统来说问题也不大,因为大部分请求调用都是在最上层的 Redis 中完成的,只要做一定降级就行,等拉起了 MongoDB 集群后自然就会好了。
但此时可别忘了那个 Redis,是一个 200GB 大的 Redis,更是带了个从机的 Redis,所以这时的 Redis 是绝对不能出任何问题的,一旦有故障,所有请求会立即全部打向最底层的关系型数据库,在如此大量的压力下,数据库瞬间就会瘫痪。但是,怕什么来什么,还是出了状况:主从 Redis 之间的网络出现了一点小动荡,想想这么大的一个东西在主从同步,一旦网络动荡了一下下,会怎么样呢?主从同步失败,同步失败就直接开启全同步,于是 200GB 的 Redis 瞬间开始全同步,网卡瞬间打满。为了保证 Redis 能够继续提供服务,运维同学,直接关掉从机,主从同步不存在了,流量也恢复正常。不过,主从的备份架构变成了单机 Redis,心还是悬着的。俗话说,福无双至,祸不单行。这 Redis 由于下层降级的原因并发操作量每秒增加到 4 万多,AOF 和 RDB 库明显扛不住。同样为了保证能持续地提供服务,运维同学也关掉了 AOF 和 RDB 的数据持久化。连最后的保护也没有了(其实这个保护本来也没用,200GB 的 Redis 恢复太大了)。
至此,这个 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 客户端连接数爆了。
... ...
(未完待续)
想要加入中生代架构群的小伙伴,请添加群合伙人大白的微信
申请备注(姓名+公司+技术方向)才能通过哦!
阿里技术精彩文章推荐
往期推荐
深度:揭秘阿里巴巴的客群画像
多隆:从工程师到阿里巴巴合伙人
阿里技术专家楚衡:架构制图的工具与方法论
蚂蚁集团技术专家山丘:性能优化常见压测模型及优缺点
阿里文娱技术专家战獒: 领域驱动设计详解之What, Why, How?
阿里专家马飞翔:一文读懂架构整洁之道
阿里专家常昊:新人如何上手项目管理?
蚂蚁集团沈凋墨:Kubernetes-微内核的分布式操作系统
阿里合伙人范禹:常挂在阿里技术人嘴边的四句土话
阿里技术专家都铎:一文搞懂技术债
支付宝研究员兼OceanBase总架构师杨传辉:我在数据库梦之队的十年成长路
阿里技术专家麒烨:修炼测试基本功
阿里计算平台掌门人贾扬清:我对人工智能方向的一点浅见
蚂蚁资深算法专家周俊:从原理到落地,支付宝如何打造保护隐私的共享智能?
阿里高级技术专家箫逸:如何画好一张架构图?
阿里高级技术专家张建飞:应用架构分离业务逻辑和技术细节之道
蚂蚁科技 Service Mesh 落地实践与挑战 | GIAC 实录
阿里6年,我的技术蜕变之路!
蚂蚁集团涵畅:再启程,Service Mesh 前路虽长,尤可期许
阿里P9专家右军:大话软件质量稳定性
阿里合伙人程立:阿里15年,我撕掉了身上两个标签
阿里高工流生 | 云原生时代的 DevOps 之道
阿里高级技术专家邱小侠:微服务架构的理论基础 - 康威定律
阿里P9专家右军:以终为始的架构设计
阿里P8架构师:淘宝技术架构从1.0到4.0的架构变迁!12页PPT详解
阿里技术:如何画出一张合格的技术架构图?
蚂蚁资深技术专家王旭:开源项目是如何让这个世界更安全的?
阿里资深技术专家崮德:8 个影响我职业生涯的重要技能
儒枭:我看技术人的成长路径
阿里高级技术专家宋意:平凡人在阿里十年的成长之旅
阿里技术专家甘盘:浅谈双十一背后的支付宝LDC架构和其CAP分析
阿里技术专家光锥:亿级长连网关的云原生演进之路
阿里云原生张羽辰:服务发现技术选型那点事儿
蚂蚁研究员玉伯:做一个简单自由有爱的技术人
阿里高级技术专家至简: Service Mesh 在超大规模场景下的落地挑战
阿里巴巴山猎:手把手教你玩转全链路监控
阿里涉江:你真的会学习吗?从结构化思维说起
蚂蚁金服资深技术专家经国:云原生时代微服务的高可用架构设计
深入分布式缓存之EVCache探秘开局篇
END
#架构师必备#
点分享点点赞点在看