本文和大家分享一下同程凤凰缓存系统在基于Redis方面的设计与实践。在本文中除了会列举我们工作过程中遇到各种问题和误区外,还会给出我们相应的解决办法,希望能够抛砖引玉为大家带来一定的启示。
同程凤凰缓存系统要解决什么问题
2012年~2014年,我们的业务开始使用一种新的互联网销售模式——秒杀抢购,一时间,各个产品线开始纷纷加入,今天秒杀门票,明天秒杀酒店。各种活动轮番登场,用户在不亦乐乎的玩着秒杀活动的同时,也对后端技术的支撑提出了一波又一波的挑战。
在第一个秒杀抢购系统上线后不久,流量越来越大,发现不对了:只要秒杀抢购一开始,卡顿、打不开的故障就会此起彼伏。一旦出现故障,所有人都急得直跳脚,因为秒杀抢购流量一下而过,没有机会补救。其实问题也很简单,一个有点经验的兄弟就很快就将问题定位出来:抢购那一下太耗费服务器资源,在同一时间段内涌入的人数大大超过了服务器的负载,服务器根本承受不了,CPU占用率很多时候接近了100%,请求的积压也很严重,从请求接入到数据的读取都有问题,尤以数据的读取最为严重。在原来的设计方式中虽然也考虑了大并发量下的数据读取,但是因为数据相对分散,读取时间相对拉长,不像秒杀抢购是对同一批或同一条数据进行超高并发的读取。当然秒杀抢购不仅仅是数据读取的集中并发,同时也是数据写入的集中并发。
问题是发现了,表面上看起来解决没那么简单。应用层的问题的解决起来相对容易,实在不行多加点机器也能解决;但数据的问题就不是那么简单了,靠增加机器来解决是不行的。大部分关系型数据库没有真正的分布式解决方案,最多做一个主从分离或多加从库分担读取的压力。但因为秒杀抢购是数据集中式超高并发的读,所以一般的关系型数据库因为他本身局限性很难支撑这样瞬间突发的高并发。就算勉强顶上,也会因为秒杀抢购还有比如换最好的CPU、把硬盘换成SSD等等,但效果应该不会太显著,没有解决本质的问题,还比较费钱。
其实寻找新的解决方案也很简单,因为在当时那个年代的开源社区中有很多的NoSQL明星产品(如Redis等等)方案,这些方案也都提供了丰富的数据类型,拥有原子性操作和强大的并发性能特性,感觉简直就是为抢购量身定做的。于是我们也基于此做了一些方案,例如:数据在抢购活动开始前被先放到NoSQL数据库里,产生的订单数据先被放到队列中,然后通过队列慢慢消化......这一系列的操作解决了抢购的问题,这里主要不是讲抢购技术方案,我们不再细化下去。
其实这样的解决方案在技术蛮荒时代还是相对靠谱的,在我们技术强壮的今天,这个方案还是单薄和弱小了一些,但是所有的技术点都是这样一路走来的。下面我们来看一下,从弱小走向长大,经历了哪些。
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终于也变成了大家的噩梦了。
从实际案例再看Redis的使用
在一个炎热的夏天,引爆了埋藏已久的大炸弹。首先是一个产品线开发人员搭建起了一套庞大的价格存储系统,底层是关系型数据库,只用来处理一些事务性的操作和存放一些基础数据;在关系型数据库的上面还有一套MongoDB,因为MongoDB的文档型数据结构,让他们用起来很顺手,同时也可以支撑一定量的并发。在大部分情况下,一次大数据量的计算后结果可以重用但会出现细节数据的频繁更新,所以他们又在MongoDB上搭建了一层Redis的缓存,这样就形成了数据库→MongDB→Redis三级的方式,对方案本身先不评价,因为这不是本文重点,我们来看Redis这层的情况。由于数据量巨大,所以需要200GB的Redis。并且在真实的调用过程中,Redis是请求量最大的点,当然如果是Redis有故障时,也会有备用方案,从后面的MongoDB和数据库中重新加载数据到Redis,就是这么一套简单的方案上线了。
当这个系统刚开始运行的时候,一切都还安好,只是运维同学有点傻眼了,用200GB的Redis单服务器去做,他的故障可能性太大了,所以大家建议将他分片,没分不知道一分吓一跳,各种类型用得太多了,特别是里面还有一些类似消息队列使用的场景。由于开发同学对Redis使用的注意点关注不够,一味的滥用,一锤了事,所以让事情变得困难了。有些侥幸不死的想法是会传染的,这时的每个人都心存侥幸、懒惰心理,都想着:“这个应该没事,以后再说吧,先做了主从,挂了就启从”,这种侥幸也是对Redis的虚伪的信息,无知者无畏。可惜事情往往就是怕什么来什么,在大家快乐并放肆地使用时,系统中重要的节点MongoDB由于系统内核版本的BUG,造成整个MongoDB集群挂了!(这里不多说MongoDB的事情,这也是一个好玩的“哭器”)。当然对天天与故障为朋友的运维同学来说这个没什么,对整个系统来说问题也不大,因为大部分请求调用都是在最上层的Redis中完成的,只要做一定降级就行,等拉起了MongDB集群后自然就会好了。
但此时可别忘了那个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客户端连接数爆了。
- ......
如何改变Redis用不好的误区
这样的乱象一定是不可能继续了,最少同程这样的使用方式不可以再继续了,使用者也开始从喜欢到痛苦了。怎么办?这是一个很沉重的事:“一个被人用乱的系统就像一桌烧坏的菜,让你重新回炉,还让人叫好,是很困难的”。关键是已经用过这样了,总不可能让所有系统都停下来,等待新系统上线并瞬间切换吧?这是个什么活?高速公路上换轮胎。
但问题出现了总是要解决的,想了再想,论了再论,总结了以下几点:
- 必须搭建完善的监控系统,在这之前要先预警,不能等到发生了,我们才发现问题。
- 控制和引导Redis使用的使用,我们需要有自己研发的Redis客户端,在使用时就开始控制和引导。
- Redis的部分角色要改,将Redis的storage角色降低为cache角色。
- Redis的持久化方案要重新做,需要自己研发一个基于Redis协议的持久化方案,让使用者可以把Redis当DB用。
- Redis的高可用要按照场景分开,根据不同的场景决定采用不同的高可用方案。
留给开发同学的时间并不多,必须两个月的时间来完成这些事情。这其实还是很有挑战的,考验开发同学这个 轮胎到底能不换下来的时候到了。同学们开始研发我们自己的Redis缓存系统,下面我们来看一下这个代号为凤凰的缓存系统的第一版方案。
首先是监控系统。原有的开源Redis监控从大面上讲只一些监控工具,不能算作一个完整的监控系统。当然这个监控是全方位从客户端开始一直到返回数据的全链路的监控。
其次是改造Redis客户端。广泛使用的Redis客户端有的太简单有的太重,总之不是我们想要的东西,比如,.Net下的BookSleeve和servicestack.Redis(同程还有一点老的.Net开发的应用),前者已经好久没人维护了,后者直接收费了。好吧,我们就开发一个客户端,然后督促全公司的研发用他来替换目前正在使用的客户端。在这个客户端里面,我们植入了日志记录,记录了代码对Redis的所有操作事件,例如耗时、key、value大小、网络断开等,我们将这些有问题的事件在后台进行收集,由一个收集程序进行分析和处理,同时取消了直接的IP端口连接方式,通过一个配置中心分配IP地址和端口。当Redis发生问题并需要切换时,直接在配置中心修改,由配置中心推送新的配置到客户端,这样就免去了Redis切换时需要运维人员修改配置文件的麻烦。另外,把Redis的命令操作分拆成两部分:安全的命令和不安全的命令。对于安全的命令可以直接使用,对于不安全的命令需要分析和审批后才能打开,这也是由配置中心控制的,这样就解决了研发人员使用Redis时的规范问题,并且将Redis定位为缓存角色,除非有特殊需求,否则一律以缓存角色对待。
最后,对Redis的部署方式也进行了修改,以前是Keepalived的方式,现在换成了主从+哨兵的模式。另外,我们自己实现了Redis的分片,如果业务需要申请大容量的Redis数据库,就会把Redis拆分成多片,通过Hash算法均衡每片的大小,这样的分片对应用层也是无感知的。
当然重客户端方式不好,并且我们要做的缓存不仅仅是单纯的Redis,我们还会做一个Redis的Proxy,提供统一的入口点,Proxy可以多份部署,客户端无论连接的是哪个Proxy,都能取得完整的集群数据,这样就基本完成了按场景选择不同的部署方式的问题。这样的一个Proxy也解决了多种开发语言的问题。例如,运维系统是使用Python开发的,也需要用到Redis,就可以直接连Proxy,然后接到统一的Redis体系中来。做客户端也好,做Proxy也好,不只是为代理请求而是为了统一治理Redis缓存的使用,不让乱象出现。让缓存在一个可管可控的场景下稳定的运维,让开发者可以安全并肆无忌惮继续乱用Redis,但这个“乱”是被虚拟化的乱,因为他的底层是可以治理的。系统架构如下图所示。
当然以上这些改造都需要在不影响业务的情况下进行。实现这个其实还是有不小的挑战,特别是分片,将一个Redis拆分成多个,还能让客户端正确找到所需要的key,这需要非常小心,因为稍有不慎,内存的数据就全部消失了。在这段时间里,我们开发了多种同步工具,几乎把Redis的主从协议整个实现了一遍,终于可以将Redis平滑过渡到新的模式上了。
凤凰缓存系统对Redis系统化改造
当全公司的Redis在快速平稳的过渡新开发的缓存系统之后,这个方案经过了各种考验,,证明了整体思路是对的,所以是时候大张旗鼓的开始做他的第二个版本——完整全套的缓存体系架构平台。特别是运维部分,因为在第一版中运维部份做的太少。当然每个系统都有一个开发代号,这个缓存系统的代号就是“凤凰”,其中的含义很明显:之前的Redis总是死掉,现在的Redis是不死的,就像凤凰一样。
在整体的平台化过程中为了更好的扩容、弹性和运维,我们决定基于Docker将他改造成一个云化的缓存系统,当然要能被称之为云,那么平台最基本的要求就是具备资源计算、资源调度功能,且资源分配无需人工参与。对用户来讲,拿到的应该是直接可用的资源,并且天生自带高可用、自动备份、监控告警、日志分析等功能,无需用户关系资源背后的事情。其次是各种日常的运维操作需求服务化输出。下面我们就来讲讲我们这个平台是如何一步步实现的。任何项目都可以接入这个缓存系统,并从里面获取资源。使用只需一个申请,给他的场景名字就能使用了,不需要知道缓存的具体位置,也不需要知道缓存的具体大小,更不需要关注具体的流量,一切都交给云化管理。
我们进一步改造了监控系统,将其改造为一个完整的监控系统,这个系统会不断的收集整个云环境中的服务器和Redis等各方面的相关信息,根据具体的阈值报警,并且通知到运维系统,运维系统再基于运维数据来自动故障转移、扩容等等,到目前为止,整个缓存系统中5000多个Redis实例,每月的运维人力平均不到0.2个人,对于整个缓存系统来说只要向其中加入物理服务器就可以完成扩容,剩下的事全部由系统自主完成。
开发的另一个重点——调度系统,这个系统会不断地分析前面提到的监控系统搜集到的数据,对整个云进行微调:Redis的用量大了,会自动扩容;扩容空间不够了,会自动分片;网络流量大了,也会自动负载均衡。以前的Redis固定在一台服务器上,现在的Redis通过我们开发的同步工具,在各个服务器中流转,而且每个Redis都很小,不会超过8GB。备份和处理都不再是难事,现在要做的只是往这个集群里面注册一台机器,注册完之后,监控系统就会启动并获取监控信息,调度系统就会根据监控数据决定要不要在新机器上建立Redis。容量动态的数据迁移(集群内部平衡,新节点增加)和流量超出时的根据负载再平衡集群,也是这个调度系统在管理的。缓存平台的平滑扩容过程,如下图所示。
除了服务端的加强,同时在客户端上进一步下功夫,在支持Redis本身的特性的基础上,通过自定义来实现一些额外的功能。让客户端开始支持场景配置,我们考虑根据场景来管控Redis的使用内容,客户端每次用Redis的时候,必须把场景上报给系统。增加场景配置之后,在缓存服务的中心节点,就可以把他分开,同一个应用里面两个比较中哟啊的场景就会不用到同一个Redis,避免无法降级一个内容过于复杂的Redis实例。
同时也利用调度系统将场景的服务端分离,不同的场景数据工作在不同的Redis集群之上。另外在客户端里也可以增加本地cache的支持以提高性能和减少资源的使用。当然这些都是对应用层透明的,应用层不需要关心真正的数据源是什么。
缓存平台的客户端让结构如下图所示。
对于Proxy我们也进行了升级,新的Proxy完全实现了Redis的协议和其他的缓存服务协议,如Memcache。Proxy可以解决客户端过重的问题,在很多情况下,升级客户端是很难进行的事。再好的程序员写出来的东西还是可能会有bug的,如果客户端发现了一个bug需要紧急升级,我们不大可能一下升级线上几千个应用。因此我们的Proxy方案中也加入了之前在客户端做的很多事情,我们的想法就是让每一个项目的每一个开发者自己开发的代码是干净的,不要在他的代码里面嵌额外的东西,访问的就是一个Redis。把Redis本身沉在我们的Proxy之后,我们也可以做更多的改进了,如冷热区分方面,我们分析大量的Redis请求发现一些场景根本不需要用Redis,压力并不大,这样使用Redis是一种资源的浪费,所以通过Proxy直接将数据放到了RocksDB里面,用硬盘来支撑他。
缓存平台的代理设计如下图所示。
最后,把Redis的集群模式3.0纳入到整个体系里,并对3.0版本进行了一定的改造,替代了之前的分片技术,这样在迁移Redis时就更方便了,原生的Redis命令可以保证更好的稳定性。
整个凤凰缓存系统的架构如下图所示。
用好Redis先运维好他
我一直认为评价一款中间件的优劣,不能只评价他额本身。我们要综合他的周边生态是否健全,比如高可用方案、日常维护难度、人才储备等等。当然对于Redis也一样,正是因为Redis很小很简单所以相对缺乏好的辅助工具是一定的。我们在Redis基础上做的凤凰缓存系统也正是为解决Redis缺少的点。一款中间件从引入到用好其实最关键的点就是运维,只有运维好他才有可能用好他。这里我们就讲一下运维。
传统的Redis运维方式
讲了凤凰缓存系统的种种,再细细的看一下如何大批量运维Redis,通常的运维方式有两种:
统一大集群方式部署
这种部署是将所有的Redis集中在一起,形成一个超大的Redis集群,通过代理的方式统一对外提供连接,使外部看来就是个完整的超大Redis,所有的项目都共享这个Redis,在这个超大Redis内部,可以自动增添服务、修改配置等,而外部完全无感知。
这种方式的优点很多:
- 扩容比较方便,直接在集群内新增机器即可,使用者完全无感知。
- 利用率高,运维简单,只需要关注整个集群的大小就可以,不需要关心里面某个Redis的具体状态(特殊情况除外)。
- 客户端使用方便,无论哪个项目,面对的就是一组Redis,内部的细节对客户端来说是透明的,他们可以简单地认为链接上了一个内存无限大的Redis。
缺点也很明显:
- 扩容虽然方便,但是具体某个项目用了多少,无法获知,极端情况下有些项目超时设置不合理,写入数据大于过期和删除的数据,极有可能会导致整个Redis的内存一直增长直到用完。
- 整个超大的Redis的内部其实还是由各个小Redis组成的,每个key都单独存储到小的Redis内部,那么如果某个key很热,读取访问非常频繁,很有可能将某个小Redis的网卡打满,导致的结果就是1个分片直接不可用,而项目又是集体共享一组Redis,某个分片不可用,可能导致的结果就很难评估。
这种部署方式的典型代表有很多,他们会预分配1024个槽,并将这些槽分配到集群内的这些机器上去,还会提供一个大集群的Proxy,所有的客户端都可以通过这个Proxy读写。
多集群分散式部署
这种部署主要提供了自动化部署,部署的各个Redis相互独立,而一般情况下一组Redis也是单独供某个项目独占,隔离性非常好,不会因为某个项目的问题导致整个集群不可用,但是由于需要维护一大堆的Redis,各个Redis的情况又不一样,在自动化部署方面就显得比较麻烦。
优点:
- 隔离性非常好,各个项目之间互不影响,不会因为某个项目的动作导致整个集群受到影响。
- 灵活性高,针对各个项目的使用情况的不同可以定制不同的部署方式,可以最大化利用Redis。
缺点:
- 部署麻烦,要针对各个项目单独部署,各个项目的定制又各有不同。
- 客户端使用麻烦,不同的Redis有自己的IP、端口,经常容易搞混,当某个Redis需要停机维护时,又需要通知具体项目修改IP地址,非常麻烦。
同程的Redis的部署运维方式早期没有做平添时也是出现了各种各样的问题,大概总结如下:
- 部署忙不过来,每天都有很多项目申请Redis,每天部署的工作就消耗了很大部分时间。
- 如果服务器需要调整、修改,通知业务修改新的IP再重新发布,耗时往往很长,很多事情都耽误了。
- 服务器的资源利用率低,项目在使用Redis的时候,比较倾向于申请比较大的Redis,这样下次就不用再提要求增加容量了,而运维在某台服务器上部署了很多Redis后,为了防止内存撑爆,就不会在这台服务器上继续部署了,但实际上项目用的很少,这就造成了大量的浪费。
比如统一大集群方式有一些开源解决方案,看上去确实很不错,我们当时也决定尝试一下,结果很快出现了问题,由于一个大集群是供大家公用的,所以我们很难实时计算某个项目的配额,而某个项目插入过量数据后,又很容易将其他项目的数据挤掉(Redis配置了LRU策略),我们研究了很久,觉得各个项目公用,问题较多,不太好处理,只能采用第二种方式。那么问题回到了如何解决分布式部署方式缺点,我们经过研究,将问题总结如下:
- 部署问题,有没有自动化的部署方式,可以一键部署Redis。
- 监控问题,如何监控大量的Redis及其所在的主机,并提供运行状态查询和监控。
- 客户端使用问题,如何在修改和停机时,不需要客户端修改配置、重新上传这些步骤。
- 运维问题,Redis本身只是个很小的工具,代码部分很简单,缺少很多分析和运维工具。
Redis的Docker化部署
凤凰缓存系统这个分布式的Redis解决方案,通过Docker解决部署问题。Docker是一个非常好的自动化部署工具。一个新机器配置好后直接就能通过Docker的RESTful API进行操作,所以我们的运维就开发了一个基于Docker的自动化运维水平。用Docker就会想到kubernetes和swarm。但我们在做凤凰缓存时,是自己开发一个Docker调度系统。因为我们以Redis为主要发布对象,而像kubernetes的资源调度、均衡容灾、服务注册、动态扩缩容这些操作都比较宽泛,我们需要是针对Redis做这方面的定制,而定制的过程又需要深入了解kubernetes的内部结构,处理起来比较繁琐。而Docker本身的提供的API接口完全够用,使用kubernetes反倒是增加了复杂度,而得到的红利却很少,所以不太适合。至于swarm,提供的功能又比较简单。
相对来说只是对Redis做Docker化的部署还是相对简单地,要做的事情也不是很多,主要集中在以下方面:
Redis在Docker下的CPU控制
Redis本身对CPU的使用敏感性不是很大,所以我们在CPU的使用隔离限制上不需要花太多的精力。用Docker隔离分配就可以满足需求。
Redis在Docker下的内存控制
在内存的控制上,就相对麻烦一点,Redis的内存使用有两个属性,实际使用的内存和操作系统分配的内存,反映在Redis上就是use_memory(实际使用的内存)和used_memory_rss(操作系统分配的内存)这两个参数,maxmemory实际控制的是used_memory,而不是used_memory_rss,由于Redis在后端执行RDB操作或者频繁的增删改产生大量的碎片,rss的值就会变得比较大。如果通过Docker强制限制内存,这个进程很可能直接就被kill了,所以,在内存上,我们没有采用Docker控制,而是通过自己的监控程序进行控制。举个例子来说,某个项目新申请的Redis是10GB,我们在通过Docker部署这个Redis的时候,只是开启了1个500MB的Redis,随着项目的使用,当Redis实际空闲内存量小于250MB的时候,我们就通过Redis命令设置maxmemory为1GB,然后继续监控,直到内存到10GB为止。等项目的内存到达10GB后,我们就根据策略的不同做不同的处理了,比如,有些项目只是拿Redis作为缓存而已,10GB的数据足够了,万一超过,可以放弃掉部分冷数据,在这样的情况下,就不会继续加大内存。如果项目很重要,也可以设置一个超额的量,这样程序会自动进行扩容同时发出警报,让项目开发人员及时检查,防止出现问题。
Docker网卡的控制
用Redis都有个很头疼的问题,就是Redis的网卡打满问题,由于Redis的性能很高,在大并发请求下,很容易将网卡打满。通常情况下,1台服务器上都会跑几十个Redis实例,一旦网卡打满,很容易干扰到应用层可用性。所以我们基于开源的Contiv netplugin项目,限制了网卡的使用,主要功能是提供基于Policy的网络和存储管理。Contiv比较“诱人”的一点就是,他的网络管理能力,既有L2(VLAN)、L3(BGP),又有Overlay(VxLAN),有了他就可以无视底层的网络基础架构,向上层容器提供一致的虚拟网络了。最住哟啊的一点是,既满足了业务场景,又兼容了以往的网络架构。在转发性能上,他能接近物理网卡的性能,特别在没有万兆网络的老机房也能很好的使用。在网络流量监控方面,我们通过使用ovs的sflow的数据包进行解析,筛选出关键数据,然后进行汇总分析,得到所需要的监控数据。通过这个定制的网络插件,我们可以随意控制某个Redis的流量,流量过大,也不会影响其他的项目,而如果某个服务器上的Redis流量很低,我们也可以缩小他的配额,提供本机其他需要大流量的程序使用。这些,通过后台的监控程序,可以实现完全自动化。
凤凰缓存系统对Redis的监控
要良好的运维一个系统,监控的好坏是关键点,监控的基础主要是收集服务器和Redis的运作信息,并将这些信息丢入一个信息处理管道,在分析之前结合经验数据输出一系列的具体处理方式。这样缓存系统自己就能处理掉大部分的故障,不需要大量的人工介入。凤凰缓存系统整个监控由搜集器、存储器、分析器和执行器4部分组成:
- 搜集器是我们利用Go开发的一个程序,这个程序搜集两个部分的数据,即服务器本身的数据和Redis的数据。首先,搜集程序会查询当前所在服务器的CPU、网卡、内存、进程等信息。然后,搜集程序查询这个服务器上的Redis,然后遍历这些Redis,获取info、slowlog、client等信息,再把这些信息整理好,上报到存储器上去。
- 存储器负责存储监控数据,他对外就是一组RESTful API。存储器对这些信息进行汇总和整理,再进行一些简单的计算,然后将数据存储到分析平台中。
- 分析器的工作是从查询ES开始的,针对各种数据进行分析和处理,然后产出各种具体处理意见,提交给执行器。比如,分析器发现某个服务的CPU在10分钟内都是100%的,着就会触发了一个警报阈值,分析器会产出一个处理意见,建议人工介入处理。这个处理意见提交给执行器,由执行器具体执行。
- 执行器是一个根据处理意见进行处理的分析程序,简单分类问题并结合处理意见进行判断是人工介入还是先自动处理。对于自动处理的事件,如Redis的内存不够,进行扩容操作等。
凤凰缓存系统对Redis的集群分片优化
Redis的集群分片我们在凤凰缓存系统中进行了完整的私有实现。但从Redis 3.0开始,提供了集群功能,可以将几台机器组织组成了一个集群。在凤凰缓存系统中也对Redis 3.0的集群模式进行了支持。我们先看一下Redis 3.0的一些分片特性:
- 节点自动发现。
- slave→master选举,集群容错。
- 在线分片。
- 基于配置的集群管理。
- ASK转向/MOVED转向机制。
Redis 3.0集群中,采用slot(槽)的概念,一共分成16384个槽。对于每个进入Redis的键值对,根据key进行散列,分配到这16384个slot中的某一个中。使用的hash算法也比较简单,就是CRC16后16384取模。
整套的Redis集群基于集群中的每个node(节点)负责分摊这16384个slot中的一部分,也就是说,每个slot都对应一个node负责处理。当动态添加或减少node节点时,需要将16384个槽做个再分配,槽中的键制也要迁移。这个过程需要人工介入。
为了增加集群的可访问性,官方推荐的方案是将node配置成主从结构,即一个master主节点,挂多个slave从节点。这时,如果主节点失效,Redis Cluster会根据选举算法从slave节点中选择一个上升为主节点,整个集群继续对外提供服务。这非常类似之前的Sentinel监控架构成主从结构,只是Redis Cluster本身提供了故障转移容错的能力。
通过这样的设计,Redis实现了完整的集群功能。但是,这个集群功能比较弱,表现在以下这些方面:
- Redis集群之间只对Redis的存货负责,而不对数据负责。这样,当客户端提交请求之后,如果这个key不归这个服务器处理,就会返回MOVE命令,需要客户端子性实现跳转,增加了客户端的复杂度。
- 当Redis需要迁移或槽重新分配时,需要人工介入,发送命令操作。虽然官方也提供了一个迁移脚本,但是本身功能比较简单,也没有办法很好的自动化。
- 集群进行分片,所有的key被分散在各节点上。之前说过集群之间只处理死活和槽分配,不处理数据,所以所有的多key操作(事务、MGET、MSET之类)的操作不能再用。
针对这些问题,我们在凤凰缓存系统中对Redis 3.0的集群做了些改造,解决了上述令人痛苦的地方。
首先,我们修改了客户端的实现,按照Redis的协议只是进行了一些自定义的修改,如对在集群中加入机器以及迁移槽的过程中的一些问题进行了优化,使客户端能更平滑的迁移。
其次,迁移槽还是比较麻烦的,主要涉及CLUSTER SETSLOT和MIGRATE两个命令,MIGRATE的主要问题是key的大小不能确定,开发迁移工具时我们主要解决网卡流量和CPU的压力问题,使之在迁移的过程中不影响Redis的正常使用。这个工具主要工作场景是当监控程序发现某个分片流量过大,或者key特别多时,就自动开始迁移过程,省去了人工的麻烦。迁移工具也会实时微调线上的Redis,保证各个分片的正常。
最后,在集群环境中,多key的操作都无法使用,这个直接导致了类似事务、MGET、MSET这样的操作无法进行。我们针对问题对Redis进行了改造,将Redis的分槽策略进行了改动。原本针对key进行分区,改造为当key满足类似{{prefix}}key这样的格式时我们将只针对{{}}内的内容计算hash值。这样,相关的一组key可以使用统一的前缀,并保存在同一片中。这样保证事务以及多key操作能顺利执行。
客户端在运维中的作用
凤凰缓存系统应用层客户端在解决运维方面最大的作用有3个:
- 系统调整后Proxy接入地址改变的问题,平滑切换的问题。
- 类似KEYS这样的命令导致redis堵塞的问题。
- 应用操作异常的发生地详细信息不透明的问题。
先来看第一个问题,我们提供的是一套完整的配置管理系统——分布式的配置系统。服务器端对Redis接入的操作和修改都会通知配置中心,然后,再由配置中心发到所有的客户端,客户端接收到配置更新后,会修改自己的连接平滑过渡。这样客户端就可以在不重启的情况下动态切换连接,另外,在客户端有个连接池的实现,当老的连接重新回到连接池后,就会被销毁掉。这样,客户端的切换是平滑的,不会因为切换导致客户端的请求抖动。
再来看第二个问题。由于Redis的单进程单线程特性,不太适合做密集的CPU计算,但是很多开发人员对具体命令的掌握不熟,经常会导致Redis的CPU使用率达到100%。悲催的是,当这样的情况发生之后,就不会处理任何客户端的请求了,他要等当前的这个任务执行完成之后才会继续下一个任务。针对这个问题,我们将容易导致CPU打满的命令和普通的命令区分了开来,并提供了自己的实现。Redis服务有两个版本,基础班和Plus版。普通版只有最基础的Redis命令,在大部分场景的使用下,都不会引起问题。Plus版从普通版继承而来,里面添加了大量的Redis复杂操作命令,当正常使用的时候,直接在普通版中操作数据;当需要高级功能可以自动转换到Plus版,这样一些特殊的命令可以无脑使用了。
最后第三个问题,在大部分异常使用中,对于异常发生的现场情况,开发人员都可以通过自己的日志看到,但对于凤凰缓存系统的运维这个信息的透明度不高,这样就在故障处理上浪费了时间,所以我们在客户端中记下了完整的操作日志信息,并整合到凤凰缓存系统的监控后台。
凤凰缓存系统在Redis运维上的工具
Redis毕竟只是个简单的KV数据库,当初老的版本,作者用了数万行代码就实现了整体的功能,在完善的运维方面,是比较欠缺的。所以,我们针对运维方面的问题点,开发了一些小工具,这些工具,大部分都整合到了凤凰缓存系统里面,可以直接由执行器操作,进行自动化运维。如Redis运行状态监控,在Redis使用工程中,经常有开发人员问,我们目前哪些key是热key?当前的Redis并发访问量比较高,能不能看一下主要是哪些命令导致的?但是Redis本身并没有提供这些命令。好在Redis有个命令monitor,可以将当前的Redis操作全部导出来,我们就基于这个命令,开发了一个Redis监控程序。当开始监控某个Redis时,会发现monitor命令,然后,Redis会将他接收到的命令源源不断地吐给我们,我们接到后,就可以进行分析:当前Redis正在执行什么操作、什么操作最频繁、具体是哪些key、占用的比例是多少、哪些key比较慢等等。然后生成一个报表,就可以获得当前的状态了,不过要注意的是,monitor命令对Redis本身有一定的影响,一般情况下,不建议打开,只在需要分析问题的时候,可以打开。另外,也可以配置一些阈值,当达到阈值的时候,自动打开。这样就可以在问题一发生,就抓取到最新的监控日志信息。
还有Redis的数据迁移,Redis的部署方式比较复杂,有单机主从、集群各种模式。而所有的数据都在内存里面,如果我们需要在各个Redis中迁移数据,就非常的麻烦。所以,我们开发了一个迁移工具,专门在Redis中迁移数据。这个程序在启动之后,会冒充自己是Redis的一个从机,然后发送从机命令从主机同步数据,主机在把数据发送给程序之后,程序会对这些数据进行解析,解析之后的内容写到具体的后端Redis中,这样,就完成了两个Redis间的数据转换。这个客户端可以智能的查看当前Redis状态,针对集群,主从模式有不同的处理方式。这样的工具我们还有很多,就不再一一说明了。
凤凰缓存系统的使用效果
这个凤凰缓存系统在2016年初整个系统正式全部完成并上线,到目前为止,在整个凤凰系统上运行着5000多个Redis实例、上百TB的内存。从申请到销毁,所有过程都由凤凰系统自动化完成,不需要人工参与其中。在凤凰系统上线后,几乎就没有再出现过Redis的故障了,曾经焦头烂额的Redis如今已经被凤凰系统驯得服服帖帖。同时凤凰系统每天会将异常的消息实时发送给各个负责人,并自动处理绝大多数异常,对于少部分无法判定的问题会通知运维人员来做具体的判定和操作。