1、集群currentEpoch
Redis Cluster使用了类似于Raft算法“term”(任期)的概念,那么在redis Cluster中term称为epoch,用来给events增量版本号。当多个nodes提供了信息有冲突时,它可以作为node来知道哪个状态是最新的。currentEpoch为一个64位无签名数字。
在集群node创建时,master和slave都会将各自的currentEpoch设置为0,每次从其他node接收到数据包时,如果发现发送者的epoch值比自己的大,那么当前node将自己的currentEpoch设置为发送者的epoch。由此,最终所有的nodes都会认同集群中最大的epoch值;当集群的状态变更,或者node为了执行某个行为需求agreement时,都将需要epoch(传递或者比较)。
当前来说,只有在slave提升期间发生;currentEpoch为集群的逻辑时钟(logical clock),指使持有较大值的获胜。(currentEpoch,当前集群已达成认同的epoch值,通常所有的nodes应该一样)
2、configEpoch
每个master总会在ping、pong数据包中携带自己的configEpoch以及它持有的slots列表。新创建的node,其configEpoch为0,slaves通过递增它们的configEpoch来替代失效的master,并尝试获得其他大多数master的授权(认同)。当slave被授权,一个新的configEpoch被生成,slave提升为master且使用此configEpoch。
接下来介绍configEpoch帮助解决冲突,当不同的nodes宣称有分歧的配置时。
slaves在ping、pong数据包中也会携带自己的configEpoch信息,不过这个epoch为它与master在最近一次数据交换时,master的configEpoch。
每当节点发现configEpoch值变更时,都会将新值写入nodes.conf文件,当然currentEpoch也也是如此。这两个变量在写入文件后会伴随磁盘的fsync,持久写入。严格来说,集群中所有的master都持有唯一的configEpoch值。同一组master-slaves持有相同的configEpoch。
3、slave选举与提升
在slaves节点中进行选举,在其他masters的帮助下进行投票,选举出一个slave并提升为master。当master处于FAIL状态时,将会触发slave的选举。slaves都希望将自己提升为master,此master的所有slaves都可以开启选举,不过最终只有一个slave获胜。当如下情况满足时,slave将会开始选举:
a.当此slave的master处于FAIL状态
b.此master持有非零个slots
c.此slave的replication链接与master断开时间没有超过设定值,为了确保此被提升的slave的数据是新鲜的,这个时间用户可以配置。
为了选举,第一步,就是slave自增它的currentEpoch值,然后向其他masters请求投票(需求支持,votes)。slave通过向其他masters传播“FAILOVER_AUTH_REQUEST”数据包,然后最长等待2倍的NODE_TIMEOUT时间,来接收反馈。一旦一个master向此slave投票,将会响应“FAILOVER_AUTH_ACK”,此后在2 * NODE_TIMOUT时间内,它将不会向同一个master的slaves投票;虽然这对保证安全上没有必要,但是对避免多个slaves同时选举时有帮助的。slave将会丢弃那些epoch值小于自己的currentEpoch的AUTH_ACK反馈,即不会对上一次选举的投票计数(只对当前轮次的投票计数)。一旦此slave获取了大多数master的ACKs,它将在此次选举中获胜;否则如果大多数master不可达(在2 * NODE_TIMEOUT)或者投票额不足,那么它的选举将会被中断,那么其他的slave将会继续尝试。
4、slave rank(次序)
当master处于FAIL状态时,slave将会随机等待一段时间,然后才尝试选举,等待的时间:DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms一定的延迟确保我们等待FAIL状态在集群中传播,否则slave立即尝试选举(不进行等待的话),不过此时其他masters或许尚未意识到FAIL状态,可能会拒绝投票。延迟的时间是随机的,这用来“去同步”(desynchronize),避免slaves同时开始选举。SLAVE_RANK表示此slave已经从master复制数据的总量的rank。当master失效时,slaves之间交换消息以尽可能的构建rank,持有replication offset最新的rank为0,第二最新的为1,依次轮推。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。当然rank顺序也不是严格执行的,如果一个持有较小rank的slave选举失败,其他slaves将会稍后继续。
一旦,slave选举成功,它将获取一个新的、唯一的、自增的configEpoch值,此值比集群中任何masters持有的都要大,它开始宣称自己是master,并通过ping、pong数据包传播,并提供自己的新的configEpoch以及持有的slots列表。为了加快其他nodes的重新配置,pong数据包将会在集群中广播。当前node不可达的那些节点,它们可以从其他节点的ping或者pong中获知信息(gossip),并重新配置。
其他节点也会检测到这个新的master和旧master持有相同的slots,且持有更高的configEpoch,此时也会更新自己的配置(epoch,以及master);旧master的slaves不仅仅更新配置信息,也会重新配置并与新的master跟进(slave of)。
5、Masters响应slave的投票请求
当Master接收到slave的“FAILOVER_AUTH_REQUEST”请求后,开始投票,不过需要满足如下条件:
a.此master只会对指定的epoch投票一次,并且拒绝对旧的epoch投票:每个master都持有一个lastVoteEpoch,将会拒绝AUTH_REQUEST中currentEpoch比lastVoteEpoch小的请求。当master响应投票时,将会把lastVoteEpoch保存在磁盘中。
b.此slave的master处于FAIL状态时,master才会投票。
c.如果slave的currentEpoch比此master的currentEpoch小,那么AUTH_REQUEST将会被忽略。因为master只会响应那些与自己的currentEpoch相等的请求。如果同一个slave再此请求投票,持有已经增加的currentEpoch,它(slave)将保证旧的投票响应不能参与计票。比如master的currentEpoch为5,lastVoteEpoch为1:
·slave的currentEpoch为3
·slave在选举开始时,使用epoch为4(先自增),因为小于master的epoch,所以投票响应被延缓。
·slave在一段时间后将重新选举,使用epoch为5(4 + 1,再次自增),此时master上延缓的响应发给slave,接收后视为有效。
·master在2 * NODE_TIMEOUT超时之前,不会对同一个master的slave再次投票。这并不是严格需要,因为也不太可能两个slave在相同的epoch下同时赢得选举。不过,它确保当一个slave选举成功后,它(slave)有一段缓冲时间来通知其他的slaves,避免另一个slave赢得了新的一轮的选择,避免不必要的二次failover。
·master并不会尽力选举最合适的slave。当slave的master处于FAIL状态,此master在当前任期(term)内并不投票,只是批准主动投票者(即master不发起选举,只批准别人的投票)。最合适的slave应该在其他slaves之前,首先发起选举。
·当master拒绝一个slave投票,并不会发出一个“否决”响应,而是简单的忽略。
·slave发送的configEpoch是其master的,还包括其master持有的slots;master不会向持有相同slots、但configEpoch只较低的slave投票。
6、Hash Slots配置传播
Redis Cluster中重要的一部分就是传播集群中哪些节点上持有的哪些hash slots信息;无论是启动一个新的集群,还是当master失效其slave提升后更新配置,这对它们都至关重要。有2种方式用于hash slot配置的传播:
a.heartbeat 消息:发送者的ping、pong消息中,总是携带自己目前持有的slots信息,不管自己是master还是slave。
b.UPDATE 消息:因为每个心跳消息中会包含发送者的configEpoch和其持有的slots,如果接收者发现发送者的信息已经stale(比如发送者的configEpoch值小于持有相同slots的master的值),它会向发送者反馈新的配置信息(UPDATE),强制stale节点更新它。
当一个新的节点加入集群,其本地的hash slots映射表将初始为NULL,即每个hash slot都没有与任何节点绑定。
·Rule 1:如果此node本地视图中一个hash slot尚未分配(设置为NULL),并且有一个已知的node声明持有它,那么此node将会修改本地hash slot的映射表,将此slot与那个node关联。slave的failover操作、reshard操作都会导致hash slots映射的变更,新的配置信息将会通过心跳在集群中传播。
·Rule 2:如果此node的本地视图中一个hash slot已经分配,并且一个已知的node也声明持有它,且此node的configEpoch比当前slot关联的master的configEpoch值更大,那么此node将会把slot重新绑定到新的node上。根据此规则,最终集群中所有的nodes都赞同那个持有声明持有slot、且configEpoch最大值的nodes为slot的持有者。
7、nodes如何重新加入集群
node A被告知slot 1、2现在有node B接管,假如这两个slots目前有A持有,且A只持有这两个slots,那么此后A将放弃这2个slots,成为空的节点;此后A将会被重新配置,成为其他新master的slave。这个规则可能有些复杂,A离群一段时间后重新加入集群,此时A发现此前自己持有的slots已经被其他多个nodes接管,比如slot 1被B接管,slot 2被C接管。在重新配置时,最终此节点上的slots将会被清空,那个窃取自己最后一个slot的node,将成为它的新master。节点重新加入集群,通常发生在failover之后,旧的master(也可以为slave)离群,然后重新加入集群。
8、Replica迁移
Redis Cluster实现了一个成为“Replica migration”的概念,用来提升集群的可用性。比如集群中每个master都有一个slave,当集群中有一个master或者slave失效时,而不是master与它的slave同时失效,集群仍然可以继续提供服务。
a.master A,有一个slave A1
b.master A失效,A1被提升为master
c.一段时间后,A1也失效了,那么此时集群中没有其他的slave可以接管服务,集群将不能继续服务。
如果masters与slaves之间的映射关系是固定的(fixed),提高集群抗灾能力的唯一方式,就是给每个master增加更多的slaves,不过这种方式开支很大,需要更多的redis实例。
解决这个问题的方案,我们可以将集群非对称,且在运行时可以动态调整master-slaves的布局(而不是固定master-slaves的映射),比如集群中有三个master A、B、C,它们对应的slave为A1、B1、C1、C2,即C节点有2个slaves。“Replica迁移”可以自动的重新配置slave,将其迁移到某个没有slave的master下。
a.A失效,A1被提升为master
b.此时A1没有任何slave,但是C仍然有2个slave,此时C2被迁移到A1下,成为A1的slave
c.此后某刻,A1失效,那么C2将被提升为master。集群可以继续提供服务。
Replica迁移算法:
迁移算法并没有使用“agree”形式,而是使用一种算法来避免大规模迁移,这个算法确保最终每个master至少有一个slave即可。起初,我们先定义哪个slave是良好的:一个良好的slave不能处于FAIL状态。触发时机为,任何一个slave检测到某个master没有一个良好slave时。参与迁移的slave必须为,持有最多slaves的master的其中一个slave,且不处于FAIL状态,且持有最小的node ID。
比如有10个masters都持有一个slave,有2个masters各持有5个slaves,那么迁移将会发生在持有5个slaves的masters中,且node ID最小的slave node上。我们不再使用“agreement”,不过也有可能当集群的配置不够稳定时,有一种竞争情况的发生,即多个slaves都认为它们自己的ID最小;如果这种情况发生,结果就是可能多个slaves会迁移到同一个master下,不过这并没有什么害处,但是最坏的结果是导致原来的master迁出了所有的slaves,让自己变得单一。但是迁移算法(进程)会在迁移完毕之后重新判断,如果尚未平衡,那么将会重新迁移。
最终,每个master最少持有一个slave;这个算法由用户配置的“cluster-migration-barrier”,此配置参数表示一个master至少保留多少个slaves,其他多余的slaves可以被迁出。此值通常为1,如果设置为2,表示一个master持有的slaves个数大于2时,多余的slaves才可以迁移到持有更少slaves的master下。
configEpoch冲突解决算法:
在slave failover期间,会生成新的configEpoch值,需要保证唯一性。不过有2种不同的event会导致configEpoch的创建是不安全的:仅仅自增本地的currentEpoch并希望它不会发生冲突。这两个事件有系统管理员触发:
a.CLUSTER FAILOVER:这个指令,就是人为的将某个slave提升为master,而不需要要求大多数masters的投票参与。
b.slots的迁移,用于平衡集群的数据分布(reshard);此时本地的configEpoch也会修改,因为性能的考虑,这个过程也不需要“agreement”。在手动reshard期间,当一个hash slot从A迁移到B,resharding程序将强制B更新自己的配置信息、epoch值也修改为集群的最大值 + 1(除非B的configEpoch已经是最大值),这种变更则不需要其他nodes的agreement(注意与failover的原理不同)。通常每次resharding都会迁移多个slots,且有多个nodes参与,如果每个slots迁移都需要agreement,才能生成新的epoch,这种性能是很差的,也不可取。我们在首个slots迁移开始时,只会生成一个新的configEpoch,在迁移完毕后,将新的配置传播给集群即可,这种方式在生产环境中更加高效。
因为上述两个情况,有可能(虽然概率极小)最终多个nodes产生了相同的configEpoch;比如管理员正在进行resharding,但是此时failover发生了...无论是failover还是resharding都是将currentEpoch自增,而且resharding不使用agreement形式(即其他nodes或许不知道,而且网络传播可能延迟),这就会发生epoch值的冲突问题。
当持有不同slots的masters持有相同的configEpoch,这并不会有什么问题。比较遗憾的是,人工干预或者resharding会以不同的方式修改了集群的配置,Cluster要求所有的slots都应该被nodes覆盖,所以在任何情况下,我们都希望所有的master都持有不同的configEpoch。避免冲突的算法,就是用来解决当2个nodes持有相同的configEpoch:
a.如果一个master节点发现其他master持有相同的configEpoch。
b.并且此master逻辑上持有较小的node ID(字典顺序)
c.然后此master将自己的currentEpoch加1,并作为自己新的configEpoch。
如果有多个nodes持有相同的congfigEpoch,那么除了持有最大ID的节点外,其他的nodes都将往前推进(+1,直到冲突解决),最终保证每个master都持有唯一的configEpoch(slave的configEpoch与master一样)。对于新创建的cluster也是同理,所有的nodes都初始为不同的configEpoch。
9、Node resets
所有的nodes都可以进行软件级的reset(不需要重启、重新部署它们),reset为了重用集群(重新设定集群),必须需要将某个(些)节点重置后添加到其他集群。我们可以使用“CLUSTER RESET”指令:
a.CLUSTER RESET SOFT
b.CLUSTER RESET HARD
指令必须直接发给需要reset的节点,如果没有指定reset类型,默认为SOFT。
a.soft和hard:如果节点为slave,那么节点将会转换为master,并清空其持有的数据,成为一个空的master。如果此节点为master,且持有slots数据,那么reset操作将被中断。
b.soft和hard:其上持有的slots将会被释放
c.soft和hard:此节点上的nodes映射表将会被清除,此后此node将不会知道其他节点的存在与状态。
d.hard:currentEpoch、configEpoch、lastVoteEpoch值将被重置为0。
e.hard:此nodeID将会重新生成。
持有数据的(slot映射不为空的)master不能被reset(除非现将此master上的slot手动迁移到其他nodes上,或者手动failover,将其切换成slave);在某些特定的场景下,在执行reset之前,或许需要执行FLUSHALL来清空原有的数据。
10、集群中移除节点
我们已经知道,将node移除集群之前,首先将其上的slots迁移到其他nodes上(reshard),然后关闭它。不过这似乎还并未结束,因为其他nodes仍然记住了它的ID,仍然不会尝试与它建立连接。因此,当我们确定将节点移除集群时,可以使用“CLUSTER FORGET”指令:
a.将此node从nodes映射表中移除。
b.然后设定一个60秒的隔离时间,阻止持有相同ID的node再次加入集群。
之所以2)规则,因为FORGET指令将会通过gossip协议传播给其他nodes,集群中所有的节点都收到消息是需要一定的时间延迟。