Redis从入门到入土系列(三)—到底什么是复制、哨兵、集群?
我们上文讲述了单机数据库的运行方式,但如今随着业务的发展,系统的高可用要求也越来越高。单节点宕机如何不影响服务的可用性成为一种问题,本章通过三种高可用的实现方式来讲述Redis是如何实现高可用的。
- 主从复制
- 哨兵模式
- 集群模式
一、主从复制
主从复制后面两种模式的基础,其余两种模式都是基于主从复制进行了拓展。
1.1 什么是主从复制
在Redis的两个实例(即一个Redis服务端进程)中,可以选择一个实例作为另一个实例的从节点。例如实例B执行命令 SLAVEOF A
,那么此时实例B为从节点(slave),实例A为主节点(master)。
从节点可以从主节点中复制数据。
例如我们在主节点上添加数据:
redis>SET MSG HELLO
那么在从节点上获取数据时也是可以获取到的
redis>GET MSG
HELLO
1.2 主从复制实现
主从复制有两个阶段:
- 从节点第一次从主节点复制数据
- 初次复制完成后,客户端执行的命令在主从节点都要执行
1.2.1 第一阶段
我们之前将到了RDB的实现,其实RDB不仅可以作为持久化的工具,也可以作为主从复制的工具。
主从复制有四步:
- 从节点向主节点发送SYNC命令
- 主节点接收到之后开始生成RDB文件,并使用一块缓冲区记录从此刻开始的命令
- 将生成完成的RDB文件发送给从节点执行
- 将缓冲区记录的数据也一并发送给从节点,自此主从节点数据一致。
1.2.2 第二阶段
在主从数据一致后,客户端可能仍会发送命令给主节点,这个时候主节点只需要把命令传播到从节点即可,从节点执行相同的命令以保证数据一致性。
1.3 存在的缺陷
在第二阶段之后,假如从节点突然宕机,那么再次重新连接之后又要开始从零开始的主从复制。即把全量的RDB数据重新复制一份。
但是其实,从节点只需要在其宕机过程中执行的增量命令即可,完全没必要全量的RDB文件。
1.4 新版的主从复制
新版的主从复制新加了offset的概念:
- 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
- 从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N。
通过从节点与主节点offset的差距,主节点就可以将相差的数据发送给从节点。
从节点A再次上线之后,只需要将10089-10119之间的字节发送给从节点A即可。
当然,如果从节点长时间宕机,相差数据过大的话,就不如用完全的主从复制划算,即从0开始生成RDB文件的方式。
因此如何判断相差数据是否过大呢?
主节点维护了一个双端队列,即每有一个新的元素插入,就会有一个新的元素被排出,主节点一直将最新的数据插入到这个双端队列中。
当一个从节点要求同步时,主节点会检查其offset是否存在于该双端链表中,如果存在则将该offset到队尾的全部数据发送至从节点,如果不存在,则直接开始完全的主从复制。
因此,双端队列的长度就是服务器能接受的最大相差数据。
二、哨兵模式
可以发现虽然上述的主从复制能够实现了数据的同步,但是仍旧不能实现故障自动切换。想要实现故障自动切换等高可用的动作,需要更优的解决方案。
哨兵模式(Sentinel),即通过Sentinel系统完成对整个Redis集群的监控,当主节点宕机时能够及时选出新的主节点,并将原来的主节点降级为从节点,完成自动的故障切换。
2.1 什么是Sentinel
Sentinel也是一台Redis实例,或者是Redis进程,只不过与普通的Redis服务端进程不同的是他没有数据库,不存储数据只做监控的动作。
启动一台Sentinel进程:
redis-server /path/to/your/sentinel.conf --sentinel
2.2 收集信息
Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO
命令,并通过分析INFO
命令的回复来获取主服务器的当前信息。
# Server
...
run_id:7611c59dc3a29aa6fa0609f841bb6a1019008a9c
...
# Replication
role:master
...
slave0:ip=127.0.0.1,port=11111,state=online,offset=43,lag=0
slave1:ip=127.0.0.1,port=22222,state=online,offset=43,lag=0
slave2:ip=127.0.0.1,port=33333,state=online,offset=43,lag=0
...
# Other sections
...
Sentinel可以从主节点中获取到整个集群的信息,包括节点的角色、地址、状态等。
之后,Sentinel以每秒一次的频率向集群内所有实例发送 PING
命令,包括主节点、从节点、其他Sentinel实例等。
然后根据相应结果判断各个实例的状态。
2.3 故障迁移
2.3.1 主观下线状态
Sentinel会根据集群返回的相应判断对应的状态,当Sentinel收到的响应为下线或超过一定时间没有收到相应的时候,会主观认为该节点已经下线。
2.3.2 客观下线状态
当Sentinel判断一个主节点为下线状态时,为了提高准确率,并不会直接将该节点下线,而是会向其他Sentinel询问这个主节点的状态, 当收集到了足够多的下线状态之后,Sentinel就会判断该节点确实存在问题。
在确定完主节点宕机后,会首先选出领头Sentinel。
接下来由领头Sentinel做出故障迁移的操作。
迁移的操作主要有三个部分:
- 在所有的从节点中选出一个新的主节点
- 将其他从节点的复制对象选为新的主节点
- 将旧的主节点的复制对象修改为新的主节
2.3.3 选择新的主节点
领头Sentinel选择新的节点有以下几个步骤:
- 首先会将所有从节点中状态不为在线的排除,保证所有的新节点都是可用的
- 然后删除列表中所有最近五秒内没有回复过领头Sentinel的INFO命令的从服务器,这可以保证列表中剩余的从服务器都是最近成功进行过通信的。
- 删除所有与已下线主服务器连接断开超过down-after-milliseconds10毫秒的从服务器:down-after-milliseconds选项指定了判断主服务器下线所需的时间,而删除断开时长超过down-after-milliseconds10毫秒的从服务器,则可以保证列表中剩余的从服务器都没有过早地与主服务器断开连接,换句话说,列表中剩余的从服务器保存的数据都是比较新的。
- 最后根据从节点优先级、运行ID的顺序最终选出一个新的主节点。
自此已经选出了新的主节点。
2.3.4 将其他从节点复制对象修改为新的主节点
领头Sentinel向其他从节点发送 SLAVEOF
命令,让其他从节点将主从复制的对象修改为新的从节点。
之后再将旧的主节点修改为从节点,并将其赋值对象修改为新的主节点
三、集群模式
上述的哨兵模式,还需要额外Redis实例集群,带来额外的开销,那么有没有什么方式能让Redis进程本身就可以进行高可用高可靠的操作呢。
分库分表的概念大家应该都了解过,通过对key进行hash,不同的key存储至不同的数据库以提供更高的性能。
Redis的集群模式(cluster)也是类似,通过每个Redis的负责不同的插槽(slot),提升服务的可用性与可靠性。
3.1 集群搭建
因为没有了哨兵模式中额外的Sentinel统计全局信息,因此集群模式下所有的节点都要通过命令相互认识,从而搭建整个集群。
各个节点通过 CLUSTER MEET
命令互相认识,命令格式如下:
CLUSTER MEET <ip> <port>
假设现在有三个节点要搭建集群7000、7001、7002
首先将7000与7001互相认识:
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7001
OK
127.0.0.1:7000> CLUSTER NODES
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204746210 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
再将7000与7002互相认识
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7002
OK
127.0.0.1:7000> CLUSTER NODES
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204848376 0 connected
9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388204847977 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
可以看到,此时7000、7001、7002已经组成了一个集群。
3.2 槽指派
每个Redis集群中共有16384个槽,其中客户端对key的每个操作,都会首先对key进行hash,然后计算出所在的槽,然后找到这个槽所对应的节点,由该节点处理该key的所有操作,增删改查等。
在搭建完同一个集群之后,就需要为集群内每一个节点指定槽(slot)。
将三个节点以此进行槽指派:
127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
OK
127.0.0.1:7001> CLUSTER ADDSLOTS 5001 5002 5003 5004 ... 10000
OK
127.0.0.1:7002> CLUSTER ADDSLOTS 10001 10002 10003 10004 ... 16383
OK
指派完成后,集群进入上线状态。
3.3 执行命令
在上线之后,客户端会向任意的一个节点发送要执行的命令。该节点会首先对key进行计算,计算出该key对应的槽:
- 如果该槽正好属于该节点,则直接执行
- 否则向客户端返回
MOVED
命令,并将正确的节点返回给客户度
然后客户端再去正确的节点执行命令即可:
3.4 横向拓展
可横向拓展对一个服务来说也是非常重要的,Redis集群提供了重新分片的方式可以在上线状态中随时添加或删除新的节点,只需要将其所负责的槽指配好即可。横向拓展也可以理解为槽重新指配问题。
3.4.1 槽重新指派
例如我们进群中新加入一个节点7003,将15001-16381的槽重新指派给他。
则此时的集群状态:
127.0.0.1:7000> cluster nodes
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master -0 0 0 connected 0-5000
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master -0 1388635782831 0 connected 5001-10000
9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master -0 1388635782831 0 connected 10001-15000
04579925484ce537d3410d7ce97bd2e260c459a2 127.0.0.1:7003 master -0 1388635782330 0 connected 15001-16383
那么槽的重新分配是如何做到的呢?
Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。
例如我们此时将7002的15001-16383迁移至7003,redis-trib的操作步骤如下:
- redis-trib对7003发送命令,告知他等待接收槽数据
- redis-trib对7002发送命令,告知他准备发送槽数据
- redis-trib对7002发送命令,获取某一个槽内n个输入15001-16383之间的key名
- redis-trib以第3步中获取到的n个key名,对7002发送命令,将该key原子性的迁移至7003。循环发送直至所有的槽数据发送完毕。
- 通知其中一个节点,槽15001-16383之间的槽已被7003分配,然后该节点会告知所有节点,自此完成了数据的迁移。
3.4.2 解决指派中数据访问的问题
但是此时有一个问题,如果在迁移过程中有客户端需要对15001-16383之间的key进行操作,那么应该怎么做呢?
首先因为此时其他节点仍认为这些槽在7002(因为步骤5未执行),所以请求此时会被分配到7002节点。
假如此时槽的传输已经到了16000,请求的key属于槽16200,因为16000-16383没有被迁移走,数据仍在7002,则7002节点直接执行即可。但是如果请求的key属于槽15500,很明显数据已经被迁移到了7003节点,则7002会返回AKS错误,并将正确的7003的信息返回给客户端。客户端拿到这个AKS错误后,重新请求7003节点,完成对应的操作、
3.5 故障迁移
集群模式中也有主从节点,可以完成故障自动迁移。与哨兵模式同的是,集群模式下由节点互相判断某个节点是否下线,而不是由额外的哨兵节点判断。
3.5.1 下线判断
对到我们之前的例子,其中7000、7001、7002、7003都是主节点,7004、7005新节点作为7000的从节点。
首先每个节点都会定期向其他节点发送PING
消息,以观测其他节点的状态。
假如此时7001向7000发送PING
消息,7000并没有在规定时间内返回 PONG
,则此时7001会将7000标识为 疑似下线,并向其他节点发送下线报告。
此后7002,7003也会同样将其标识为疑似下线并发送下线报告,若此时7001节点事先收到了其他两个节点的下线报告,并且此时超过下线报告数量已经超过半数,则由7001节点将7000节点标记为已下线,并向其他所有节点发送下线通知,即发送FAIL消息。
3.5.2 故障迁移
当一个从节点发现自己的主节点已经下线时,会进行一下操作:
- 所有的从节点中会选出一个主节点
- 新的主节点会将自己的要复制的节点设为空,标识自己现在是主节点
- 新的主节点将旧的主节点内所有的槽指派给自己
- 新的主节点向其他所有节点发送消息,告知自己是新的主节点,并且槽已经被指派给了自己
- 新的主节点开始接收请求和开始操作
选取主节点的方式如下:
1)集群的配置纪元是一个自增计数器,它的初始值为0。
2)当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一。
3)对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
4)当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
5)如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
6)每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
7)如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。
8)因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
9)如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
转载自:https://juejin.cn/post/7279562572192710716