Redis多机数据库—主从复制-Sentinel-集群

读《Redis设计与实现》笔记

一、主从复制

在Redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制(replicate)另一个服务器,被复制的服务器为主服务器(master),而对主服务器进行复制的服务器则被称为从服务器(slave),Redis的复制功能分为同步和命令传播两个操作,

1.1同步

当客户端向从服务器发送SLAVEOF命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,
以下是PSYNC命令的执行步骤:

  1. 从服务器向主服务器发送PSYNC命令
  2. 收到PSYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令
  3. 当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态
  4. 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态

在这里插入图片描述
PSYNC命令具有完整重同步和部分重同步两种模式:

  1. 其中完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步
  2. 而部分重同步则用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。

1.2命令传播

为了让主从服务器再次回到一致状态,主服务器需要对从服务器执行命令传播操作:主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。

1.3部分重同步

部分重同步功能由以下三个部分构成:

  1. 主服务器的复制偏移量和从服务器的复制偏移量;
  2. 主服务器的复制积压缓冲区;
  3. 服务器的运行ID。

1.3.1复制偏移量

执行复制的双方主服务器和从服务器会分别维护一个复制偏移量:

  • 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N
  • 从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N

1.3.2复制积压缓冲区

是由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面,如下图所示:
在这里插入图片描述
因此,主服务器的复制积压缓冲区里面会保存着一部分最近传播的写命令,并且复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量。

当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:

  • 如果offset偏移量之后的数据(也即是偏移量offset+1开始的数据)仍然存在于复制积压缓冲区里面,那么主服务器将对从服务器执行部分重同步操作;
  • 相反,如果offset偏移量之后的数据已经不存在于复制积压缓冲区,那么主服务器将对从服务器执行完整重同步操作。

1.3.3服务器运行ID

每个Redis服务器,不论主服务器还是从服务,都会有自己的运行ID,运行ID在服务器启动时自动生成,由40个随机的十六进制字符组成,

当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,而从服务器则会将这个运行ID保存起来。

当从服务器断线并重新连上一个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID:

  • 如果从服务器保存的运行ID和当前连接的主服务器的运行ID相同,那么说明从服务器断线之前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作
  • 相反地,如果从服务器保存的运行ID和当前连接的主服务器的运行ID并不相同,那么说明从服务器断线之前复制的主服务器并不是当前连接的这个主服务器,主服务器将对从服务器执行完整重同步操作

在这里插入图片描述

1.3心跳检测

在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:REPLCONF ACK <replication_offset>,其中replication_offset是从服务器当前的复制偏移量。发送REPLCONF ACK命令对于主从服务器有三个作用:

  • 检测主从服务器的网络连接状态
  • 辅助实现min-slaves选项
  • 检测命令丢失

辅助实现min-slaves配置选项,Redis的min-slaves-to-write和min-slaves-max-lag两个选项可以防止主服务器在不安全的情况下执行写命令。举个例子,如果我们向主服务器提供以下设置:

min-slaves-to-write 3
min-slaves-max-lag 10

那么在从服务器的数量少于3个,或者三个从服务器的延迟值都大于或等于10秒时,主服务器将拒绝执行写命令。

如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发送REPLCONF ACK命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量,然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数据,并将这些数据重新发送给从服务器。

二、Sentinel

Sentinel是Redis的高可用性解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。
在这里插入图片描述
在这里插入图片描述

2.1启动并初始化sentinel

  1. 初始化服务器
  2. 使用sentinel专有代码
  3. 初始化sentinel状态
  4. 初始化sentinel状态的masters属性
    Sentinel 状态中的 masters 字典记录了所有被监视的主服务器信息,键为服务器名字,值为被监视主服务器对应的sentinel.c/sentinelRedisInstance结构。每个sentinelRedisInstance实例结构代表监视一个Redis服务器实例,这个实例可以是主服务器,也可以是从服务器,或者另外一个sentinel服务器。
    而masters字典的初始化是根据被该入的sentinel配置文件来进行的:
sentinel monitor master1 127.0.0.1 6379 2
sentinel down-after-milliseconds master1 60000
sentinel failover-timeout master1 180000
sentinel parallel-syncs master1 1

sentinel monitor master2 192.168.1.3 6380 4
sentinel down-after-milliseconds master2 10000
sentinel failover-timeout master2 180000
sentinel parallel-syncs master2 5
  • 创建连向主服务器的网络连接
    初始化Sentinel的最后一步是创建连向被监视主服务器的网络连接,Sentinel将成为主服务器的客户端,它可以向主服务器发送命令,并从命令回复中获取相关的信息,对于每个被Sentinel监视的主服务器来说,Sentinel会创建两个连向主服务器的异步网络连接
    • 命令连接,这个连接专门用于向主服务器发送命令,并接收命令回复
    • 订阅连接,这个连接专门用于订阅主服务器的 sentinel :hello频道
      在这里插入图片描述

2.2获取主服务器信息

Sentinel默认会以每十秒一次的频率,通过命令 连接向被监视的主服务器发送INFO命令,并通过分 析INFO命令的回复来获取主服务器的当前信息。
在这里插入图片描述
通过分析主服务器返回的INFO命令回复,可以获取以下两方面的信息:

  • 关于主服务器本身的信息,包括run_id域记录的服务器运行ID,以及 role域记录的服务器角色
  • 关于主服务器属下所有从服务器的信息,每个从服务器都由一个"slave" 字符串开头的行记录,每行的ip=域记录了从服务器的IP地址,而port域则记录了从服务器的端口号。根据这些IP地址和端口号,Sentinel无须用户提供从服务器的地址信息,就可以自动发现从服务器.
    sentinel将分别为三个从服务器创建他们各自的实例结构,并将这些结构保存到主服务器实例结构的slaves字典里面,如下图:
    在这里插入图片描述

2.3获取从服务器信息

当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器 创建相应的实例结构之外,Sentinel还会创建连接到从服务器的命令连接和订阅连接。在创建命令连接之后,sentinel在默认情况下,会以每十秒一次的频率通过命令连接向从服务器发送INFO命令。
INFO回复命令获取的信息:

  • 从服务器的运行ID run_id
  • 从服务器的角色role
  • 主服务器的IP地址master_host,以及主服务器的端口号master_port
  • 主从服务器的连接状态master_link_status
  • 从服务器的优先级slave_priority
  • 从服务器的复制偏移量slave_repl_offset

根据这些信息,sentinel会对从服务器的实例结构进行更新。

2.4向主服务器和从服务器发送信息

默认情况下sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送以下的命令:

PUBLISH _sentinel_:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>, <m_ip>,<m_port>,<m_epoch>"

命令解释:

  • s_ip:Sentinel的IP地址
  • s_port:Sentinel的端口号
  • s_runid:Sentinel的运行ID
  • s_epoch:Sentinel 当前的配置纪元(configuration epoch )
  • m_port:主服务器的端口号
  • m_epoch: 主服务器当前的配置纪元

2.5接收来自数服务器和从服务器的频道信息

当Sentinel与一个主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送以下命令:

SUBSCRIBE _sentinel_:hello

对于监视同一个服务器的多个Sentinel 来说,一个Sentinel发送的信息会被其他 Sentinel接到,这些信息会被用于更新其他Sentinel对发送信息Sentinel的认知,也会被用于更新其他Sentinel对被监视服务器的认知。
在这里插入图片描述
主服务器实例结构中的sentinel字典如下:
在这里插入图片描述
一个sentinel可以通通过分析接收到的频道信息来获知其他sentinel的存在,并通过发送频道信息来让其他sentinel知道自己的存在。
当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会为新Sentinel在sentinels字典中 创建相应的实例结构,还会创建一个连向新Sentinel 的命令连接,而新Sentinel也同样会创建连向这个 Sentinel的命令连接,最终监事同一主服务器的多个sentinel将形成相互连接的网络:
在这里插入图片描述

2.6检测主观下线状态

在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。
Sentinel配置文件中的down-after-milliseconds选项指定了Sentinel判断实例进入主观下线所需的时间长度:如果一个实例在down-after-milliseconds毫秒内,连续向Sentinel返回无效回复,那么Sentinel会修改这个实例所对应的实例结构,在结构的flags属性中打开SRI_S_DOWN标识,以此来表示这个实例已经进入主观下线状态。

2.7检测客观下线状态

当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线 了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里接收到足够数量(配置中设置quorum参数的值)的已下线判断之后,Sentinel就会将主服务器判定为客观下线,并对主服务器执行故障转移操作。
Sentinel使用下面的命令询问其他Sentinel是否同意主服务器已下线:

SENTINEL is-master-down-byaddr <ip> <port> <current_epoch> <runid>

2.8选举领头sentinel

当一个主服务器被判断为客观下线时,监控这个Master服务器的所有Sentinel将会选举出一个领头Sentinel。并由领头Sentinel对客观下线的Master进行故障转移。以下是redis选举领头sentinel的规则和方法:

  1. 所有在线的Sentinel都会有被选为头领Sentinel的资格,换句话说,监视同一个主服务器的多个在线Sentinel中的任何一个都有成为领头Sentinel。
  2. 每次进行头领Sentinel选举之后,不论选举是否成功,所有Sentinel的配置纪元的值都会自增一次,配置纪元实际上就是一个计数器。
  3. 在一个配置纪元所有Sentinel都有一次将某个Sentinel设置为局部头领Sentinel的机会,并且局部领头一旦设置,在这个配置纪元中就不能再次更改。
  4. 每个发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自己设置为局部头领Sentinel。
  5. 当源Sentinel向目标Seninel发送SENTINEL is-master-down-by-addr命令,并且命令中runid不是*而是源Sentinel的运行ID时,这表示源Sentinel要求目标Sentinel将自己设置为局部头领Sentinel。
  6. Sentinel设置局部头领Sentinel的规则是先到先得
  7. 目标Sentinel在接收到SENTINEL is-master-down-by-addr命令之后,将向源Sentinel返回一条命令回复,回复中的leader_runid参数和leader_epoch分别记录了目标Sentinel的局部头领Sentinel的运行ID和配置纪元。
  8. 源Sentinel在接收到目标Sentinel返回的命令之后,会检查回复中leader_epoch参数的值和自己的配置纪元是否相同,如果相同,那么源Sentinel继续取出回复中的leader_runid参数,如果leader_runid参数的值和源Sentinel的运行ID一致,那么标识目标Sentinel将源Sentinel设置为局部头领Sentinel。
  9. 如果某个Sentinel被半数以上的Sentinel设置为局部领头Sentinel,那么这个Sentinel就称为头领Sentinel
  10. 如果在给定时间限制内,没有一个Sentinel被选举为头领Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出头领Sentinel。

2.9故障转义

在选举出头领Sentinel之后,头领Sentinel将对这个下线的服务器执行故障转移操作。

  1. 在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。
    挑选过程:
    1. 删除列表中的所有处于下线或者断线状态的从服务器。
    2. 删除列表中所有最近5秒内没有服务过头领Sentinel的INFO命令的从服务器。
    3. 删除与已下线主服务器连接断开超过down-after-milliseconds * 10 毫秒的从服务器。
    4. 按照优先级进行排序,如果优先级最高的有多台,则按照偏移量最大的排序,如果还有多台,则按照运行ID排序取运行ID最小的从服务器
  2. 让已下线主服务器属下的所有从服务器改为复制新的主服务器。sentinel向从服务器发送slaveof命令来实现。
  3. 将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,他就会成为新的主服务器的从服务器。

三、集群

3.1集群数据结构

每个节点都保存着一个 clusterState 结构, 这个结构记录了在当前节点的视角下, 集群目前所处的状态 —— 比如集群是在线还是下线, 集群包含多少个节点, 集群当前的配置纪元, 诸如此类:

typedef struct clusterState {
    
    
    // 指向当前节点的指针
    clusterNode *myself;
    // 集群当前的配置纪元,用于实现故障转移
    uint64_t currentEpoch;
    // 集群当前的状态:是在线还是下线
    int state;
    // 集群中至少处理着一个槽的节点的数量
    int size;
    // 集群节点名单(包括 myself 节点)
    // 字典的键为节点的名字,字典的值为节点对应的 clusterNode 结构
    dict *nodes;
    // ...
} clusterState;

clusterNode 结构保存了一个节点的当前状态, 每个节点都会使用一个 clusterNode 结构来记录自己的状态:

struct clusterNode {
    
    
    // 创建节点的时间
    mstime_t ctime;
    // 节点的名字,由 40 个十六进制字符组成
    // 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
    char name[REDIS_CLUSTER_NAMELEN];
    // 节点标识
    // 使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
    // 以及节点目前所处的状态(比如在线或者下线)。
    int flags;
    // 节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch;
    // 节点的 IP 地址
    char ip[REDIS_IP_STR_LEN];
    // 节点的端口号
    int port;
    // 保存连接节点所需的有关信息
    clusterLink *link;
    // ...
};

clusterNode 结构的 link 属性是一个 clusterLink 结构, 该结构保存了连接节点所需的有关信息, 比如套接字描述符, 输入缓冲区和输出缓冲区:

typedef struct clusterLink {
    
    
    // 连接的创建时间
    mstime_t ctime;
    // TCP 套接字描述符
    int fd;
    // 输出缓冲区,保存着等待发送给其他节点的消息(message)。
    sds sndbuf;
    // 输入缓冲区,保存着从其他节点接收到的消息。
    sds rcvbuf;
    // 与这个连接相关联的节点,如果没有的话就为 NULL
    struct clusterNode *node;
} clusterLink;

下图结构是从节点 7000 的角度记录了集群、以及集群包含的7000、7001 、7002 三个节点的当前状态:
在这里插入图片描述

3.2CLUSTER MEET命令的实现

通过向节点 A 发送 CLUSTER MEET 命令, 客户端可以让接收命令的节点 A 将另一个节点 B 添加到节点 A 当前所在的集群里面:

CLUSTER MEET <ip> <port>

收到命令的节点 A 将与节点 B 进行握手(handshake), 以此来确认彼此的存在, 并为将来的进一步通信打好基础:

  1. 节点 A 会为节点 B 创建一个 clusterNode 结构, 并将该结构添加到自己的clusterState.nodes字典里面。
  2. 之后, 节点 A 将根据 CLUSTER MEET 命令给定的 IP 地址和端口号, 向节点 B 发送一条 MEET 消息(message)。
  3. 如果一切顺利, 节点 B 将接收到节点 A 发送的 MEET 消息, 节点 B 会为节点 A 创建一个 clusterNode 结构, 并将该结构添加到自己的 clusterState.nodes 字典里面。
  4. 之后, 节点 B 将向节点 A 返回一条 PONG 消息。
  5. 如果一切顺利, 节点 A 将接收到节点 B 返回的 PONG 消息, 通过这条 PONG 消息节点 A 可以知道节点 B 已经成功地接收到了自己发送的 MEET 消息。
  6. 之后, 节点 A 将向节点 B 返回一条 PING 消息。
  7. 如果一切顺利, 节点 B 将接收到节点 A 返回的 PING 消息, 通过这条 PING 消息节点 B 可以知道节点 A 已经成功地接收到了自己返回的 PONG 消息, 握手完成。
    在这里插入图片描述
  8. 之后, 节点 A 会将节点 B 的信息通过 Gossip 协议传播给集群中的其他节点, 让其他节点也与节点 B 进行握手, 最终, 经过一段时间之后, 节点 B 会被集群中的所有节点认识。

3.3槽指派

Redis集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被划分为16384个槽(slot),数据库中的每个键都属于这16384个槽中的一个,每个节点可以处理0~16384个槽。当数据库中16384个槽都有节点处理时集群处理上线状态,反之如果有任意一个槽没有得到处理则集群处于下线状态。
执行一下命令可以将槽0到槽5000指派给节点7000负责:

127.0.0.1:7000>cluster addslots 0 1 2 3 4 ... 5000

在cluster.h/clusterNode结构中保存了每个集群节点的状态,包括当前节点所处理的槽信息:

struct clusterNode {
    
    
    //...
    // 由这个节点负责处理的槽
    // 一共有16384/8个字节长
    // 每个字节的每个位记录了一个槽的保存状态
    // 位的值为1表示槽正由本节点处理,值为0则表示槽并非本节点处理
    // 比如slots[0]的第一个位保存了槽0的保存情况
    // slots[0] 的第二个位保存了槽1的保存情况,以此类推
    unsigned char slots[REDIS_CLUSTER_SLOTS/8];
    // 该节点负责处理的槽数量
    int numslots;
    //...
}clusterNode;

这样就可以通过节点的slots属性,以O(1)的时间复杂度判断出当前节点是否处理某个槽:
在这里插入图片描述
一个节点还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。
但如果只将槽指派信息保存在各个节点的clusterNode.slots数组里,当想了解槽i是否被指派时需要遍历每个节点,整个过程的负责度为O(N)。为达到简单高效的目的,Redis在时在cluster.h/clusterState结构中保存了每个槽的指派信息:

typedef struct clusterState {
    
    
    //...    
    // 负责处理各个槽的节点
    // 例如 slots[i] = clusterNode_A 表示槽 i 由节点 A 处理
    clusterNode *slots[REDIS_CLUSTER_SLOTS];
    ///...
}

在这里插入图片描述
通过这种方式,程序检查节点是否负责处理某个槽,或者是将某个槽指派给某个节点的时间复杂度都为O(1)。

为什么使用槽类指派数据存储?而不是直接均匀分布在集群中的每台主服务器中,采用hash(key)%集群数量
1、可以指定给某台服务器分多少个槽,槽分得少,服务器存储数据就少,槽分得多,服务器存储数据就多,灵活指派。
2、数据迁移可以以槽为单位进行,否则就要以整个集群中的节点为单位进行数据迁移。

3.4集群中执行命令

当客户端向节点发送数据库键相关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,然后检查槽是否指派给了自己:

  1. 如果指派给了自己,则节点直接执行该命令;
  2. 如果没有指派给自己,向客户端返回一个moved错误,指引客户端转向到正确的节点,客户端收到错误之后哦,再次发送之前想要执行的命令。
    在这里插入图片描述

3.5计算键属于哪个槽

数据库中的每个键都属于这 16384 个哈希槽的其中一个, 集群使用公式 CRC16(key) &16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC-16 校验和 。

3.6重新分片

节点和单机服务器在数据库方面的一个区别是:节点只能使用0号数据,而单机redis服务器则没有这一限制。
下图展示节点7000的数据库状态,数据库中包含列表键"lst",哈希键"book",以及字符串键"date",其中键"lst"和键"book"带有过期时间。
在这里插入图片描述
除了将键值对保存在数据库里面之外,节点还会用clusterState结构中的slots_to_keys跳跃表来保存槽和键之间的关系:

typedef struct clusterState {
    
    
  // ...
  zskiplist *slots_to_keys;
  // ...
} clusterState;

slots_to_keys跳跃表每个节点的分值(score)都是一个槽号,而每个节点的成员(member)都是一个数据库键:
在这里插入图片描述
通过在slots_to_keys跳跃表中记录各个数据库键所属的槽,节点可以很方便地对属于某个或某些槽的所有数据库键进行批量操作,例如命令CLUSTER GETKEYSINSLOT 命令可以返回最多count个属于槽slot的数据库键(运用在重新分片的时候,将某个槽的count个键迁移到新的服务器),而这个命令就是通过遍历slots_to_keys跳跃表来实现的。

Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。重新分片操作可以在线(online)进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。

Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。
在这里插入图片描述

3.7ASK错误

在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。

当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:

  • 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。
  • 相反地,如果源节点没能在自己的数据库里面找到指定的键,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。
    在这里插入图片描述

ASK错误和MOVED错误的区别:

  • MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点。
  • 与此相反,ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现。

3.8复制与故障转移

3.8.1故障检测

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线。

如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。

3.8.2故障转移

当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:

  1. 复制下线主节点的所有从节点里面,会有一个从节点被选中。
  2. 被选中的从节点会执行SLAVEOF no one命令,成为新的主节点。
  3. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
  4. 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
  5. 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

3.8.3选举新的主节点

新的主节点是通过选举产生的,以下是集群选举新的主节点的方法:

  1. 集群的配置纪元是一个自增计数器,它的初始值为0。
  2. 当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一。
  3. 对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票
  4. 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
  5. 如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条消息,表示这个主节点支持从节点成为新的主节点。
  6. 每个参与选举的从节点都会接收消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
  7. 如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点
  8. 因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
  9. 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

这个选举新主节点的方法和前面介绍的选举领头Sentinel的方法非常相似,因为两者都是基于Raft算法的领头选举方法来实现的。

一般来说,满足上述条件的从节点不会立即发起选举,而是会等待一个随机时间,然后才尝试选举,等待的时间:

DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms

一定的延迟确保我们等待FAIL状态在集群中传播,否则其他masters或许尚未意识到FAIL状态,可能会拒绝投票。延迟的时间是随机的,避免slaves同时开始选举。

SLAVE_RANK表示此slave已经从master复制数据的总量的rank。当master失效时,slaves之间交换消息以尽可能的构建rank,持有replication offset最新的rank为0,第二最新的为1,依次轮推。由公式可见,持有最新数据的slave将会首先发起选举(理论上)。 如果一个持有较小rank的slave选举失败,其他slaves将会稍后继续。

3.9消息

节点发送的消息主要有以下五种:

  • MEET消息:当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
  • PING 消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING 消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A 最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,这可以防止节点A因为长时间没有随机选中节点B作为PING 消息的发送对象而导致对节点B的信息更新滞后。
  • PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET 消息或者PING消息已到达,接收者会向发送者返回一条PONG 消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识,例如当一次故障转移操作成功执行之后,新的主节点会向集群广播一条PONG消息,以此来让集群中的其他节点立即知道这个节点已经变成了主节点,并且接管了已下线节点负责的槽。
  • FAIL消息:当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A 会向集群广播一条关于节点B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
  • PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。

猜你喜欢

转载自blog.csdn.net/lihuayong/article/details/107747139