Redis的高可用方案
1.1 Redis集群的设计原则和初衷
在官方文档Cluster Spec中,作者详细介绍了Redis集群为什么要设计成现在的样子。最核心的目标有三个:
- 性能:这是Redis赖以生存的看家本领,增加集群功能后当然不能对性能产生太大影响,所以Redis采取了P2P而非Proxy方式、异步复制、客户端重定向等设计,而牺牲了部分的一致性、使用性。
- 水平扩展:集群的最重要能力当然是扩展,文档中称可以线性扩展到1000结点。
- 可用性:在Cluster推出之前,可用性要靠Sentinel保证。有了集群之后也自动具有了Sentinel的监控和自动Failover能力,也就是说Redis集群的主从自动集成了Sentinel的功能,在创建主从的时候无需再使用Sentinel。
1.2 架构变化与CAP理论
在Redis Cluster出现之前(也就是单机版的Redis),每个Master之间是没有任何通信的,所以我们一般使用客户端(类似Memcache实现集群的方式,把锅丢给客户端Driver)或者codis(豆瓣开源的Redis集群方案)和twemproxy(twitter开源的Redis集群方案)这样的代理做Pre-sharding。
按照CAP理论来说,单机版的Redis属于保证CP(Consistency & Partition-Tolerancy)而牺牲A(Availability),也就说Redis能够保证所有用户看到相同的数据(一致性,因为Redis不自动冗余数据)和网络通信出问题时,暂时隔离开的子系统能继续运行(分区容忍性,因为Master之间没有直接关系,不需要通信),但是不保证某些结点故障时,所有请求都能被响应(可用性,某个Master结点挂了的话,那么它上面分片的数据就无法访问了)。
有了Cluster功能后,Redis从一个单纯的NoSQL内存数据库变成了分布式NoSQL数据库,CAP模型也从CP变成了AP。也就是说,通过自动分片和冗余数据,Redis具有了真正的分布式能力,某个结点挂了的话,因为数据在其他结点上有备份,所以其他结点顶上来就可以继续提供服务,保证了Availability。然而,也正因为这一点,Redis无法保证曾经的强一致性了(因为Redis的主从是异步复制的,所以主从数据的同步会有延迟)。这也是CAP理论要求的,三者只能取其二。
1.3 Redis集群实现
Sharing
一个集群系统最重要的一个点就是Sharding(分片)技术,这是实现水平扩展的前提条件。下面介绍一下Redis集群是如何做分片的。
Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用另外一种叫做“哈希槽 (hash slot)“的方式来分配的。redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16
算法来取模得到所属的slot,然后将这个key 分到哈希槽区间的节点上,具体算法就是:hash_slot = CRC16(key) % 16384
。从上面我们可以看到redis集群一共有16384个slot,所以一个redis集群也就被限制为最多有16384个节点,当然了,在现实世界中,也不可能会有那么多的节点,因为当节点数量达到一定的量级时,Redis集群内部的通讯就会占满整个带宽,何谈对外提供服务呢。另外16384这个数字也不是作者随意指定的,Redis集群内部使用位图(bit map)来标志一个slot是否被占用,为了减少集群之间信息交换的大小,信息的大小被固定为2048字节,所以,crc16生成的是一个16位的整形,216/2048=214=16384。
我们假设现在有3个节点已经组成了集群,分别是:A, B, C 三个节点,它们可以是一台机器上的三个端口,也可以是三台不同的服务器。那么,采用哈希槽 (hash slot)
的方式来分配16384个slot 的话,它们三个节点分别承担的slot 区间是:
- 节点A覆盖0-5460;
- 节点B覆盖5461-10922;
- 节点C覆盖10923-16383.
那么,现在我想设置一个key ,比如叫my_name
:
set my_name hanyugang
按照redis cluster的哈希槽算法:CRC16('my_name')%16384 = 12803
。 那么就会把这个key 的存储分配到 C上了。
-----------------------------------------------------------------------------------------------------------------------------下面插播一条小贴士------------------------------------------------------------------------------------------------------------------------------------------------------
Tips:如何计算一个key的hash slot值呢?
1、使用redis集群自带的CLUSTER KEYSLOT key命令来计算:
使用cluster keyslots 命令计算hash slot
2、使用在线计算工具:http://www.ip33.com/crc.html
记得参数模型选择CRC-16/XMODEM哦,计算出来的数值别忘了MOD16384呢!
---------------------------------------------------------------------------------------------------------------------------------------------------------------小贴士完-----------------------------------------------------------------------------------------------------------------------------------
这也是使用redis集群管理工具redis-trib.rb的默认slot的分配结果。当然了我们也可以不使用redis-trib.rb工具,通过redis提供的cluster指令集设置和分配slot,这是后话,暂且不提。
你可能注意到redis集群使用hash_slot来切分数据,而不是传统的一致性哈希算法来切分数据。hash slot的算法比较简单,相当于最最原始的取模算法的加强版,这样做的好处就是计算简单,逻辑清晰。坏处当然也大大的,不然那么多的一致性哈希算法被发明出来也就没道理了。我假设您了解一致性hash算法的目的,一句话:相当于提供了一个虚拟的中间层,把虚拟的slot和真实的物理节点关联起来,当动态地添加和减少节点时,能够有效地减少数据的迁移量,这也应证了业界著名的一句格言:没有什么是添加一层(中间件)解决不了问题的。由于redis的集群没有使用一致性hash算法,它的hash slot在集群初始化就是固定分配好的,因此当添加一个或者减少一个节点时,就不能做到自动化了,也就是说当添加一个节点或者删除一个节点时,需要人工的介入,这点和cassandra等分布式NOSQL比起来简直是逊爆了,不是吗?好在redis的作者也意识到这个问题,提供了一个redis-trib.rb集群工具,可以“比较”方便地对进行数据的迁移。_
添加一个节点:
1、把新的节点添加到集群中,通过cluster meet命令,这样添加后的新节点就是一个slot为空的主节点
2、为新的节点分配slot,由于之前的集群可能把slot已经分配完毕,这个时候就需要从其他节点把slot迁移过来(当然数据也会一起迁移过来)
删除一个节点:
1、redis集群是无法删除一个slot不为空的节点的,因此想要删除一个节点,就需要把该节点的slot迁移到其他节点上去
2、slot迁移完毕后,使用cluster forget命令把该节点从集群中删除
看到了吗,搓搓的。
另外Redis集群本身不提供任何的负载均衡,因此当出现严重的负载不均的问题时——由于Redis集群使用key对数据进行分片,因此如果您给key起名的时候过于执着,刚好所有的key都映射到同一个节点上去了,其他的节点数据为空(当然了,这不太可能,crc16算法还是比较均衡的,O(∩_∩)O哈哈~,您不可能那么点背,这里只是为了说明Redis集群本身不提供任何的Load Balance而已)———我们需要人工的对数据进行迁移(对数据的迁移实际上就是对slot的迁移嘛),这点也是搓搓的。
Proxy?NO!!!
Redis集群使用了去中心化、去中间件的设计方案。如果您熟悉Mongodb的话,一定对mongos记忆犹新,然而Redis集群不提供像mongodb提供的mongos一样的东西,把客户端发出的一条指令转发到不同的节点,然后再把结果合并返回给客户端。Redis集群使用下面的策略来处理一条命令:
1、假设客户端连接到A节点,去请求一个key叫做key_3的值,很巧,这个key所在的slot位于A节点上(CRC16('key3')=3740),那么Redis就会直接返回key_3的值给客户端
2、如果这时客户端去请求一个key叫做my_name的值该怎么办呢?我们知道key=my_name的值存储在C节点上,这时Redis就会返回一个MOVED的反馈给客户端,告诉客户端my_name这个值在C节点上,你要到那里去取。如下图所示:
MOVED重定向
可以看到,Redis集群返回了一个Redirect消息给客户端,告诉客户端你要的key=my_name的值存储在10.91.42.213也就是C节点上,你到那里去取吧。如你所见,最终服务器还是返回了hanyugang值给客户端,那么这个返回值是Redis内部做了跳转返回给客户端的吗?你太天真了,这个其实是redis-cli又发起了一次请求到C节点上才取回的(所以在运行redis-cli的时候要加上-c参数哦,该参数表示让redis-cli支持集群功能)。实际上Redis集群什么都没做,它只是告诉你key=my_name的值在C节点上而已。希望您可以通过这个例子看出Redis集群对命令的处理方式,No Proxy的方式就是这样做的,把锅丢给客户端,我不会做任何的内部处理。
您也可能会问,Redis集群是怎么知道key=my_name的值存储在C节点上的?原因就是Redis集群通过bus port,使用Gossip的协议在集群之间相互交换信息,也就是说整个集群是知道节点和slot的对应关系的,通过计算my_name的slot是12803就知道这个值存储在C节点上了(C节点负责的slot是10923-16383嘛)。值得注意的是,这个Bus Port是无法设置的,它的值永远是与客户端通信的端口(默认是6379)+10000,默认值当然也就是16379了。还需要注意的一个点是:在早期的Redis集群版本中,在节点相互通信的时候,整个集群会检查16384个slot是否被完全覆盖,如果发现没有被完全覆盖,整个集群是拒绝对外提供服务的,这个就比较坑爹了,一部分节点down掉时,整个节点就无法对外提供服务,说好的高可用呢?所以在新的版本里,这个问题得到了修正,在redis的配置文件中,有个cluster-require-full-coverage的设置项,我们建议一定要把这个值设置为no(默认是yes哦),不然redis集群的高可用会变的辣鸡无比。
No Proxy? Problem!!!
正是由于上面介绍的去中心化、去中间件的设计,Redis集群对Multi Key的操作做了诸多限制。一句话:对于Multi Key的操作被限制在同一个slot上面,跨节点的Multi Key操作,就更别想了。在单机版本中愉快的使用MGET key_1 key_2 key_3 ...的命令,在集群版本中会报错哦!
支持multi key的命令需要所有的key都在一个hash slot上
同理,Lua脚本中的multi key操作,transcation上面的multi key操作,pipleline上的multi key操作等都会受到这个限制的影响。MMP,怎么办?好消息是Redis集群提供了hash tag的解决办法(这个hash tag其实并不是什么新鲜玩意,在redis官方集群方案出来之前,第三方的tweenproxy和codis等早就采取了这个方案),你可以这么做在set的时候使用hash tag:mset {key}_1 xxx {key}_2 yyy {key}_3 zzz,这样就可以在集群上执行mget {key}_1 {key}_2 {key}_3了:
hash tag的作用
这正是hash tag的用处:
在计算hash slots时有一个意外的情况,用于支持“hash tags”;
hash tags用于确保多个keys能够被分配在同一个hash slot中,用于支持multi-key操作。
hash tags的实现比较简单,key中“{}”之间的字符串就是当前key的hash tags,在计算slot的时候只使用hash tag,而不是使用整个key,因此具有相同hash tag的key总会被存储在相同的slot上;
hash tag还支持多级嵌套,我想为了程序的简单化,还是不要使用多级嵌套了,伤脑,因为为key起个有意义的名字本身就是一件很有挑战性的难题了,不是吗?
Redis集群对一些复杂的运算,比如Set的intersection等操作进行了重新实现,只要两个Set位于同一个节点,就可以进行运算;另外对Publish/Subscribe做了重新实现,能够保证在集群环境下,提供和单机版本相同的功能,也就是说在集群上任何一个节点上Publish,在集群的任何一个节点上都能够Sub到这个信息。
还有一种很特别的情况,就是当Redis集群在做数据迁移时,一个key刚从一个节点迁移到另外一个节点,那么请求该节点数据的时候,Redis会返回一个ASK反馈,并附带该key目前所处的新节点的位置,此时客户端Driver需要向新的节点位置发起ASKING请求;当然这是对单个key进行操作的情况,如果是multi key的操作,那么Redis将会直接返回一个失败的消息。因此在做数据迁移的时候,multi key的操作有很大的概率会出错。目前这个无解哦,您只能等待迁移完成在或者迁移回滚再进行操作了。
顺便提一下:Redis集群取消了db number的支持,也就是说之前单机版本的db0、db1...db16不复存在了,在集群环境中使用SELECT命令是会报错了。
-----------------------------------------------------------------------------------------------------------------------------下面插播一条小贴士------------------------------------------------------------------------------------------------------------------------------------------------------
Tips:客户端Driver是如何实现Redis集群的这些奇葩的特性的?
客户端Driver和Redis集群的连接:
对于Redis集群而言,客户端无论连接到A节点还是C节点,其实总能获取到相同的结果,因此Redis集群的客户端Driver在和集群建立连接的时候并不是同时与集群中的所有节点都建立连接哦,实际上是这样做的:随机选择集群中的一个节点(集群中的节点们当然组成了一个HOST:PORT数组),进行连接,如果连接不成功,重试下一个节点,直到有一个节点能够连接成功。
客户端Driver处理重定向的方式:
上面已经介绍了Redis集群经常会返回重定向的反馈消息告诉客户端Driver,信息没有存储在我这里,你到另外一个节点去找吧。如果客户端Driver按照这个思路来做,实际上是要发起2次请求的,这和Redis集群的设计初衷相违背(Redis集群之所以使用重定向来处理命令,就是为了速度,现在要发起2次请求才能获取到最终值,岂不是南辕北辙了?)。当然客户端Driver不会那么傻的,它们在实现的时候是这么做的:客户端本地会保存一份slot与节点的映射Map,比如现在要存储key=my_name的值,客户端driver计算出CRC16('my_name') mod 16384 = 12803,然后查找本地存储的slot与节点的映射Map,发现slot 12803位于节点C上,因此客户端Driver会直接向C节点发起请求,这就做到了一步到位。可以想象,这个Map并不是一直不变的,什么时候客户端要更新自己本地的映射Map呢,很简单啦,当出现MOVED反馈的时候,客户端Driver不但会再次发起请求,还会更新本地的映射Map哦。另外对于Redis集群返回的ASK请求,客户端Driver是不应该更新本地的映射Map的,因为处于ASK状态只是一种临时的状态(ASK状态本身是由于数据的迁移造成的,数据迁移很可能会取消,所以这只是一种临时状态,只有当迁移完毕时,slot的位置才是确定的)。
说了那么多,Redis集群的客户端Driver必须要按照上面的要求实现吗?不是的,这只是一个建议而已。你完全可以按照使用2次请求的方案来实现,不过有点笨是不是?
-------------------------------------------------------------------------------------------------------------------------------------小贴士完-------------------------------------------------------------------------------------------------------------------------------------------------------------
高可用
下面介绍一下Redis集群是怎么实现高可用的。传统的分布式NOSQL数据库一般都是使用数据副本的概念来实现数据的冗余,从而实现高可用。
Redis集群由于历史包袱或者说实现的简单化,使用主从异步复制的技术来实现数据的冗余,并通过集成Sentinel的功能实现自动Fail Over来实现整个集群的高可用。
也就是说我们在做Redis集群的时候,如果没有为主节点挂载一个Slave,那么这个集群就是脆弱的,不堪一击的,因为只要有主节点挂掉,没有Slave节点顶上,整个slot环就会有缺失,从而导致集群变得”部分可用”。
当我们在谈论Redis集群的时候,自然就把Slave包含进去了,嗯,这是约定俗成。
这也解释了为什么我们在做Redis集群的时候要开启6个实例(3主3从嘛)。至于为什么要3主,因为redis集群提供的redis-trib.rb工具要求要有3主3从呀,至于为什么要使用redis-trib.rb的集群工具,当然是为了方便集群的部署咯。
Redis集群可以2主2从吗,可以的,这需要我们手工为Redis集群分配slot等一系列繁琐的操作。得不偿失。
另外Redis集群的主从自动集成了Sentinel的功能,所有的failover和主节点down掉后恢复自动成为新主节点的Slave,这些都是自动化的,因为有sentinel。这算是Redis集群比较良心的一个点了。
另外Redis集群还支持Slave Migration的功能,一句话:当某个主节点A配置了多于1个的Slave时,如果另外一个主节点B的所有Slave不巧都挂掉了,那么A主节点下面的某个slave(不是随机哦)会自动迁移,成为B节点的Slave。这个特性加强了Redis集群的可用性。详见redis配置文件中的cluster-migration-barrier配置项。
Redis集群的部署
上面介绍了Redis集群的一些实现细节,下面我们开始实践了,首先要部署集群的环境,Let's GO!
建立Redis集群的前提条件
首先我们要开启Redis的集群功能哦,这个比较简单,就是修改Redis的配置文件而已,一个最小化的集群的配置如下:
-
绑定地址:bind 192.168.XXX.XXX。不能绑定到127.0.0.1或localhost,否则指导客户端重定向时会报”Connection refused”的错误。
-
开启Cluster:cluster-enabled yes
-
集群配置文件:cluster-config-file nodes-7000.conf。这个配置文件不是要我们去配的,而是Redis运行时保存配置的文件,所以我们也不可以修改这个文件。
-
集群超时时间:cluster-node-timeout 15000。结点超时多久则认为它宕机了。
-
槽是否全覆盖:cluster-require-full-coverage no。默认是yes,只要有结点宕机导致16384个槽没全被覆盖,整个集群就全部停止服务,所以一定要改为no
修改了这些配置之后,才算真正的开启了Redis的集群功能。
然后准备6个Redis的实例吧(每个Redis的实例都要做上面的配置修改哦)。我们假设集群的环境如下:
Host | Port | Master/Salve |
---|---|---|
10.91.42.211 | 6379 | Master |
10.91.42.211 | 6479 | Slave |
10.91.42.212 | 6379 | Master |
10.91.42.212 | 6479 | Slave |
10.91.42.213 | 6379 | Master |
10.91.42.213 | 6479 | Slave |
启动这6个实例哦!!!
redis-server /etc/redis/redis.conf
然后运行redis集群提供的集群工具:
./redis-trib.rb create \
--replicas 1 \
10.91.42.211:6379 \
10.91.42.212:6379 \
10.91.42.213:6379 \
10.91.42.211:6479 \
10.91.42.212:6479 \
10.91.42.213:6479
这个创建集群的命令行会使用前三个参数作为Master,后三个作为Slave,然后会询问你是否接受这样的配置,输入“yes”。
等待一会,整个集群就会被建立起来。很方便的。值得注意的是:这时候Redis的配置不能设置密码,因为redis-trib.rb工具不支持提供密码验证的功能。
至此,使用redis-trib.rb工具就已经把所有的Redis集群环境搭建起来了。下面介绍一下如何手工搭建一个Redis的集群环境,知其然才能知其所以然嘛!
看似非常简单,可是这个脚本都干了什么事情呢,如果我们手动建立Redis的集群该怎么做呢?
1、假设我们已经运行了上面介绍的6个redis的实例
2、集群发现:CLUSTER MEET
最开始时,每个Redis实例自己是一个集群,我们通过cluster meet
让各个结点(这里是指角色为MASTER的节点啦)互相“握手”。这也是Redis Cluster目前的一个欠缺之处:缺少结点的自动发现功能。
redis-cli -h 10.91.42.211 -p 6379 CLUSTER MEET 10.91.42.212 6379
redis-cli -h 10.91.42.211 -p 6379 CLUSTER MEET 10.91.42.213 6379
运行了上面的命令之后主节点的集群就建立起来了,他们会自动发现彼此。
3、角色设置:CLUSTER REPLICATE
主节点全部“握手”成功后,就可以用cluster replicate
命令为结点指定角色了,默认每个结点都是Master。
3.1 把10.91.42.211:6479节点指派为10.91.42.212:6379节点的Slave节点:
redis-cli -c -h 10.91.42.211 -p 6479 cluster replicate 33c0bd93d7c7403ef0239ff01eb79bfa15d2a32c
注意后面的33c0bd93d7c7403ef0239ff01eb79bfa15d2a32c,这个是Redis集群中唯一标识一个Redis实例的Node ID,这里我们假设10.91.42.212:6379节点的Node ID为:33c0bd93d7c7403ef0239ff01eb79bfa15d2a32c,这个ID可以通过cluster nodes命令来显示,也可以通过nodes.conf来查看,还记得cluster-config-file配置项吗,这个ID就会保存在这个配置文件中。
3.2 把10.91.42.212:6479节点指派为10.91.42.213:6379节点的Slave节点:
redis-cli -c -h 10.91.42.212 -p 6479 cluster replicate 63162ed000db9d5309e622ec319a1dcb29a3304e
3.3 把10.91.42.213:6479节点指派为10.91.42.211:6379节点的Slave节点:
redis-cli -c -h 10.91.42.213 -p 6479 cluster replicate 45baa2cb45435398ba5d559cdb574cfae4083893
Tips:不要把主从都设置在同一台机器上哦,原因你懂得,一台机器坏掉主从都挂掉,还谈什么主从切换,fail over!
4、hash slot 指派:CLUSTER ADDSLOTS
设置好主从关系之后,就可以用cluster addslots
命令指派16384个槽的位置了(这里要在Master节点上指派哦)
redis-cli -c -h 10.91.42.211 -p 6379 cluster addslots {0..5460}
redis-cli -c -h 10.91.42.212 -p 6379 cluster addslots {5461..10922}
redis-cli -c -h 10.91.42.211 -p 6379 cluster addslots {10923..16383}
嗯呢,16384个slot已经分配完毕了。
至此,我们手工搭建了和redis-trib.rb工具搭建的一样的Redis的集群环境了。嗯呢,其实redis-trib.rb脚本就是干了这些事情。
这也解释了为什么我们需要6台Redis的实例,因为redis-trib.rb工具需要呀,我们当然可以手工搭建2主2从的Redis集群,步骤有些麻烦不是吗?再者,Redis官方也建议3主3从的集群配置,因此我们为了偷懒,就使用3主3从的Redis集群环境吧!
至于添加一个节点到集群,以及删除集群中的一个节点,以及数据的迁移,redis-trib工具都有相应的功能,为了防止这个文档过长,具体的细节就不再赘述了,当您需要时,您可以查阅Redis的官方文档。