Skip to content

Redis 集群

litter-fish edited this page Dec 13, 2019 · 1 revision

Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能。

节点

使用cluster meet命令将节点加入到集群节点中,

命令实现:假如节点A收到客户端发送的CLUSTER MEET ,IP和port为节点B

  1. 节点A会为节点B创建一个clusterNode结构,并将该节点添加到自己的clusterState.nodes字典中。
  2. 节点A将根据指定的IP和port,发送一条MEET消息。
  3. 节点B收到节点A的MEET消息后,创建一个clusterNode结构,并将该节点添加到自己的clusterState.nodes字典中。
  4. 节点B向节点A返回一条PONG消息。
  5. 节点A收到PONG消息后,将向节点B发送PING消息。
  6. 节点B收到PING消息后,表示握手完成。 9f474eb06ffe0d4209dc20b8ac66419b.png

槽指派

集群的整个数据库被分成16384个槽,数据库中的每个键都属于这16384个槽中的一个,集群中的每个节点可以处理0个或16384个槽。 当集群中的16384个槽都有节点在处理时,集群处于上线,否则处于下线。

通过向节点发送CLUSTER ADDSLOTS命令,将一个或多个槽指派给其负责。

typedef struct clusterNode {
    ......
    // 将数据库分成16384个二进制位,位上的值如果为1表示,该节点处理该槽位,否则不处理。
    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
    // 节点负责处理槽的个数
    int numslots;   /* Number of slots handled by this node */
    ......
} clusterNode;

d9613772dc4384c885d8bef6ae70b805.png

传播节点的槽指派信息 节点除了保存节点自己的处理槽信息外,会通过消息,发送给集群中的其他节点告知节点处理槽,当集群的其他节点收到后会在clusterState.nodes字典中找到对应的节点,更新其处理槽信息

记录集群中所有槽指派信息

typedef struct clusterState {
    .....
    // 记录集群中所有槽的指派信息
    clusterNode *slots[CLUSTER_SLOTS];
    ......
} clusterState;

c74ccbe440c5a1097aac57c40a803eac.png

CLUSTER ADDSLOTS的实现

伪代码: 8ca63c713c3e2201788fcbb9781319bb.png

节点执行:cluster addslots 1 2 0e93054697cdaee69c1ddf94b6a9153f.png

在集群中执行命令

当集群所有槽都被指派后,集群将上线,这时如果向集群发送命令后,集群节点会计算该槽位是否指派给自己。 如果该节点正是指派给当前节点的,则直接处理命令。 否则,节点将向客户端返回一个MOVED错误,指引客户端转向正确的节点,并重新发送之前要执行的命令。 b2b22fc4f929398f6d8463c94fad6118.png

fdbaf6cab899950a673ddd2cbebf7527.png

MOVED错误

MOVED格式: MOVED

  1. 客户端发送命令返回MOVED错误。 c88407bbfecb628a1e236e6351e731b1.png

  2. 客户端根据MOVED,重新转向服务器,并重新发送命令 e9d71f2a62051bcafac604a996370ae5.png

重新分片

将某些属于原来节点(源节点)的槽重新指派给其他节点(目标节点)

实现步骤 假设将属于节点7002的槽重新指派给7003节点。

  1. redis-trib对目标节点发送CLUSTER SETSLOT IMPORTING <source_id>命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。
typedef struct clusterState {
    .....
    // 记录当前节点正在从其他节点导入的槽
    clusterNode *importing_slots_from[CLUSTER_SLOTS];
    ......
} clusterState;

执行上面命令后,会将目标节点(7003)中的importing_slots_from对应槽位(solt)指向源节点(7002)。 300dfe8bf804be0b71927ca6970b44f7.png

  1. redis-trib对源节点发送CLUSTER SETSLOT MIGRATING <target_id>命令,让源节点准备好将属于槽slot的键值对迁移(migrate)到目标节点。
typedef struct clusterState {
    ......
    // 记录将槽迁移至指定节点
    clusterNode *migrating_slots_to[CLUSTER_SLOTS];
    ......
}

将源节点(7002)属性migrating_slots_to的槽(solt)设置为要迁移的目标节点(7003) ec021d5df11bf26413eea35dfcd05243.png

  1. redis-trib向源节点发送CLUSTER GETKEYSINSLOT 命令,获得最多count个属于槽slot的键值对的键名。
  2. 对于步骤3中的每个键名,redis-trib都向源节点发送一个MIGRATE <target_id> <target_port> <key_name> 0 命令, 将被选中的键原子的从源节点迁移到目标节点
  3. 重复执行3、4步骤,直到所有键值对都被迁移完成。
  4. redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT NODE <target_id>命令,告诉其他节点slot槽已经指派给了目标节点。

迁移过程: 8fa857ae54225c4bbf6049c8f9e2577a.png

对槽slot进行重新分片的过程: 1cb106f4306cada6cba1ea3b912d3743.png

ASK错误

当客户端向源节点发送一个与数据库键有关的命令,此时该键正处于迁移过程中时。 10ee3ddf1f00d30f203b5e39e83c727b.png

  1. 首先,客户端发送命令,到源节点执行命令,如果源节点中不存在键,则去正在执行迁移的槽中找,如果存在则返回一个ASK错误 82496f4093904e57cb25c9cb0f4e67ad.png

  2. 根据上一步的返回结果,重新定位,并执行命令 320c15e9db1229d992385b46ba5e81b8.png

过程: 5e635ab15ee6b3c6832b6774a57b9105.png

ASK错误与MOVED错误的区别

MOVED:代表槽的负责权已经从一个节点转移到另外一个节点中。 ASK:代表槽节点正在迁移。

复制和故障转移

复制

设置从节点

  1. 客户端发送命令,CLUSTER REPLICATE <node_id>,让接收命令的节点成为node_id的从节点

  2. 接收到命令的节点,首先在clusterState.nodes字典中找到node_id所对应的节点的clusterNode结构(主节点),并将自己节点的slaveof指向主节点

typedef struct clusterState {
    clusterNode *myself;  /* This node */
    // ......
} clusterState;

typedef struct clusterNode {
    // ......
    // 如果这是一个从节点,则指向他的主节点
    struct clusterNode *slaveof;
    // ......
} clusterNode;

93174cd587b3a68e4ed50ea87a6e8d7a.png

  1. 节点修改自己的flag标志,打开REDIS_NODE_SLAVE标识。

  2. 节点调用复制代码,根据slaveof指向的IP和port,对主节点进行复制。

  3. 同时还会将成为某个节点的从节点,并复制数据这个情况通过消息发送给集群中的其他节点。并更新其节点的从节点信息

typedef struct clusterNode {
    // ......
    // 正在复制这个主节点的从节点个数
    int numslaves;  /* Number of slave nodes, if this is a master */
    // 一个数组,每个数组元素指向复制这个主节点的从节点对象
    struct clusterNode **slaves; /* pointers to slave nodes */
    // ......
} clusterNode;

849ff30ddf2b371459f19c207006fb14.png

故障检测 集群中的每个节点都会定期向集群中的其他节点发送PING命令,以此检测节点是否在线,如果接收到节点未在规定时间内返回PONG消息,那么发送PING消息的节点会将接收PING消息的节点标记为疑似下线,即打开REDIS_NODE_PFAIL标识。

当一个主节点A(7001),通过消息从主节点B(7002)中得知主节点C(7000)疑似下线,主节点A会在clusterState.nodes字典中找到主节点C所对应的clusterNode结构, 并将主节点B的下线报告添加到clusterNode结构的fail_reports中。

typedef struct clusterNodeFailReport {
    // 报告目标节点已经下线的节点
    struct clusterNode *node;  /* Node reporting the failure condition. */
    // 最后一次从node节点收到下线的时间
    mstime_t time;             /* Time of the last report from this node. */
} clusterNodeFailReport;

typedef struct clusterNode {
    // ......
    // 记录其他节点对该节点的下线报告(clusterNodeFailReport)
    list *fail_reports;         /* List of nodes signaling this as failing */
} clusterNode;

主节点中C节点结构: 8661963b6992c900d2377f30c41fd368.png

当超过半数的主节点认为某个主节点x疑似下线,则会将主节点x标记为已下线,并向集群广播主节点x的FAIL消息。 c570373a1e302ff318240ef062dd5626.png

故障转移 当一个从节点发现其主节点已经下线,从节点将开始对主节点进行故障转移:

  1. 复制下线主节点的所有从节点里面,会有一个从节点被选中。

  2. 被选中的节点会指向slaveof no one命令,成为新主节点。

  3. 新的主节点会撤销所有对以下线主节点的槽指派,并将这些槽指派信息指向新主节点。

  4. 新的主节点向集群中广播一条PONG消息,告知集群其他主节点,其成为新的主节点,并更新其相关信息

  5. 新主节点处理槽相关命令请求,完成迁移。

选举新的主节点 一些选举规则: 集群的配置纪元是一个自增计数器,初始值为0 当集群中的某个节点开始一次故障转移时,配置纪元都会自增1, 对于每个配置纪元,集群中每个负责处理槽的主节点都有一次投票机会,而第一个向主节点要求投票的从节点将获取主节点的投票。 在每一个配置纪元,每个具有投票权限的节点,只能投一次票。 如果在一个配置纪元里面,还未选出主节点,那么集群将进入一个新的配置纪元,直至选出新的主节点。

步骤:

  1. 从节点发现自己的主节点下线,从节点会向集群主节点广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求收到消息、具有投票权的主节点向这个从节点投票。

  2. 如果主节点具有投票权,且未向其他从节点投过票,主节点将向从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_OK消息,表示这个从节点支持从节点成为新的主节点。

  3. 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_OK消息,并根据接收到多少条这样的消息统计有多少个主节点支持它。

  4. 如果集群中有N个节点具有投票权限,那么当一个节点获得的票数大于等于N/2 + 1,这个从节点将成为新的主节点

节点发送的消息

MEET PING PONG FAIL PUBLISH
接收者加入到发送者所在的集群中 集群每个节点默认每1秒会从已知节点列表中随机选出5个节点,然后对着5个节点中最长时间没发送过PING消息的节点发送PING消息,以此检测节点是否在线。 告知节点已经收到MEET或PING消息、向集群广播自己的PONG消息,让其他节点刷新节点 节点广播一条节点下线的FAIL消息,收到消息的节点将会标记下线节点 节点收到一个PUBLISH命令时,节点执行命令,并向集群广播一头PUBLISH消息

消息 消息头

typedef struct {
    char sig[4];        /* Signature "RCmb" (Redis Cluster message bus). */
    // 消息长度,包括头和体
    uint32_t totlen;    /* Total length of this message */
    uint16_t ver;       /* Protocol version, currently set to 1. */
    // 发送者端口
    uint16_t port;      /* TCP base port number. */
    // 消息类型
    uint16_t type;      /* Message type */
    // 消息发送消息体包含的信息数量,只在MEET、PING、PONG三种消息时使用
    uint16_t count;     /* Only used for some kind of messages. */
    // 发送者所处的配置纪元
    uint64_t currentEpoch;  /* The epoch accordingly to the sending node. */
    // 如果发送则是主节点,则记录其配置纪元,如果是从节点,则是其复制的主节点的配置纪元
    uint64_t configEpoch;   /* The config epoch if it's a master, or the last
                               epoch advertised by its master if it is a
                               slave. */
    uint64_t offset;    /* Master replication offset if node is a master or
                           processed replication offset if node is a slave. */
    // 发送者名字
    char sender[CLUSTER_NAMELEN]; /* Name of the sender node */
    unsigned char myslots[CLUSTER_SLOTS/8];
    // 如果发送者是一个从节点,则记录其复制主节点的名字,如果是主节点则记录REDIS_NODE_NULL_NAME
    char slaveof[CLUSTER_NAMELEN];
    char myip[NET_IP_STR_LEN];    /* Sender IP, if not all zeroed. */
    char notused1[34];  /* 34 bytes reserved for future usage. */
    uint16_t cport;      /* Sender TCP cluster bus port */
    // 发送者标识
    uint16_t flags;      /* Sender node flags */
    // 发送者所处集群状态
    unsigned char state; /* Cluster state from the POV of the sender */
    unsigned char mflags[3]; /* Message flags: CLUSTERMSG_FLAG[012]_... */
    // 消息体
    union clusterMsgData data;
} clusterMsg;

消息体

  1. MEET、PING、PONG三种消息
typedef struct {
    // 节点的名字
    char nodename[CLUSTER_NAMELEN];
    // 最后一次向这个节点发送Ping的时间戳
    uint32_t ping_sent;
    // 最后一次从这个节点接收到PONG消息的时间戳
    uint32_t pong_received;
    // 节点IP地址
    char ip[NET_IP_STR_LEN];  /* IP address last time it was seen */
    // 节点的端口号
    uint16_t port;              /* base port last time it was seen */
    uint16_t cport;             /* cluster port last time it was seen */
    // 节点的标识符
    uint16_t flags;             /* node->flags copy */
    uint32_t notused1;
} clusterMsgDataGossip;

每次发送MEET、PING、PONG消息时,发送者都是从自己的已知列表中随机选择两个节点,并将这两个节点保存到两个clusterMsgDataGossip结构中。 当接收者接收到MEET、PING、PONG消息时,根据消息体中两个clusterMsgDataGossip结构: 1)如果被选中的节点不存在与接收者已知队列中,提取IP和端口进行节点握手 2)如果已经在已知列表中,则做更新节点操作 46f886b232ee5f61da3da012caee1663.png

  1. FAIL消息 当集群中的某个节点A将节点B标识为已下线时,主节点A将向集群广播一条关于节点B的FAIL消息。
typedef struct {
    // 节点的名字
    char nodename[CLUSTER_NAMELEN];
} clusterMsgDataFail;

节点接收和发送(FAIL)消息的过程 e825a45f7388c447e4eb0e8af5ffa2cb.png

  1. PUBLISH消息 当客户端向集群中的一个节点发送PUBLISH 命令时,将导致集群中的所有节点都向channel频道发送message消息。 9db293c4a254a7c6ecff7d3f929baba0.png
typedef struct {
    
    // publish <channel> <message>
    uint32_t channel_len;
    uint32_t message_len;
    // 数组的0 - channel_len-1 保存channel信息
    // channel_len - channel_len+message_len-1字节保存message信息
    unsigned char bulk_data[8]; /* 8 bytes just as placeholder. */
} clusterMsgDataPublish;

例如: publish "news.it" "hello" f755779cb7e43c6dc97599af237d01a9.png

Clone this wiki locally