更多Redis入门系列文章,参见Redis入门-大纲
心跳机制
Cluster中的每个节点都维护一份在自己看来当前整个集群的状态,主要包括:
当前集群状态
集群中各节点所负责的slots信息,及其migrate状态
集群中各节点的master-slave状态
集群中各节点的存活状态及不可达投票
当集群状态变化时,如新节点加入、slot迁移、节点宕机、slave提升为新Master,我们希望这些变化尽快的被发现,传播到整个集群的所有节点并达成一致。节点之间相互的心跳(PING,PONG,MEET)及其携带的数据是集群状态传播最主要的途径。
心跳时机
Redis节点会记录其向每一个节点上一次发出ping和收到pong的时间,心跳发送时机与这两个值有关。
通过下面的方式既能保证及时更新集群状态,又不至于使心跳数过多:
每次Cron向所有未建立链接的节点发送ping或meet
每1秒从所有已知节点中随机选取5个,向其中上次收到pong最久远的一个发送ping
每次Cron向收到pong超过timeout/2的节点发送ping
收到ping或meet,立即回复pong
心跳数据
Header,发送者自己的信息
- 所负责slots的信息
- 主从信息
- ip port信息
- 状态信息
Gossip,发送者所了解的部分其他节点的信息
- ping_sent, pong_received
- ip, port信息
- 状态信息,比如发送者认为该节点已经不可达,会在状态信息中标记其为PFAIL或FAIL
心跳处理
新节点加入
向已知结点发送meet包加入集群。从收到的回复pong包中的gossip得到未知的其他节点。循环上述过程,直到最终加入集群。
Slots信息
判断发送者声明的slots信息,跟本地记录的是否有不同。如果不同,且发送者epoch较大,更新本地记录。如果不同,且发送者epoch小,发送Update信息通知发送者。
Master slave信息
发现发送者的master、slave信息变化,更新本地状态。
节点Fail探测
详见宕机处理
广播
当需要发布一些非常重要需要立即送达的信息时,上述心跳加Gossip的方式就显得捉襟见肘了,这时就需要向所有集群内机器的广播信息,使用广播发的场景:
节点的Fail信息:当发现某一节点不可达时,探测节点会将其标记为PFAIL状态,并通过心跳传播出去。当某一节点发现这个节点的PFAIL超过半数时修改其为FAIL并发起广播。
Failover Request信息:slave尝试发起FailOver时广播其要求投票的信息。
新Master信息:Failover成功的节点向整个集群广播自己的信息。
宕机处理
节点失效检测
1)当一个节点向另一个节点发送 PING 命令,但是目标节点未能在给定的时限内返回 PONG 命令时,那么发送命令的节点会将目标节点标记为PFAIL (possible failure,可能已失效)。
2)等待 PING 命令回复的时限称为“节点超时时限(node timeout)”,是一个节点选项(node-wise setting)。
3)当节点接收到其他节点发来的ping时,它会读取gossip包,记下那些被其他节点标记为失效的节点。这称为失效报告(failure report)。失效报告有过期时限,过期的失效报告会被移除,所以主节点要将某个节点标记为 FAIL 的话,必须以最近接收到的失效报告作为根据。
4)如果节点已经将某个节点标记为 PFAIL ,并且根据节点所收到的失效报告显式,集群中的大部分其他主节点也认为那个节点进入了失效状态,那么节点会将那个失效节点的状态标记为 FAIL 。
Gossip的存在使得集群状态的改变可以更快的达到整个集群。每个心跳包中会包含多个Gossip包,那么多少个才是合适的呢,redis的选择是N/10,其中N是节点数,这样可以保证在PFAIL投票的过期时间内,节点可以收到80%机器关于失败节点的gossip,从而使其顺利进入FAIL状态。
5)一旦某个节点被标记为 FAIL ,关于这个节点已失效的信息就会被广播到整个集群,所有接收到这条信息的节点都会将失效节点标记为 FAIL 。
节点宕机处理
一旦某个从节点进入FAIL状态,对该节点的直接访问将被拒绝Connection Refused。
一旦某个主节点进入 FAIL 状态,如果这个主节点有一个或多个从节点存在,则会进入从节点选举。
集群FAIL判断
某个主节点和所有从节点全部挂掉,我们集群就进入faill状态。
如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。
如果集群任意master挂掉,且当前master没有slave.集群进入fail状态。
从节点选举
集群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(传递或者比较)。
configEpoch
每个master总会在ping、pong数据包中携带自己的configEpoch以及它持有的slots列表。新创建的node,其configEpoch为0。slaves在ping、pong数据包中也会携带自己的configEpoch信息,不过这个epoch为它与master在最近一次数据交换时,会更新为master的configEpoch。
每当节点发现configEpoch值变更时,都会将新值写入nodes.conf文件,当然currentEpoch也也是如此。这两个变量在写入文件后会伴随磁盘的fsync,持久写入。严格来说,集群中所有的master都持有唯一的configEpoch值。同一组master-slaves持有相同的configEpoch。
选举准备
当master处于FAIL状态时,将会触发slave的选举。slaves都希望将自己提升为master,此master的所有slaves都可以开启选举,不过最终只有一个slave获胜。
只有满足以下条件的slave拥有被选举权:
这个节点是已下线主节点的从节点。
已下线主节点负责处理的槽数量非空。
从节点的数据被认为是可靠的,也即是,主从节点之间的复制连接(replication link)的断线时长不能超过节点超时时限(node timeout)乘以REDIS_CLUSTER_SLAVE_VALIDITY_MULT 常量得出的积。
一般来说,满足上述条件的从节点不会立即发起选举,而是会等待一个随机时间,然后才尝试选举,等待的时间:
DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
一定的延迟确保我们等待FAIL状态在集群中传播,否则其他masters或许尚未意识到FAIL状态,可能会拒绝投票。延迟的时间是随机的,这用来“去同步”(desynchronize),避免slaves同时开始选举。
SLAVE_RANK表示此slave已经从master复制数据的总量的rank。当master失效时,slaves之间交换消息以尽可能的构建rank,持有replication offset最新的rank为0,第二最新的为1,依次轮推。由公式可见,持有最新数据的slave将会首先发起选举(理论上)。 如果一个持有较小rank的slave选举失败,其他slaves将会稍后继续。
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将会继续尝试。
Master响应投票请求
当Master接收到slave的“FAILOVER_AUTH_REQUEST”请求后,开始投票。
master并不会尽力选举最合适的slave, 只是批准主动投票者。最合适的slave应该在其他slaves之前,首先发起选举。当master拒绝一个slave投票,并不会发出一个“否决”响应,而是简单的忽略。
Mater投票规则:
此master只会对指定的epoch投票一次,并且拒绝对旧的epoch投票:每个master都持有一个lastVoteEpoch,将会拒绝AUTH_REQUEST中currentEpoch比lastVoteEpoch小的请求。当master响应投票时,将会把lastVoteEpoch保存在磁盘中。
此slave的master处于FAIL状态时,master才会投票。
如果slave的currentEpoch比此master的currentEpoch小,那么AUTH_REQUEST将会被忽略。因为master只会响应那些与自己的currentEpoch相等的请求。
master在2 * NODE_TIMEOUT超时之前,不会对同一个master的slave再次投票。这并不是严格需要,因为也不太可能两个slave在相同的epoch下同时赢得选举。不过,它确保当一个slave选举成功后,它(slave)有一段缓冲时间来通知其他的slaves,避免另一个slave赢得了新的一轮的选择,避免不必要的二次failover。
slave发送的configEpoch是其master的,还包括其master持有的slots;master不会向持有相同slots、但configEpoch只较低的slave投票。
选举成功
一旦,slave选举成功,它将获取一个新的、唯一的、自增的configEpoch值,此值比集群中任何masters持有的都要大,它开始宣称自己是master,并进行一下步骤:
1)通过 PONG 数据包(packet)告知其他节点,这个节点现在是主节点了。
2)通过 PONG 数据包告知其他节点,这个节点是一个已升级的从节点(promoted slave)。
3)接管(claiming)所有由已下线主节点负责处理的哈希槽。
4)显式地向所有节点广播一个 PONG 数据包,加速其他节点识别这个节点的进度,而不是等待定时的 PING / PONG 数据包。当前node不可达的那些节点,它们可以从其他节点的ping或者pong中获知信息(gossip),并重新配置。
5)所有其他节点都会根据新的主节点对配置进行相应的更新,特别地:
所有被新的主节点接管的槽会被更新。
已下线主节点的所有从节点会察觉到 PROMOTED 标志,并开始对新的主节点进行复制。
旧master节点恢复
旧的master(也可以为slave)离群,然后重新加入集群时,若其持有的所有slot被某一节点接管,旧master将放弃这些slots,成为空的节点;此后 将会被重新配置,成为其他新master的slave。若离群一段时间后重新加入集群,发现此前自己持有的slots已经被其他多个nodes接管, 在重新配置时,最终此旧节点上的slots将会被清空,那个窃取自己最后一个slot的node,将成为它的新master。