主从复制
在Redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制另一个服务器,我们称被复制的服务器为主服务器,而对主服务器进行复制的服务器被称为从服务器。
进行复制中的主从服务器双方的数据库将保存相同的数据,概念上将这种现象称作“数据库一状态一致”,或者简称“一致”。
Redis在2.8版本之前使用旧版复制,从2.8版本开始使用新版复制,旧版复制存在短线重连后的效率低下问题,而新版则通过部分重同步解决了这一问题,一下将简要学习旧版、新版复制的实现原理。
旧版复制
Redis的复制功能分为同步和命令传播两个操作:
- 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
- 命令传播操作则用于在主服务器的数据库状态被修改,导致从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态。
同步
当客户端向从服务器发送SLAVEOF命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,也就是将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
步骤如下:
- 从服务器向主服务器发送SYNC命令。
- 收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
- 当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
- 主服务器将记录在缓存区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。
命令传播
在同步操作执行完毕之后,主从服务器两者的数据库将达到一致状态,但这种一致并不是一成不变的,每当主服务器执行客户端发送的写命令时,主服务区ide数据库就有哦可能会被修改,并导致主从服务器状态不再一致。
为了让主从服务器再次回到一致状态,主服务器需要对从服务器执行命令传播操作:主服务器会将自己执行的写命令,即造成主从服务器不一致的那条写命令,发送给从服务器执行,每当从服务器执行了相同的写命令之后,主从服务器再次回到一致状态。
旧版复制的缺陷
在Redis中从服务器对主服务器的复制可以分为以下两种情况:
- 初次复制:从服务器以前没有复制过任何主服务器,或者从服务器当前要复制的主服务器和上一次复制的主服务器不同。
- 断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连接重连上了主服务器,并继续复制主服务器。
对于初次复制来说,旧版复制功能能够很好地完成任务,但对于断线后重复制来说,旧版复制虽然也能让主从服务器重新回到一致状态,但效率却非常低。
因为不管断线了的从服务器已经保存有多少已有的主服务器的数据库状态,都必须向主服务器发送SYNC命令,主服务器接收到SYNC后会执行BGSAVE命令来生成RDB文件,这一系列步骤非常消耗资源,接下来学习新版复制,了解新版复制是如何解决这一问题的。
新版复制
为了解决旧版复制功能在处理短线重复制情况时的低效问题,Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。
PSYNC命令具有完整重同步和部分重同步两种模式:
- 完整重同步:执行步骤和SYNC命令的执行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓存区里面的写命令来进行同步。
- 部分重同步:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。
对比SYNC命令和PSYNC命令处理断线重复制的方法,不难看出,虽然SYNC命令和PSYNC命令都可以让断线的主从服务器重新回到一致的状态,但执行部分重同步所需的资源比起执行SUNC命令所需的资源要少得多,完成同步的速度也快很多。执行SYNC命令需要生成、传送和载入整个RDB文件,而部分重同步只需要将从服务器缺少的写命令发送给从服务器执行就可以了。
部分重同步的实现
部分重同步功能由复制偏移量、复制积压缓冲区和运行ID组成。
复制偏移量:
执行复制的双方会分别维护一个复制偏移量,用于记录当前已经复制的字节数。
- 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
- 从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N。
通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态。因为如果主从服务器两者的偏移量不相同,说明主从服务器不是一致状态。
在这种断线的情况下,主从服务器之间可通过利用积压缓冲区的缓冲命令来实现部分重同步。
复制积压缓冲区:是由主服务器维护的一个固定长度先进先出队列,默认大小为1MB。
当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里,因此主服务器的复制积压缓冲区里面会保存着一部分最近传播的写命令,并且缓冲区会为队列中的每个字节记录相应的复制偏移量,当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作。
如果offset偏移量之后的数据仍存在于复制积压缓冲区里面,那么主服务器将对从服务器执行部分重同步操作;如果数据已不存在于复制积压缓冲区,则执行完整重同步操作。
心跳检测
在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:REPLCONF ACK <replication_offset> 其中replication_offset是从服务器当前的复制偏移量。发送该命令的作用有如下三个:
- 检测主从服务器的网络连接状态:如果主服务器超过一秒钟没有收到从服务器发来的命令,那么主服务器就知道主从服务器之间的连接出现问题了。
- 辅助实现min-slaves选项:可以通过配置Redis的min-slaves-to-write和min-slaves-max-log两个选项,使得当主服务器在从服务器小于x个或者x个从服务器的延迟值都大于y秒时,主服务器将拒绝写命令。
- 检测命令丢失:如果因为网络故障,主服务器发送给从服务器的写命令在半路丢失,那么当从服务器向主服务器发送REPLCONF ACK命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量,然后主服务器会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数据,并将这些数据重新发送给从服务器。
主从复制demo体验
# 注意!5.0之前使用slaoveof 5.0之后使用replicaof
# 可通过replica-read-only yes 配置从服务器为只读
# 步骤如下:
# 1、开启主服务器6379
# 2、开启从服务器12345
# 3、通过cli客户端向服务器12345发送SLAVEOF命令
127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
# 4、通过cli客户端对主服务器进行一条写命令
127.0.0.1:6379> set hello masterAndSlave
# 5、在从服务器端获取hello这个key的值
127.0.0.1:12345> get hello
哨兵Sentinel
Sentinel-哨兵是Redis的高可用性解决方案:由一个或多个哨兵实例组成的哨兵系统可以监视任意多个主服务器以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
哨兵模式工作原理
Sentinel本质上其实是一个运行在特殊模式下的Redis服务器,它使用sentinel.conf作为其配置文件。哨兵服务器在创建后会默认以每十秒一次的频率,通过命令连接向被监视的主、从服务器发送INFO命令,并通过分析INFO命令的回复来获取主、从服务器的当前信息;且哨兵会以每两秒一次的频率,通过命令连接向所有被监视的主从服务器发送发布/订阅命令来获取节点的信息;而且哨兵会以每秒一次的频率向所有与它创建了命令连接的实例(包括主、从服务器和其他哨兵在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。
主观下线和客观下线
如前所述,哨兵会以每秒一次的频率发送PING命令,当一个哨兵节点因某些原因(网络波动、实例宕机等)在一定的毫秒内没有接收到回复,那么这个实例就会被哨兵判定为主观下线,且这个毫秒数可以通过配置文件中的down-after-milliseconds选项配置。
当哨兵将一个主服务器判定为主观下线后,为了确认这个主服务器是否真的下线,它会向同样监视这一主服务器的其他哨兵进行询问,看它们是否也认为该主服务器已经进入下线状态(主观下线或者客观下线)。当哨兵从其他哨兵节点哪里接收到足够数量的已下线判断之后,该哨兵节点就会把主服务器判定为客观下线,并对主服务器进行故障转移操作;而判定需要多少个哨兵认为该主服务器下线就可以判定为客观下线的哨兵节点数量,可以通过配置文件的quorum参数配置。
以上不同的哨兵节点,可以有不同的配置,也就是不同哨兵节点,可以有不同的主观下线判定毫秒数和不同的客观下线判定哨兵节点数量。
选举领头哨兵
当一个主服务器被判定为客观下线时,监视这个下线的主服务器的各个哨兵会进行协商,选举出一个领头哨兵,并由领头哨兵对下线主服务器执行故障转移操作。
选举规则如下:
- 所有在线的哨兵都有被选为领头哨兵的资格。
- 每次进行领头哨兵选举之后,不论是否选举成功,所有哨兵的配置纪元的值
epoch都会自增一次。 - 在一个配置纪元里面,所有哨兵都有一次将某个哨兵设置为局部领头哨兵的机会,并且局部领头一旦被设置,在这个配置纪元里面就不能再更改。
- 每个发现主服务器进入客观下线的哨兵都会要求其他哨兵将自己设置为局部领头哨兵。
- 当一个哨兵向另一个哨兵发送
SENTINEL is-master-down-by-addr命令,并且命令中的runid参数不是*符号而是源哨兵的运行ID时,这表示源哨兵要求目标哨兵将前者设置后者的局部领头哨兵。 - 哨兵设置局部领头哨兵的规则是先导先得。
- 目标哨兵在接收到
SENTINEL is-master-down-by-addr命令后,将向源哨兵返回一条命令回复,回复中的leader——runid参数和leader_epoch参数分别记录了目标哨兵的局部领头哨兵的运行ID和配置纪元。 - 源哨兵在接收到目标哨兵返回的命令回复之后,会检查回复中的
leader_epoch参数的值和自己的配置纪元是否相同,如果相同的话,那么源哨兵继续取出回复中的leader_runid参数,如果leader_runid参数的值和源哨兵的运行ID一致,那么表示目标哨兵将源哨兵设置成了局部领头哨兵。 - 如果有某个哨兵被半数以上的哨兵设置为局部领头哨兵,那么这个哨兵成为领头哨兵。
- 因为领头哨兵的产生需要半数以上的哨兵支持,并且每个哨兵在每个配置纪元里面只能设置一次局部领头哨兵,所以在一个配置纪元里面,只会出现一个领头哨兵。
- 如果在给定的时限内,没有一个哨兵被选举为领头哨兵,那么各个哨兵将在一段时间之后再次进行选举,直到选出领头哨兵为止。
领头哨兵选举例子如下:
- 每个主观下线的哨兵节点向其他哨兵节点发送上面
SENTINEL is-master-down-by-addr host port runid命令,要求将它设置为领导者。 - 收到命令的哨兵节点如果还没有同意过其他的哨兵发送的命令(即还未投过票),那么就会同意,否则拒绝。
- 如果该哨兵节点发现自己的票数已经过半且达到了quorum的值,就会成为领头哨兵。
- 如果这个过程没有一个哨兵被选举为领头哨兵,则会等待一段时间重新选举。
故障转移
在选举出领头哨兵之后,领头哨兵将对已下线的主服务器执行故障转移操作,该操作包含以下三个步骤:
- 在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。
- 让已下线的主服务器属下的所有从服务器改为复制新的主服务器。
- 将已下线的主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。
哨兵demo体验
sentinel.conf #作为哨兵节点的配置文件
# 启动哨兵节点,监控主服务器
sentinel monitor mymaster 127.0.0.1 6379 1
# 也可以通过这个方式来启动
redis-server sentinel.conf --sentinel
# 可通过info获取主从复制信息
info replication
集群
Redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。Redis集群不需要哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中 心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的 性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。
集群工作原理
Redis 集群将所有数据划分为16384个slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。当 Redis集群的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个key时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。
槽位定位算法
Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。
HASH_SLOT = CRC16(key) mod 16384
跳转重定位
当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽位映射表。
重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,并且相关槽所属的键值对也会从源节点被移动到目标节点。重新分片操作可以在线进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。
Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。
步骤如下:
- redis-trib对目标节点发送
CLUSTER SETSLOT <slot> IMPORTING <source_id>命令,让目标节点准备好从源节点导入属于槽slot的键值对。 - redis-trib对源节点发送
CLUSTER SETSLOT <slot> MIGRATING <target_id>命令,让源节点准备好将属于槽slot的键值对迁移至目标节点。 - redis-trib向源节点发送
CLUSTER GETKEYSINSLOT <slot> <count>命令,获得最多count个属于槽slot的键值对的键名。 - 对于步骤3获得的每个键名,redis-trib都向源节点发送一个
MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>命令,将被选中的键原子地从源节点迁移至目标节点。 - 重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。
- redis-trib向集群中的任意一个节点发送
CLUSTER SETSLOT <slot> NODE <target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点。
ASK错误
在进行重新分片期间,源节点想目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里,而另一部分被保存在目标节点里。当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时,源节点会现在自己的数据库里面查找指定的键,如果找到就直接执行客户端发送的命令,如果没能找到,那么这个键有可能已经迁移到目标节点,这时源节点向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并在此发送之前想要执行的命令。
接到ASK错误的客户端会根据错误提供的IP地址和端口号,转向至正在导入槽的目标节点,然后首先向目标节点发送一个ASKING命令,之后再重新发送原本想要执行的命令。(因为如果不带ASKING,这个命令会被直接返回MOVED命令)
复制和故障转移
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定时间内向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(PFAIL)状态。
集群中的各个节点都会通过互相发送消息的方式来交换集群中各个节点的状态信息,例如某个节点是处于在线状态、疑似下线状态(PFAIL),还是已下线状态(FAIL)。
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线,将主节点x标记为已下线的节点都会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。
故障转移
当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,步骤如下:
- 复制下线主节点的所有从节点里面,会有一个从节点被选中。
- 被选中的从节点会执行
SLAVEOF no one命令,成为新的主节点。 - 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
- 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
- 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
选举新的主节点
类似于领头哨兵的选举过程,步骤如下:
- 从节点发现自己的主节点变为FAIL状态
- 将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST信息
- 其他节点收到该信息,只有主节点会响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,在每一个epoch中只发送一次ack
- 尝试failover的从节点收集其他主节点返回的FAILOVER_AUTH_ACK
- 从节点收到超过半数主节点的ack后变成新主节点(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)(如果这一个纪元内没有选举出主节点,则会进入新的纪元并重新选举)
- 新的主节点广播PONG消息通知其他集群节点
消息
redis集群节点间采取gossip协议进行通信,节点发送的消息主要有以下五种:
- MEET:某个节点发送MEET给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信。
- PING:每个节点都会频繁给其他节点发送PING,其中包含自己的状态还有自己维护的集群元数据,互相通过PING交换元数据(类似自己感知到的集群节点增加和移除,哈希槽信息等)。
- PONG: 对PING和MEET消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新。
- FAIL: 某个节点判断另一个节点FAIL之后,就发送FAIL给其他节点,通知其他节点,指定的节点宕机了。
- PUBLISH:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。
集群demo体验
# 启动集群
redis-cli --cluster create --cluster-replicas 1 127.0.0.1:8001 127.0.0.1:8002 127.0.0.1:8003 127.0.0.1:8004 127.0.0.1:8005 127.0.0.1:8006
# 获取集群信息
cluster info
cluster nodes
总结
在上一篇文章里,学习了Redis单机数据库的使用、基本原理,包括持久化、事件,但是单机数据库面临着四个问题:读压力、写压力、数据备份、故障自愈,在这篇文章中,学习了主从复制、哨兵和集群,主从复制解决了读压力和数据备份,哨兵模式在主从的基础上解决了故障自愈,集群则统统包含了主从和哨兵的功能且通过分片和槽分配的原理解决了写压力。