redis集群原理

redis集群原理

本文部分图片和文字来源于:https://www.jianshu.com/p/0232236688c1

摘要

redis集群是redis官方提供的分布式缓存方案,通过分片实现数据的集群化,对于redis集群我们需要关注这样几个方面,节点连接、槽指派、命令执行过程、重新分片、转向、故障检测、故障转移、消息通信等。在学习完之后至少需要知道这样几个问题:

  1. 一个集群是如何将一台redis服务器加入到自己的集群的?
  2. 集群使用了什么样的数据结构保存节点之间的状态信息?
  3. 槽指派的过程是怎样的?
  4. 来自客户端的命令请求是怎样被集群处理的?
  5. moved错误和ASK错误是什么?有什么区别
  6. redis集群某个结点发生故障如何进行故障转移
  7. 如何进行故障检测?
  8. 主服务器挂掉后新的主节点的选举过程是怎样的?
  9. 集群中的消息通信手段(命令)

1. 一个集群是如何将一台redis服务器加入到自己的集群的?

节点握手
节点握手

meet命令: cluster meet ip port

CLUSTER MEET命令实现:
1)节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。
2)节点A根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息。
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会被集群中的所有节点认识。
作者:Lucien_168
链接:https://www.jianshu.com/p/0232236688c1
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

2. 集群使用了什么样的数据结构保存节点之间的状态信息?

clusterNode

clusterNode指的是某个节点的状态

// cluster.h

// 集群节点结构体
typedef struct clusterNode {

    // 节点创建时间
    mstime_t ctime;                     

    // 节点ID,长度为40,每一个字符都是一个16进制字符,通过随机数生成
    char name[REDIS_CLUSTER_NAMELEN];   

    // 节点标识,标识节点是 Master 或 Slave 
    int flags;                          

    // 节点当前的配置纪元
    uint64_t configEpoch;               

    // REDIS_CLUSTER_SLOTS:整个集群分块的总数目,即16384
    // Slots是一个二进制位数组(bitarray),数组长度为REDIS_CLUSTER_SLOTS/8=2048个字节
    // 位值1表示对应slot的数据存储在当前节点,是0表示不在这个节点。  
    unsigned char slots[REDIS_CLUSTER_SLOTS/8]; 

    // 当前节点所分配的 slot 总数
    int numslots; 

    // 若当前节点为 Master,则表示相应 Slave 的数据
    int numslaves; 

    // 指针数组,指向 Slave 节点
    struct clusterNode **slaves;

    // 指正数组,指向 Master 节点
    struct clusterNode *slaveof;

    // 最近一次发送 ping包的时间
    mstime_t ping_sent;  

    // 最近一次接受 pong包的时间
    mstime_t pong_received;

    // 最近一次被设置成 fail状态的时间
    mstime_t fail_time; 

    // 最近一次为某个 Salve 投票的时间
    mstime_t voted_time;    

    // 最近一次从这个节点接收到复制偏移量的时间 
    mstime_t repl_offset_time;

    // 当前节点的复制偏移量 
    PORT_LONGLONG repl_offset;

    // 当前节点的 IP
    char ip[REDIS_IP_STR_LEN];

    // 当前节点的 Port
    int port;  

    // 保存连接相关的信息  
    clusterLink *link;

    // 一个链表,记录了所有其他节点对该节点的下线报告 
    list *fail_reports;

} clusterNode;

clusterStates

clusterStates指的是当前节点在集群中所处的状态

// cluster.h
// 节点状态信息
typedef struct clusterState {
    // 当前节点
    clusterNode *myself;

    // 集群当前的配置纪元
    uint64_t currentEpoch;

    // 状态标识:OK?FAIL?
    int state;          

    // 集群中至少拥有一个 slot 的 Master 的数目
    int size;   

    // 整个集群所有节点,键:节点ID,值:clusterNode结构体
    dict *nodes; 

    // 节点黑名单
    dict *nodes_black_list;

    // 记录从当前节点迁移至目标节点的 slot,以及迁移的目标节点  
    // migrating_slots_to[i] = NULL 表示slot i未被迁移  
    // migrating_slots_to[i] = clusterNode_A 表示slot i要从本节点迁移至节点 A
    // REDIS_CLUSTER_SLOTS:16384
    clusterNode *migrating_slots_to[REDIS_CLUSTER_SLOTS];

    // 记录要从源节点迁移到本节点的 slot,以及进行迁移的源节点  
    // importing_slots_from[i] = NULL 表示slot i未进行导入  
    // importing_slots_from[i] = clusterNode_A 表示正从节点 A中导入slot i  
    clusterNode *importing_slots_from[REDIS_CLUSTER_SLOTS];

    // 记录各节点所分配的 slot
    // 例如 slots[i] = clusterNode_A表示slot i 由节点 A处理  
    clusterNode *slots[REDIS_CLUSTER_SLOTS];

    // 跳跃表
    zskiplist *slots_to_keys;


    // -------------------以下这些域被用于进行故障转移选举
    // 上次执行选举或者下次执行选举的时间  
    mstime_t failover_auth_time;

    // 节点收到的投票数目
    int failover_auth_count;

    // True:当前节点已经向其他节点发送投票请求
    int failover_auth_sent; 

    // Slave 在当前故障转移选举中的排名
    int failover_auth_rank; 

    // 当前选举的纪元
    uint64_t failover_auth_epoch; 

    // Slave 不能执行故障转移选举的原因
    int cant_failover_reason;  


    // -------------------共用的手动故障转移状态
    // 手动故障转移时限
    mstime_t mf_end;           


    // -------------------Mater 节点的手动故障转移状态
    // Slave 节点
    clusterNode *mf_slave;      


    // -------------------Slave 节点的手动故障转移状态
    // 指示手动故障转移是否可以开始的标志值  
    PORT_LONGLONG mf_master_offset; 

    // 非0表示开始为选举 Master 投票                                 
    int mf_can_start;          

    // -------------------以下字段由 Master使用,用于记录选举状态
    // 最近一次投票的纪元
    uint64_t lastVoteEpoch;   

    // clusterBeforeSleep() 需要做的事情
    int todo_before_sleep;

    // 通过 cluster bus 发送消息的数目 
    PORT_LONGLONG stats_bus_messages_sent;

    // 通过 cluster bus 接收消息的数目 
    PORT_LONGLONG stats_bus_messages_received;

} clusterState;

redis集群数据结构

3. 槽指派的过程是什么样的?

槽指派过程
命令:cluster addslots 0,1,2…5000

什么是槽

答:Redis Cluster中有一个16384长度的槽的概念,他们的编号为0、1、2、3……16382、16383。这个槽是一个虚拟的槽,并不是真正存在的。正常工作的时候,Redis Cluster中的每个Master节点都会负责一部分的槽,当有某个key被映射到某个Master负责的槽,那么这个Master负责为这个key提供服务,至于哪个Master节点负责哪个槽,这是可以由用户指定的,也可以在初始化的时候自动生成(redis-trib.rb脚本)。这里值得一提的是,在Redis Cluster中,只有Master才拥有槽的所有权,如果是某个Master的slave,这个slave只负责槽的使用,但是没有所有权。

记录节点的槽指派信息

clusterNode中有两个属性slots和numslots,slots是一个位图,长度为2048,一共有16384(2048*8)个插槽,如果某一位上为1,代表该clusterNode会处理这个槽。numslots代表该节点所处理插槽的数量。
clusterstates中同样有一个slots数组,但是和上面不同,slot类型是clusterNode,每个元素的值代表所指向的clusterNode。

传播节点的槽指派信息

redis节点的槽指派信息
一个节点除了记录自己负责处理的槽之外,还会将自己负责处理哪些槽的信息传播给其他节点,以此来告知其他节点自己目前负责处理哪些槽。

4. 来自客户端的命令请求是怎样被集群处理的

从客户端来看,集群应该是一个整体,具备高可用性、高性能。一个客户端向集群中的某个节点发起命令请求之后,集群会执行相应的查找,不一定会在当前节点被处理,有可能会重定向到其他节点。

集群处理命令过程

  1. 客户端向集群发出一条消息。
  2. 当前节点中是否有所需要的插槽。
  3. 如果没有的话返回一个moved错误,并引导重定向到具有这个插槽的节点。

问题拓展

  1. 如何计算命令属于哪个插槽
  2. 如何判断槽是否由当前节点处理
  3. moved错误是怎么回事

5. moved错误和ask错误有什么区别

moved错误是一个转向的命令,在当前节点检查完自己的ckusterNode后发现自己不处理这个槽就会出现一个moved错误可以定位到处理这个槽的节点上去。

ask错误是在重新分片阶段,可能会出现的一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
ask错误

6. redis集群中某个结点发生故障如何进行故障转移

为了保证集群的可用性,在搭建过程中一个节点往往都是一主一备甚至一主多备的模式,一旦某个主节点下线,其他几个从结点就会接管主节点的任务,通过选举算法选出一个新的主节点出来,后上线的主节点会变成新的主节点的从节点。

设置从节点

命令:cluster replicate node_id

向一个node_id发送命令,可以成为这个node_id的从节点

过程:

  1. 发送这个命令的节点会首先在自己的clusterState.nodes字典中找到node_id所对应节点的,然后将自己的clusterState.myself.slaveof指针指向这个结构,成为这个node_id节点的从节点
  2. 该节点修改clusterState.myself.flags中的属性为redis_node_slave
  3. 从节点开启复制功能,向主节点复制。于此同时,集群中的其他节点也会接收这个消息。

故障检测

过程:

  1. 节点A向节点B发送ping命令,如果没有收到节点B返回的pong命令,就会将这个节点标记为疑似下线的状态。
  2. 当一个主节点A通过消息得知主节点B认为主节点C进入疑似下线状态后,自己也会把这个节点标记为下线状态。
  3. 如果在一个集群里,半数以上的主节点都将主节点B标记为下线状态,那么就会将这个节点标记为已下线状态并在集群广播这条消息

故障转移

过程:

  1. 从节点中的一个节点会成为一个新的主节点
  2. 新的主节点会撤销对所有已下线主节点的槽指派并转向自己
  3. 发广播

新的主节点选举过程

  1. 集群中有一个配置纪元,本职是一个计数器,每当有一次故障转移发生,计数器就加1
  2. 对于每个配置纪元内,集群中每个负责处理槽的主节点都有一次投票的机会,第一个到达的从节点将获得该主节点的投票。
  3. 当节点发现自己正在复制的主节点下线时,从节点会向集群广播一条消息,要求所有的主节点给他投票,但是这些主节点只有一票,只有谁先到就给谁。
  4. 最后统计如果一个从节点的票数大于N/2+1(N是具有投票权的主节点个数),那么这个从节点称为新的主节点
  5. 如果当前的配置纪元没有选举出一个新的节点,那么进入一个新的配置纪元,并再次选举,知道选出新的主节点为止。