在《全面剖析Redis Cluster原理和应用》中,我们已经详细剖析了现阶段Redis Cluster的缺点:
当然之前也说过了:“这与Redis的设计初衷有关,毕竟作者都已经说了,最核心的设计目标就是性能、水平伸缩和可用性”。但综合来看,要想在生产环境中使用Redis Cluster,我们还是有一些工作要做的。本文就从宏观层面上,列举一些架构优化的参考方案。
Gossip消息的通信开销是P2P分布式系统带来的第一个副作用。有一篇关于Gossip通俗易懂的文章《Life in a Redis Cluster: Meet and Gossip with your neighbors》。Redis为集群操作的消息通信单独开辟一个TCP通道,交换二进制消息:
cluster-node-timeout
一半的时间还未PING过或未收到PONG的结点,所以这个参数会极大影响集群内部的消息通信量。心跳包除了结点自己的数据外,还包含一些其他结点(集群规模的1/10)的数据,这也是“Gossip流言”的含义。关于Gossip的问题不可避免,我们只能通过参数调整和优化,在通信效率和开销之间找到一个平衡点。因为笔者还未搭建过大规模的Redis Cluster集群,关于集群的性能和参数调优还不能给出建议,留到积累足够经验再做整理吧。
以Nginx为例,修改配置甚至升级版本都不需要停机,Master会逐一启动新的Worker实例去替代旧的Worker。对于单机版的Redis,我们也可以用类似的方式实现的。但目前不知道Redis Cluster或者其他P2P分布式系统像Cassandra,是否有比较好的方案。
由于集群内结点都是对等的,所以像数据热度这种整体的统计数据就无处存放。当内存有限时,要想实现层次化存储,将冷数据Swap到慢存储如磁盘上时,就变得有些棘手了!
解决方法就是计算机界号称万能的“增加中间层”方法。增加一层Proxy,负责做数据统计、Swap甚至L1缓存。关于冷热数据的统计和处理,请参考《微博CacheService架构浅析》
Redis Cluster的引入会给客户端带来一些挑战。要么“勇敢面对”,通过引入最新的支持Cluster协议的Jedis客户端,再加以改造来应对这些挑战。要么增加Proxy,像防洪堤坝一样将危险隔离在外。
对于Java,最主流的Jedis客户端已经早早开始支持Cluster协议了,但仔细看了一下,貌似处理集群中结点Failover时有些问题。Slave替换上来了,Jedis的确可以根据”MOVED”消息更新Slot与结点的对应关系,但是:
不知道这算不算Jedis由来已久的问题了。因为之前Jedis就是只支持要么用分片连接池,要么用Sentinel连接池,没有两者的结合!还好有热心的程序员“出手相助”,详见《Jedis分片Sentinel连接池实验》。上面两个问题对应的源码看得不是很细,突然想到的这两个问题,要是说的不对还请指正!
为了实现Smart客户端,Jedis要缓存16384个Slot到结点的映射关系。这还不算什么,Jedis还要为每个结点单独开一个连接池。假如你有一台强劲的32核服务器,为了在多核上充分释放Redis的处理能力,可能会起16甚至32个实例,想想会有多少连接建立?如果你有两台应用呢?
这个问题在像Hazelcast或GridGain等其他P2P系统中还不会这么严重。因为这两个产品都是用Java多线程开发的,每台服务器上起一个实例就可以了。这样客户端即便是Smart模式也不会开很多连接到服务器。后面还会讲到,单线程的Redis的运维成本也不小。
因为Redis Cluster自动数据Sharding的缘故,MultiOp和Pipeline都被限定在命令中的所有Key必须都在同一Slot内。如果想突破这个限定该怎么办?那扩展Jedis或者在Proxy中实现命令拆分和结果聚合的逻辑。
关于Redis的具体实现细节问题,主要是Redis简洁的设计、redis-trib等工具的欠缺导致的。我们可以通过Dashboard或Agent组件来解决这些问题。
Redis Cluster没有使用传统的Multicast通知自动发现集群结点,我们能做的也只能是像redis-trib那样,在用户指定新结点时帮它执行CLUSTER MEET
命令。
手动指的不只是像Codis那样要在控制台上添加完新结点后手动触发Rebalance,而是要我们指定哪些Slot迁移到哪些结点上!就像建立集群时做的那样!如果我们有个统一的Dashboard,实现个简单的根据各个机器和Slot负载进行Resharding的算法,那么就能将这部分工作自动化了。
Redis一直没有官方的监控管理工具,到了Redis Cluster依旧是这个样子。这个问题比较好解决,像Codis那样提供一套漂亮的Dashboard就可以了,底层使用各种CLUSTER
命令完成工作。
关于“脑裂”(网络分区)问题,只能靠Redis官方提供解决方案了。
GitHub上有人提了一个Issue “redis-trib: use pipeline to speed up moving slot”,通过Pipeline调用Migrate命令,改善redis-trib的迁移速度。但这样只是治标不治本,毕竟迁移的基本单位还是Key而不是Slot。但因为Redis的save/bgsave都是实例级别,所以要想不改Redis源码就获得Slot的复制或迁移能力,还真不太好办!
看看Codis作者的思路:“在RebornDB中我们会尝试提供基于复制的迁移方式,也就是开始迁移时,记录某slot的操作,然后在后台开始同步到slave,当slave同步完后,开始将记录的操作回放,回放差不多后,将master的写入停止,追平后修改路由表,将需要迁移的slot切换成新的master,主从(半)同步复制,这个之前提到过。”
由于无中心化的设计,数据迁移的进度等信息无处保存。如果迁移中发生失败,则可能某一个Slot处于迁移中间状态。再加上没有进度信息的话,会给我们的恢复工作带来很大麻烦。可以考虑重新启用ZooKeeper,或者单独使用一个Redis实例做全局信息存储。
对于Redis Cluster不会将请求转发给Slave结点,造成Slave冷备的问题,可以靠Proxy做读写分离来解决,当然这样会牺牲一部分的一致性。
在解决上面各种问题时我们引入了三个组件:Proxy、Dashboard和Agent。这三个组件都担当一定的职责,但这三个组件不一定非要对应部署三个子系统。根据需要,可以选择去掉或合并来简化设计。
从上面问题解决方案的分析可以看出,Proxy层的保留还是有其必要性的:
既然保留了Proxy组件,Redis Cluster的优势就不明显了。那为什么后端还要用Redis Cluster而不是单机版的Redis呢?因为Redis Cluster给我们带来几个最大的好处:
一个美观而实用的Dashboard完全有理由让用户抛弃redis-trib,要是再具有自动部署和Resharding算法那就更完美了!
Agent不仅可以完成运行数据采集,仅仅这样的话Dashboard完全可以自己完成。它还可以完成Redis Cluster的部署工作,这样就能大大降低开发人员的工作量和手工出错的概率。
Codis完全依赖ZooKeeper,进入到Redis Cluster后,ZooKeeper哪里去了?我们先回忆一下ZooKeeper在其中的角色,详情请参见《豆瓣Redis解决方案Codis源码剖析:Dashboard》:
因为Redis Cluster的P2P架构,Slot与结点的映射关系都打散到集群中的各个结点上,所以第一个问题就解决了。又因为当客户端去旧结点请求数据时会收到MOVED或ASK消息进行重定向,就像是LAZY缓存过期策略一样,等访问时再更新或清除,所以第二和第三个问题也解决了。唯一要重点考虑的就是迁移任务这种Redis Cluster并不负责的全局信息的保存。
关于如何降低Redis的运维成本,可以参考AliRedis和Reborndb。
AliRedis是来自阿里巴巴的基于Redis改造的缓存产品,目前还未开源。网上只能搜到这么一篇资料《AliRedis单机180w QPS, 8台服务器构建1000w QPS Cache集群》。
AliRedis采取“多线程Master + N*Worker的工作模式。Master线程负责监听网络事件, 在接收到一个新的连接后, Master会把新的fd注册到Worker的epoll事件中, 交由worker处理这个fd的所有读写事件。这样Master线程就可以完全被释放出来接收更多的连接, 同时又不妨碍worker处理业务逻辑和IO读写。”
AliRedis对Redis架构上非常类似Nginx的Master-Worker架构模式,那Nginx中的Master进程都有哪些作用呢?(《Nginx工作进程模型》)
因此,每台服务器上由一个Master管理这台机器上的所有Redis实例,达到充分利用多核多线程。同时每台服务器只需维护一个AliRedis实例,大大降低运维成本的目的。也可以根据需要,实现类似Nginx中Master的各种功能。
Reborndb基本是从Codis衍生出来的,但相比Codis多了一个Agent组件。agent主要是部署在Redis实例的机器上,类似AliRedis的Master,但Agent不负责处理请求。它负责监管Redis实例的生命周期,例如Redis部署、启动、停止、重启以及升级等等。它通过一套RESTFul接口来暴露这些操作。此外,它还担当着Redis高可用性协调者的角色,类似官方HA方案的Sentinel。
Master与Redis实例在一台机器上,Master负责建立连接,之后的I/O读写都交给Worker进程处理。因此,这与《Netty 4源码解析:请求处理》介绍过的主从Reactor模式里的主Reactor非常像!只不过Netty的模型是在一个进程里通过线程实现的,而AliRedis是类似Nginx用进程实现的。
而Proxy的责任要比Master大得多,它负责请求和响应处理的全过程,而不是建立连接后直接交给后端。所以Proxy的压力也不小,一般与Redis实例不部署在一台机器上。实际上,它与Master并不矛盾。Proxy模式负责解决协议解析、请求的过滤转发、结果聚合等问题,而Master-Worker模式则让Redis享受到多核的速度、不停机程序升级、降低运维成本等。
Codis作者谈到第二代Codis,即Reborndb的发展方向,很值得学习:
“在开源Codis后,我们收到了很多社区的反馈,大多数的意见是集中在Zookeeper的依赖,Redis的修改,还有为啥需要Proxy上面,我们也在思考,这几个东西是不是必须的。当然这几个部件带来的好处毋庸置疑,上面也阐述过了,但是有没有办法能做得更漂亮。于是,我们在下一阶段会再往前走一步,实现以下几个设计:
1)使用proxy内置的Raft来代替外部的Zookeeper,zk对于我们来说,其实只是一个强一致性存储而已,我们其实可以使用Raft来做到同样的事情。将raft嵌入proxy,来同步路由信息。达到减少依赖的效果。
2)抽象存储引擎层,由proxy或者第三方的agent来负责启动和管理存储引擎的生命周期。具体来说,就是现在codis还需要手动的去部署底层的Redis或者qdb,自己配置主从关系什么的,但是未来我们会把这个事情交给一个自动化的agent或者甚至在proxy内部集成存储引擎。这样的好处是我们可以最大程度上的减小Proxy转发的损耗(比如proxy会在本地启动Redis instance)和人工误操作,提升了整个系统的自动化程度。
3)还有replication based migration。众所周知,现在Codis的数据迁移方式是通过修改底层Redis,加入单key的原子迁移命令实现的。这样的好处是实现简单、迁移过程对业务无感知。但是坏处也是很明显,首先就是速度比较慢,而且对Redis有侵入性,还有维护slot信息给Redis带来额外的内存开销。大概对于小key-value为主业务和原生Redis是1:1.5的比例,所以还是比较费内存的。在RebornDB中我们会尝试提供基于复制的迁移方式。
4)QDB:QDB使用LevelDB、RocksDB、GoLevelDB作为后端存储。我们喜欢Redis,并且希望超越它的局限,因此我们创建了一个服务叫做QDB,它兼容Redis,将数据保存在磁盘来越过内存大小的限制并且将热点数据保存在内存中以提高性能。”
除了第三点“基于复制的迁移”,这些改进思路在下面要介绍的Redis商业版(RLEC)中得到了印证。因为RLEC并未透露它的迁移实现方式,所以技术细节我们不得而知。
Redis作者Salvatore Sanfilippo所在公司RedisLab提供了企业版的Redis产品——Redis Labs Enterprise Cluster (RLEC)——几乎解决了我们上述的所有问题:对客户端完全透明化,自动集群管理(伸缩、高可用、持久化等),多种监控和预警方式等等。
RLEC支持单结点(单主)、单结点主从(一主一从)、集群(多主)、高可用性的集群(多主多从)。从架构上看,RLEC集群的每个结点由以下组件组成:
任意时刻,集群中会有一个结点处于”Master”角色,即这个结点上的Cluster Manager负责整个集群的管理工作,包括集群健康检查、分片迁移、请求监管等。由此能够看出,RLEC本身的结点之前也会互相通信,选举出一个主。
额外地,RLEC还支持一些比较高级的功能,如所有Key和热Value保存在RAM,Swap冷Value到SSD,Rack感知的集群等。RLEC可以免费下载试用,免费版只支持4个分片,而且不能用于生产环境。感兴趣的话,大家可以自己下载试用一下。
除了RLEC软件,RedisLab还提供了RedisCloud云,以公有云、私有云等服务的形式提供缓存服务——Redis as a Service (RaaS)。可以说,RLEC代表了一个比较理想的Redis产品方向。