最近私人的事情比较多, 没有抽出时间来整理博客. 书接上文, 上一篇里总结了 Redis 故障迁移的几个关键点, 以及 Redis 中故障检测的实现. 本篇主要介绍集群检测到某主节点下线后, 是如何选举新的主节点的. 注意到 Redis 集群是无中心的, 那么使用分布式一致性的算法来使集群中各节点能对在新主节点的选举上达成共识就是一个比较可行的方案.
在工程上, Raft 一致性算法是比较易于实现和理解的分布式一致性算法; Redis 也是使用了 Raft 来做主节点选举的. 所以这里先简单介绍下 Raft 的原理:
一, Raft 算法
Raft 为解决一致性问题, 引入了 Leader 中心. 简单来说就是通过选举出一个单 Leader, 让 Leader 来接收来自客户端的所有数据, Leader 会将数据分发给所有节点, 当 Leader 判断出数据已经分发到系统中大半节点以后, 即认为该数据已经可以在系统中持久化存储, 并通知客户端该数据提交成功, 后续系统中数据的一致性可以让分布式系统内部通过数据同步的方式实现.(注意到这里和 Redis 去中心化的思想不符, 所以 Redis 只是利用了其中选举的部分)
算法主要部分如下:
1. Leader 选举
2. 客户数据一致化处理(即论文中的 log repliecation 部分)
Leader 选举
Raft 给系统中的节点分配了三种状态, 分别是 Follower, Candidates, Leader; 选举过程即是在分布式式系统中选出 Leader 状态节点的过程. 三种状态的转换关系如下所示:
当系统中没有 Leader 时, 所有节点初始状态均为 Follewer, 每个节点在经历 time_out 时间后, 会自动转变为 Candidate, 并尝试发起投票以便成为新的 Leader; 为了保证系统中只存在一个 Leader, 当选新 Leader 的条件是 Candidate 收到了超过半数节点以上的投票(每个节点在每轮投票中只能投给唯一的节点, 通常是投个第一个发来邀票请求的节点), 达到该条件后, Candidate 即变为 Leader. 注意到投票是有轮次的, 只有收到当前轮次的投票才是有效票. 在状态机中, 用 term 来表示投票的轮次.
根据上面介绍的流程, 容易注意到为实现 Leader 的选举, 有几个前提: 1. 每个节点都需要知道系统中到底有哪些节点存在(为了能向每个节点邀票, 同时要知道节点总数才能判断最终是否收到了多数的投票); 2. 所有节点应该有统一的初始化 term, 且后续需要不断的同步 term
第一点前提, 只需要初始化阶段给所有节点置统一的 term 即可; 而第二点前提则要求各节点主动拥抱新 term, 无情抛弃老 term; 具体来说就是每个节点收到旧 term 的消息, 可以不处理消息请求, 并把自身的较高的 term 返回; 而每个节点收到新的 term 之后, 则要积极响应, 并在响应结束之后更新自己的 term.
有一种可能, 就是在选举轮次中所有的节点都没有收到多数节点的投票认可, 那么就需要发起新一轮的投票; 为了减少新一轮投票依旧无法选出 Leader 的概率, 每个 Candidate 节点的 time_out 时间是随机的, 这样通常会有一个最先发出请求的节点占得先机, 能收获大多数的选票.
客户数据一致化处理
由于 Redis 中并没有用到 Raft 算法的一致化数据处理, 这里不过多描述(该部分内容比较复杂, 需要考虑的异常场景较多); 详细的介绍可以看这篇博文, https://www.cnblogs.com/mindwind/p/5231986.html; 个人觉得这篇博文很直观的介绍了分布式系统下在各种异常情况下, 数据的一致性是如何保证的.
二, Redis 选举新主节点的实现
Redis 选举新的主节点, 有一个很重要的概念需要先理解下, 就是 epoch-- 纪元, 类似于 Raft 中 term 的意义. Redis 的数据结构中记录了两个 epoch, 分别是 currentepoch 和 configepoch; 这两个纪元的作用是不同的.
currentepoch: 这个变量是记录整个集群投票轮次的, 初始时, 集群中所有节点的 currentepoch 都是 0; 后面虽然没个发起投票的节点都会自增该值, 但也会同时将该值作为投票轮次信息发给其他节点, 整个集群最终还是会具有相同的 currentepoch 值;
configepoch: 这个变量是记录节点自身的特征值, 主要用于判断槽的归属节点是否需要更新; 考虑如下的场景, A 节点负责槽 1,2,3 且 configepoch=n,A 节点掉线后从属节点 B 接替了 A 的工作成为新的主节点, 那么 B 此时就负责 1,2,3 槽, B 的 configepoch=n1(n1 一定大于 n, 因为每当有从节点通过故障迁移接替主节点工作时, 该从节点的 configepoch 就会变更为整个集群中最大的 configepoch); 当 A 节点恢复工作后, 还不知道自己已经被替代了, 还向其他节点宣称自己是 1,2,3 槽的负责节点. 其他节点已将 1,2,3 槽的负责节点改为 B 了, 当其他节点收到 A 恢复之后的心跳包, 会比较 1,2,3 槽所属节点 B 的 configepoch(=n1)与 A 的 configepoch(=n)哪个更大, 发现 n1 更大就不会变更自己的记录, 反过来还要通知 A 它所负责的槽已经被接管了.
介绍下 Redis 选举主节点的流程:
1. 从节点检测到自己从属的主节点下线, 开始发起一次选举; 发起方式是向所有主节点广播发送一条投票请求, 希望其他主节点同意自己为新的主节点;(带上自己记录的 currentepoch, 即 Raft 算法中的 term), 源码中如下:
- /*
- 从节点的故障转移, 是在函数 clusterHandleSlaveFailover 中处理的, 该函数在集群定时器函数 clusterCron 中调用. 本函数
- 用于处理从节点进行故障转移的整个流程, 包括: 判断是否可以发起选举; 判断选举是否超时; 判断自己是否拉
- 到了足够的选票; 使自己升级为新的主节点这些所有流程.
- */
- //slave 调用
- void clusterHandleSlaveFailover(void) { //clusterBeforeSleep 对 CLUSTER_TODO_HANDLE_FAILOVER 状态的处理, 或者 clusterCron 中实时处理
- // 也就是当前从节点与主节点已经断链了多长时间, 从通过 ping pong 超时, 检测到本 slave 的 master 掉线了, 从这时候开始算
- mstime_t data_age;
- // 该变量表示距离发起故障转移流程, 已经过去了多少时间;
- mstime_t auth_age = mstime() - server.cluster->failover_auth_time;
- // 该变量表示当前从节点必须至少获得多少选票, 才能成为新的主节点
- int needed_quorum = (server.cluster->size / 2) + 1;
- // 表示是否是管理员手动触发的故障转移流程;
- int manual_failover = server.cluster->mf_end != 0 &&
- server.cluster->mf_can_start; // 说明向从发送了 cluster failover force 要求该从进行强制故障转移
- int j;
- // 该变量表示故障转移流程 (发起投票, 等待回应) 的超时时间, 超过该时间后还没有获得足够的选票, 则表示本次故障转移失败;
- mstime_t auth_timeout,
- // 该变量表示判断是否可以开始下一次故障转移流程的时间, 只有距离上一次发起故障转移时, 已经超过 auth_retry_time 之后,
- // 才表示可以开始下一次故障转移了(auth_age> auth_retry_time);
- auth_retry_time;
- server.cluster->todo_before_sleep &= ~CLUSTER_TODO_HANDLE_FAILOVER;
- /* Compute the failover timeout (the max time we have to send votes
- * and wait for replies), and the failover retry time (the time to wait
- * before waiting again.
- *
- * Timeout is MIN(NODE_TIMEOUT*2,2000) milliseconds.
- * Retry is two times the Timeout.
- */
- auth_timeout = server.cluster_node_timeout*2;
- if (auth_timeout <2000) auth_timeout = 2000;
- auth_retry_time = auth_timeout*2;
- /* Pre conditions to run the function, that must be met both in case
- * of an automatic or manual failover:
- * 1) We are a slave.
- * 2) Our master is flagged as FAIL, or this is a manual failover.
- * 3) It is serving slots. */
- /*
- 当前节点是主节点; 当前节点是从节点但是没有主节点; 当前节点的主节点不处于下线状态并且不是手动强制进行故障转移;
- 当前节点的主节点没有负责的槽位. 满足以上任一条件, 则不能进行故障转移, 直接返回即可;
- */
- if (nodeIsMaster(myself) ||
- myself->slaveof == NULL ||
- (!nodeFailed(myself->slaveof) && !manual_failover) ||
- myself->slaveof->numslots == 0) {
- // 真正把 slaveof 置为 NULL 在后面真正备选举为主的时候设置, 见后面的 replicationUnsetMaster
- /* There are no reasons to failover, so we set the reason why we
- * are returning without failing over to NONE. */
- server.cluster->cant_failover_reason = REDIS_CLUSTER_CANT_FAILOVER_NONE;
- return;
- };
- //slave 从节点进行后续处理, 并且和主服务器断开了连接
- /* Set data_age to the number of seconds we are disconnected from
- * the master. */
- // 将 data_age 设置为从节点与主节点的断开秒数
- if (server.repl_state == REDIS_REPL_CONNECTED) { // 如果主从之间是因为网络不通引起的, read 判断不出 epoll err 事件, 则状态为这个
- data_age = (mstime_t)(server.unixtime - server.master->lastinteraction)
- * 1000; // 也就是当前从节点与主节点最后一次通信过了多久了
- } else {
- // 这里一般都是直接 kill 主 master 进程, 从 epoll err 感知到了, 会在 replicationHandleMasterDisconnection 把状态置为 REDIS_REPL_CONNECT
- // 本从节点和主节点断开了多久,
- data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000;
- }
- /* Remove the node timeout from the data age as it is fine that we are
- * disconnected from our master at least for the time it was down to be
- * flagged as FAIL, that's the baseline. */
- // node timeout 的时间不计入断线时间之内 如果 data_age 大于 server.cluster_node_timeout, 则从 data_age 中
- // 减去 server.cluster_node_timeout, 因为经过 server.cluster_node_timeout 时间没有收到主节点的 PING 回复, 才会将其标记为 PFAIL
- if (data_age> server.cluster_node_timeout)
- data_age -= server.cluster_node_timeout; // 从通过 ping pong 超时, 检测到本 slave 的 master 掉线了, 从这时候开始算
- /* Check if our data is recent enough. For now we just use a fixed
- * constant of ten times the node timeout since the cluster should
- * react much faster to a master down.
- *
- * Check bypassed for manual failovers. */
- // 检查这个从节点的数据是否较新:
- // 目前的检测办法是断线时间不能超过 node timeout 的十倍
- /* data_age 主要用于判断当前从节点的数据新鲜度; 如果 data_age 超过了一定时间, 表示当前从节点的数据已经太老了,
- 不能替换掉下线主节点, 因此在不是手动强制故障转移的情况下, 直接返回;*/
- if (data_age>
- ((mstime_t)server.repl_ping_slave_period * 1000) +
- (server.cluster_node_timeout * REDIS_CLUSTER_SLAVE_VALIDITY_MULT))
- {
- if (!manual_failover) {
- clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_DATA_AGE);
- return;
- }
- }
- /* If the previous failover attempt timedout and the retry time has
- * elapsed, we can setup a new one. */
- /*
- 例如集群有 7 个 master, 其中 redis1 下面有 2 个 slave, 突然 redis1 掉了, 则 slave1 和 slave2 竞争要求其他 6 个 master 进行投票, 如果这 6 个
- master 投票给 slave1 和 slave2 的票数都是 3, 也就是 3 个 master 投给了 slave1, 另外 3 个 master 投给了 slave2, 那么两个 slave 都得不到超过一半
- 的票数, 则只有靠这里的超时来进行重新投票了. 不过一半这种情况很少发生, 因为发起投票的时间是随机的, 因此一半一个 slave 的投票报文 auth req 会比
- 另一个 slave 的投票报文先发出. 越先发出越容易得到投票
- */
- /*
- 如果 auth_age 大于 auth_retry_time, 表示可以开始进行下一次故障转移了. 如果之前没有进行过故障转移, 则 auth_age 等
- 于 mstime, 肯定大于 auth_retry_time; 如果之前进行过故障转移, 则只有距离上一次发起故障转移时, 已经超过
- auth_retry_time 之后, 才表示可以开始下一次故障转移.
- */
- if (auth_age> auth_retry_time) {
- // 每次超时从新发送 auth req 要求其他主 master 投票, 都会先走这个 if, 然后下次调用该函数才会走 if 后面的流程
- server.cluster->failover_auth_time = mstime() +
- 500 + /* Fixed delay of 500 milliseconds, let FAIL msg propagate. */
- random() % 500; /* Random delay between 0 and 500 milliseconds. */ // 等到这个时间到才进行故障转移
- server.cluster->failover_auth_count = 0;
- server.cluster->failover_auth_sent = 0;
- server.cluster->failover_auth_rank = clusterGetSlaveRank();// 本节点按照在 master 中的 repl_offset 来获取排名
- /* We add another delay that is proportional to the slave rank.
- * Specifically 1 second * rank. This way slaves that have a probably
- * Less updated replication offset, are penalized. */
- server.cluster->failover_auth_time +=
- server.cluster->failover_auth_rank * 1000;
- /* However if this is a manual failover, no delay is needed. */
- /*
- 注意如果是管理员发起的手动强制执行故障转移, 则设置 server.cluster->failover_auth_time 为当前时间, 表示会
- 立即开始故障转移流程; 最后, 调用 clusterBroadcastPong, 向该下线主节点的所有从节点发送 PONG 包, 包头部分带
- 有当前从节点的复制数据量, 因此其他从节点收到之后, 可以更新自己的排名; 最后直接返回;
- */
- if (server.cluster->mf_end) {
- server.cluster->failover_auth_time = mstime();
- server.cluster->failover_auth_rank = 0;
- }
- redisLog(REDIS_WARNING,
- "Start of election delayed for %lld milliseconds"
- "(rank #%d, offset %lld).",
- server.cluster->failover_auth_time - mstime(),
- server.cluster->failover_auth_rank,
- replicationGetSlaveOffset());
- /* Now that we have a scheduled election, broadcast our offset
- * to all the other slaves so that they'll updated their offsets
- * if our offset is better. */
- /*
- 调用 clusterBroadcastPong, 向该下线主节点的所有从节点发送 PONG 包, 包头部分带
- 有当前从节点的复制数据量, 因此其他从节点收到之后, 可以更新自己的排名; 最后直接返回;
- */
- clusterBroadcastPong(CLUSTER_BROADCAST_LOCAL_SLAVES);
- return;
- }
- /* 进行故障转移 */
- /* It is possible that we received more updated offsets from other
- * slaves for the same master since we computed our election delay.
- * Update the delay if our rank changed.
- *
- * Not performed if this is a manual failover. */
- /*
- 如果还没有开始故障转移, 则调用 clusterGetSlaveRank, 取得当前从节点的最新排名. 因为在开始故障转移之前,
- 可能会收到其他从节点发来的心跳包, 因而可以根据心跳包中的复制偏移量更新本节点的排名, 获得新排名 newrank,
- 如果 newrank 比之前的排名靠后, 则需要增加故障转移开始时间的延迟, 然后将 newrank 记录到 server.cluster->failover_auth_rank 中;
- */
- if (server.cluster->failover_auth_sent == 0 &&
- server.cluster->mf_end == 0) // 还没有进行过故障庄毅
- {
- int newrank = clusterGetSlaveRank();
- if (newrank> server.cluster->failover_auth_rank) {
- long long added_delay =
- (newrank - server.cluster->failover_auth_rank) * 1000;
- server.cluster->failover_auth_time += added_delay;
- server.cluster->failover_auth_rank = newrank;
- redisLog(REDIS_WARNING,
- "Slave rank updated to #%d, added %lld milliseconds of delay.",
- newrank, added_delay);
- }
- }
- /* Return ASAP if we can't still start the election. */
- // 如果执行故障转移的时间未到, 先返回
- if (mstime() <server.cluster->failover_auth_time) {
- clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_WAITING_DELAY);
- return;
- }
- /* Return ASAP if the election is too old to be valid. */
- // 如果距离应该执行故障转移的时间已经过了很久
- // 那么不应该再执行故障转移了(因为可能已经没有需要了)
- // 直接返回
- if (auth_age> auth_timeout) {// 如果 auth_age 大于 auth_timeout, 说明之前的故障转移超时了, 因此直接返回;
- clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_EXPIRED);
- return;
- }
- /* Ask for votes if needed. */
- // 向其他节点发送故障转移请求
- if (server.cluster->failover_auth_sent == 0) {
- // 增加配置纪元
- server.cluster->currentEpoch++;
- // 记录发起故障转移的配置纪元
- server.cluster->failover_auth_epoch = server.cluster->currentEpoch;
- redisLog(REDIS_WARNING,"Starting a failover election for epoch %llu.",
- (unsigned long long) server.cluster->currentEpoch);
- // 向其他所有节点发送信息, 看它们是否支持由本节点来对下线主节点进行故障转移
- clusterRequestFailoverAuth();
- // 打开标识, 表示已发送信息
- server.cluster->failover_auth_sent = 1;
- // TODO:
- // 在进入下个事件循环之前, 执行:
- // 1)保存配置文件
- // 2)更新节点状态
- // 3)同步配置
- clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|
- CLUSTER_TODO_UPDATE_STATE|
- CLUSTER_TODO_FSYNC_CONFIG);
- return; /* Wait for replies. */
- }
- /* Check if we reached the quorum. */
- // 如果当前节点获得了足够多的投票, 那么对下线主节点进行故障转移
- if (server.cluster->failover_auth_count>= needed_quorum) {
- // 旧主节点
- clusterNode *oldmaster = myself->slaveof; // 在后面 clusterSetNodeAsMaster 中把 slaveof 置为 NULL
- redisLog(REDIS_WARNING,
- "Failover election won: I'm the new master.");
- redisLog(REDIS_WARNING,
- "configEpoch set to %llu after successful failover",
- (unsigned long long) myself->configEpoch);
- /* We have the quorum, perform all the steps to correctly promote
- * this slave to a master.
- *
- * 1) Turn this node into a master.
- * 将当前节点的身份由从节点改为主节点
- */
- clusterSetNodeAsMaster(myself);
- // 让从节点取消复制, 成为新的主节点
- replicationUnsetMaster();
- /* 2) Claim all the slots assigned to our master. */
- // 接收所有主节点负责处理的槽 轮训 16384 个槽位, 当前节点接手老的主节点负责的槽位;
- for (j = 0; j <REDIS_CLUSTER_SLOTS; j++) {
- if (clusterNodeGetSlotBit(oldmaster,j)) {
- // 将槽设置为未分配的
- clusterDelSlot(j);
- // 将槽的负责人设置为当前节点
- clusterAddSlot(myself,j);
- }
- }
- /* 3) Update my configEpoch to the epoch of the election. */
- // 更新集群配置纪元 本节点此时的配置 epoch 就是集群中最大的 configEpoch
- myself->configEpoch = server.cluster->failover_auth_epoch;
- /* 4) Update state and save config. */
- // 更新节点状态
- clusterUpdateState();
- // 并保存配置文件
- clusterSaveConfigOrDie(1);
- // 如果一个主 master 下面有 2 个 savle, 如果 master 挂了, 通过选举 slave1 被选为新的主, 则 slave2 通过这里来触发重新连接到新主, 即 slave1, 见 clusterUpdateSlotsConfigWith
- /* 5) Pong all the other nodes so that they can update the state
- * accordingly and detect that we switched to master role. */
- // 向所有节点发送 PONG 信息
- // 让它们可以知道当前节点已经升级为主节点了
- clusterBroadcastPong(CLUSTER_BROADCAST_ALL); // 真正触发其他本来属于同一个 master 的 slave 节点, 连接到这个新选举出的 master, 是在 clusterUpdateSlotsConfigWith
- /* 6) If there was a manual failover in progress, clear the state. */
- // 如果有手动故障转移正在执行, 那么清理和它有关的状态
- resetManualFailover();
- } else {
- // 说明没有获得足够的票数, 打印: Waiting for votes, but majority still not reached.
- clusterLogCantFailover(REDIS_CLUSTER_CANT_FAILOVER_WAITING_VOTES); // 例如 6 个主节点, 现在只有 1 个主节点投票 auth ack 过来了, 则会打印这个
- }
- }
2. 主节点接收所有从节点发来的投票请求, 但会比较该请求附带的配置纪元是否为新的(大于主节点自己记录的 currentepoch), 只有带着大于等于的消息才是有效请求; 主节点只会投票给第一个发来有效投票请求的从节点.
- void clusterSendFailoverAuthIfNeeded(clusterNode *node, clusterMsg *request) {//node 为 sender
- // 主节点对 slave 发送过来的 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 进行投票
- /*
- 集群中所有节点收到用于拉票的 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 包后, 只有负责一定槽位的主节点能投票, 其他没资格的节点直接忽略掉该包.
- */
- // 请求节点的主节点
- clusterNode *master = node->slaveof;
- // 请求节点的当前配置纪元
- uint64_t requestCurrentEpoch = ntohu64(request->currentEpoch);
- // 请求节点想要获得投票的纪元
- uint64_t requestConfigEpoch = ntohu64(request->configEpoch);
- // 请求节点的槽布局
- unsigned char *claimed_slots = request->myslots;
- int force_ack = request->mflags[0] & CLUSTERMSG_FLAG0_FORCEACK;
- int j;
- /* IF we are not a master serving at least 1 slot, we don't have the
- * right to vote, as the cluster size in Redis Cluster is the number
- * of masters serving at least one slot, and quorum is the cluster
- * size + 1 */
- // 如果节点为从节点, 或者是一个没有处理任何槽的主节点,
- // 那么它没有投票权
- if (nodeIsSlave(myself) || myself->numslots == 0) return; // 从节点和不负责槽位处理的直接返回, 不参与投票
- /* Request epoch must be>= our currentEpoch. */
- // 请求的配置纪元必须大于等于当前节点的配置纪元
- /*
- 如果发送者的 currentEpoch 小于当前节点的 currentEpoch, 则拒绝为其投票. 因为发送者的状态与当前集群状态不一致,
- 可能是长时间下线的节点刚刚上线, 这种情况下, 直接返回即可;
- */
- if (requestCurrentEpoch <server.cluster->currentEpoch) {
- redisLog(REDIS_WARNING,
- "Failover auth denied to %.40s: reqEpoch (%llu) <curEpoch(%llu)",
- node->name,
- (unsigned long long) requestCurrentEpoch,
- (unsigned long long) server.cluster->currentEpoch);
- return;
- }
- /* I already voted for this epoch? Return ASAP. */
- // 已经投过票了
- /*
- 如果当前节点 lastVoteEpoch, 与当前节点的 currentEpoch 相等, 说明本界选举中, 当前节点已经投过票了, 不
- 在重复投票, 直接返回(因此, 如果有两个从节点同时发起拉票, 则当前节点先收到哪个节点的包, 就只给那个
- 节点投票. 注意, 即使这两个从节点分属不同主节点, 也只能有一个从节点获得选票);
- */
- if (server.cluster->lastVoteEpoch == server.cluster->currentEpoch) {
- redisLog(REDIS_WARNING,
- "Failover auth denied to %.40s: already voted for epoch %llu",
- node->name,
- (unsigned long long) server.cluster->currentEpoch);
- return;
- }
- /* Node must be a slave and its master down.
- * The master can be non failing if the request is flagged
- * with CLUSTERMSG_FLAG0_FORCEACK (manual failover). */
- /*
- 如果发送节点是主节点; 或者发送节点虽然是从节点, 但是找不到其主节点; 或者发送节点的主节点并未下线
- 并且这不是手动强制开始的故障转移流程, 则根据不同的条件, 记录日志后直接返回;
- */
- if (nodeIsMaster(node) || master == NULL ||
- (!nodeFailed(master) && !force_ack)) { // 如果从接收到 cluster failover, 然后发起 auth req 要求投票, 则 master 受到后, 就是该 master 在线也需要进行投票
- if (nodeIsMaster(node)) { //auth request 必须由 slave 发起
- redisLog(REDIS_WARNING,
- "Failover auth denied to %.40s: it is a master node",
- node->name);
- } else if (master == NULL) {
- //slave 认为自己的 master 下线了, 但是本节点不知道他的 master 是那个, 也就不知道是为那个 master 的 slave 投票,
- // 因为我们要记录是对那个 master 的从节点投票的, 看 if 后面的流程
- redisLog(REDIS_WARNING,
- "Failover auth denied to %.40s: I don't know its master",
- node->name);
- } else if (!nodeFailed(master)) { // 从这里也可以看出, 必须集群中有一个主节点判断出某个节点 fail 了, 才会处理 slave 发送过来的 auth req
- //slave 认为自己的 master 下线了, 于是发送过来 auth request, 本主节点收到该信息后, 发现
- // 该 slave 对应的 master 是正常的, 因此给出打印, 不投票
- redisLog(REDIS_WARNING,
- "Failover auth denied to %.40s: its master is up",
- node->name);
- }
- return;
- }
- /* We did not voted for a slave about this master for two
- * times the node timeout. This is not strictly needed for correctness
- * of the algorithm but makes the base case more linear. */
- /*
- 针对同一个下线主节点, 在 2*server.cluster_node_timeout 时间内, 只会投一次票, 这并非必须的限制条
- 件(因为之前的 lastVoteEpoch 判断, 已经可以避免两个从节点同时赢得本界选举了), 但是这可以使得获
- 胜从节点有时间将其成为新主节点的消息通知给其他从节点, 从而避免另一个从节点发起新一轮选举又进
- 行一次没必要的故障转移;
- */
- // 如果之前一段时间已经对请求节点进行过投票, 那么不进行投票
- if (mstime() - node->slaveof->voted_time <server.cluster_node_timeout * 2)
- {
- redisLog(REDIS_WARNING,
- "Failover auth denied to %.40s:"
- "can't vote about this master before %lld milliseconds",
- node->name,
- (long long) ((server.cluster_node_timeout*2)-
- (mstime() - node->slaveof->voted_time)));
- return;
- }
- /* The slave requesting the vote must have a configEpoch for the claimed
- * slots that is>= the one of the masters currently serving the same
- * slots in the current configuration. */
- /*
- 判断发送节点, 对其宣称要负责的槽位, 是否比之前负责这些槽位的节点, 具有相等或更新的配置纪元 configEpoch:
- 该槽位当前的负责节点的 configEpoch, 是否比发送节点的 configEpoch 要大, 若是, 说明发送节点的配置信息不是最新的,
- 可能是一个长时间下线的节点又重新上线了, 这种情况下, 不能给他投票, 因此直接返回;
- */
- for (j = 0; j <REDIS_CLUSTER_SLOTS; j++) {
- // 跳过未指派节点
- if (bitmapTestBit(claimed_slots, j) == 0) continue;
- // 查找是否有某个槽的配置纪元大于节点请求的纪元
- if (server.cluster->slots[j] == NULL || server.cluster->slots[j]->configEpoch <= requestConfigEpoch)
- // 这里就是 configEpoch 真正发挥作用的地方
- {
- continue;
- }
- // 如果有的话, 说明节点请求的纪元已经过期, 没有必要进行投票
- /* If we reached this point we found a slot that in our current slots
- * is served by a master with a greater configEpoch than the one claimed
- * by the slave requesting our vote. Refuse to vote for this slave. */
- redisLog(REDIS_WARNING,
- "Failover auth denied to %.40s:"
- "slot %d epoch (%llu)> reqEpoch (%llu)",
- node->name, j,
- (unsigned long long) server.cluster->slots[j]->configEpoch,
- (unsigned long long) requestConfigEpoch);
- return;
- }
- /* We can vote for this slave. */
- // 为节点投票
- clusterSendFailoverAuth(node);
- // 更新时间值
- server.cluster->lastVoteEpoch = server.cluster->currentEpoch;
- node->slaveof->voted_time = mstime();
- redisLog(REDIS_WARNING, "Failover auth granted to %.40s for epoch %llu",
- node->name, (unsigned long long) server.cluster->currentEpoch);
- }
3. 如果有从节点收到了系统中超过半数主节点的投票, 则变为新的主节点, 并向所有节点广播自己当选的信息;
4,. 如果从节点在超时时间内没有收到多数主节点的投票, 也没有收到其他从节点升级为新主节点的广播, 就会再次发起投票, 重复第 3 步.
新的主节点向所有其他节点广播之后, 其他的节点会更新自己的配置, 将新的主节点和其负责的槽对应起来. 再有和相关槽对应的命令发到集群就会被转发给这个新的主节点; 至此故障迁移结束. 我们可以考虑下, 如果同时有半数以上的服务器掉线了, 是否会导致集群彻底失效呢?
来源: https://www.cnblogs.com/gogoCome/p/9944152.html