Redis选举领头Sentinel(raft算法)

Sentinel是Redis实现高可用的保证。Sentinel系统作用就是监视Redis服务器集群,它可以不停的获得redis集群状态,当一个主节点挂了,故障转移操作会在从节点中选出一个新的主节点,这里故障转移就是由Sentinel来主导完成的。不要把Sentinel想的太复杂,它其实就是一个特殊工作模式的Redis服务器而已,Redis是集群部署的,这里的Sentinel也是要集群部署的,要是非单点部署,你的Sentinel挂了,此时的Redis集群就GG了。接着上边说,当主服务器节点挂了,Sentinel系统就会选出一个领头的Sentinel来完成故障转移工作。选举规则如下: - 监视这个挂了的主节点的所有Sentinel都有被选举为领头的资格

每进行一次选举,不论是否成功,配置纪元+1,配置纪元就是个计数器

每个Sentinel在每个配置纪元中有且仅有一次选举机会,一旦选好了该节点认为的主节点,在这个纪元内,不可以再更改

每个发现服务器挂了的Sentinel都会配置纪元+1并投自己一票,接着发消息要求其他Sentinel设置自己为领头人1,每个Sentinel都想成为领头的

每个Sentinel会将最先发来请求领头的节点设为自己的领头节点并发送回复,谁先来我选谁

当源Sentinel收到回复,并且回复中的配置纪元和自己的一致且领头Id是自己的Sentinel Id时,表明目标Sentinel已经将自己设为领头

在一个配置纪元内,当某个Sentinel收到半数以上的同意回复时,它就是领头的了

如果在给定时间内,没有被成功选举的Sentinel,那么过段时间发起新的选举

选举领头Sentinel的过程和规则大概就如上所述,需要注意的是只有集群出现节点挂了才需要选举出领头Sentinel,平时每个Sentinel还是平等身份~

Zookeeper选举

Zookeeper是一个很强的分布式数据一致性解决方案,比如dubbo中的注册中心就使用的Zookeeper。当然,这也是集群部署的,但是它没有采用传统的Master/Slave结构,而是引入了Leader、Follwer和Observer。Leader和Follower类似于Master/Slave,新增的Observer作用仅仅只是增加集群的读性能,它不参与Leader的选举。

节点的状态有以下几种:

LOOKING: 节点正处于选主状态,不对外提供服务,直至选主结束;

FOLLOWING: 作为系统的从节点,接受主节点的更新并写入本地日志;

LEADING: 作为系统主节点,接受客户端更新,写入本地日志并复制到从节点

Zookeeper的状态同步是基于Zab协议实现的,Zab协议有两种模式,它们分别是崩溃恢复(选主)和消息广播(同步)。当服务启动或者在Leader崩溃后,Zab就进入了恢复模式,当Leader被选举出来,且超过一半机器完成了和 leader的状态同步以后,恢复模式就结束了。

首先明确几个概念: - Sid:服务器id;

Zxid:服务器的事务id,数据越新,zxid越大;zxid的高32位是epoch,低32位是zpoch内的自增id,由0开始。每次选出新的Leader,epoch会递增,同时zxid的低32位清0。

整个选主流程如下

状态变更。服务器启动的时候每个server的状态时Looking,如果是leader挂掉后进入选举,那么余下的非Observer的Server就会将自己的服务器状态变更为Looking,然后开始进入Leader的选举状态;

发起投票。每个server会产生一个(sid,zxid)的投票,系统初始化的时候zxid都是0,如果是运行期间,每个server的zxid可能都不同,这取决于最后一次更新的数据。将投票发送给集群中的所有机器;

接收并检查投票。server收到投票后,会先检查是否是本轮投票,是否来自looking状态的server;

处理投票。对自己的投票和接收到的投票进行PK:

先检查zxid,较大的优先为leader;如果zxid一样,sid较大的为leader;根据PK结果更新自己的投票,再次发送自己的投票;

统计投票。每次投票后,服务器统计投票信息,如果有过半机器接收到相同的投票,那么leader产生,如果否,那么进行下一轮投票;

改变server状态。一旦确定了Leader,server会更新自己的状态为Following或者是Leading。选举结束。

我们要保证选主完成后,原来的主节点已经提交的事务继续完成提交;原主节点只是提出而没提交的事务要抛弃。这也是为什么倾向于选zxid最大的从节点为主节点,因为它上边的事务最新,最与原主节点保持一致。

哨兵模式(raft)与Zookeeper模式(zab)选主的总结

Redis中的Sentinel选主相对来说更简单,因为不涉及事务状态的一致性

Sentinel选主是基于raft协议,Zookeeper则基于Zab协议

二者都是收到半数的选票就选举成功

Sentinel投票发消息主要内容是Sentinel id和配置纪元,Zookeeper则是 zxid和 sid

Sentinel谁先来找他投票他就投谁,Zookeeper中则是要细细检查比较一番,检查内容包括epoch和节点状态,检查完毕后再跟自己的投票进行pk,进而看需不需要更新自己的投票,若是需要,则自己的投票也要广播出去

分布式选举的算法:bully , Raft,ZAB

分布式选举的算法:

Paxos 理论为基础而衍生出来的变种和简化版。例如 Google 的 Chubby、MegaStore、Spanner 等系统,ZooKeeper 的 ZAB 协议,还有更加容易理解的 raft 协议。大部分系统都是靠在实践中运行很长一段时间才能谨慎的表示,系统已基本运行,没有发现大的问题。

Bully算法:

在所有活着的节点中,选取 ID 最大的节点作为主节点。

集群中每个节点均知道其他节点的 ID

节点:主节点和普通节点

三种消息:Election 消息,用于发起选举;Alive 消息,对 Election 消息的应答;Victory 消息,竞选成功的主节点向其他节点发送的宣誓主权的消息。

eg:MongoDB 的副本集故障转移功能。MongoDB 的分布式选举中,采用节点的最后操作时间戳来表示 ID

优点:选举速度快、算法复杂度低、简单易实现。

缺点:需要每个节点有全局的节点信息;

前提条件

集群中的每个节点均知道其他节点的ID(mongo各个节点都会保存其他节点的快照信息)

任意一个比当前主节点 ID 大的新节点或节点故障后恢复加入集群的时候,都可能会触发重新选举,成为新的主节点,如果该节点频繁退出、加入集群,就会导致频繁切主。

Raft算法:

少数服从多数

redis - cluster用的该算法

eg:etcd 的集群管理器 etcds

Leader:主节点,同一时刻只有一个 Leader,负责协调和管理其他节点;

Candidate:即候选者,每一个节点都可以成为 Candidate,节点在该角色下才可以被选为新的 Leader;

Follower:Leader 的跟随者,不可以发起选举。

选举流程

初始化时,所有节点均为 Follower 状态。

开始选主时,所有节点的状态由 Follower 转化为 Candidate,并向其他节点发送选举请求。

其他节点根据接收到的选举请求的先后顺序,回复是否同意成为主。在每一轮选举中,一个节点只能投出一张票。

若发起选举请求的节点获得超过一半的投票,则成为主节点,其状态转化为 Leader,其他节点的状态则由 Candidate 降为 Follower。Leader 节点与 Follower 节点之间会定期发送心跳包,以检测主节点是否活着。

当 Leader 节点的任期到了,Leader 节点的状态由 Leader 降级为 Follower,进入新一轮选主。

优点:选举速度快、算法复杂度低、易于实现的优点

缺点:要求系统内每个节点都可以相互通信,且需要获得过半的投票数才能选主成功,因此通信量大

ZAB算法

相较于 Raft 算法的投票机制,ZAB 算法增加了通过节点 ID 和数据 ID 作为参考进行选主,节点 ID 和数据 ID 越大,表示数据越新,优先成为主。

选举原则:server_zxID 最大者成为 Leader;若 server_zxID 相同,则 server_id 最大者成为 Leader。

server_id 表示本节点的唯一 ID;

server_zxID 表示本节点存放的数据 ID,数据 ID 越大表示数据越新,选举权重越大;

epoch 表示当前选取轮数,一般用逻辑时钟表示。

优点:稳定性比较好,性能高。

缺点:每个节点同时广播,每个节点同时广播。对比节点 ID 和数据 ID,需要知道所有节点的 ID 和数据 ID,所以选举时间相对较长。

eg:ZooKeeper 实现分布式协调功能

深入ZAB算法

ZAB算法分为两大块内容,消息广播和崩溃恢复。

消息广播(boardcast):Zab 协议中,所有的写请求都由 leader 来处理。正常工作状态下,leader 接收请求并通过广播协议来处理。

崩溃恢复(recovery):当服务初次启动,或者 leader 节点挂了,系统就会进入恢复模式,直到选出了有合法数量 follower 的新 leader,然后新 leader 负责将整个系统同步到最新状态。

ZAB消息广播过程

Zookeeper集群中,存在以下三种角色的节点:

Leader:Zookeeper集群的核心角色,在集群启动或崩溃恢复中通过Follower参与选举产生,为客户端提供读写服务,并对事务请求进行处理。 Follower:Zookeeper集群的核心角色,在集群启动或崩溃恢复中参加选举,没有被选上就是这个角色,为客户端提供读取服务,也就是处理非事务请求,Follower不能处理事务请求,对于收到的事务请求会转发给Leader。 Observer:观察者角色,不参加选举,为客户端提供读取服务,处理非事务请求*,对于收到的事务请求会转发给Leader。使用Observer的目的是为了扩展系统,提高读取性能。

Leader 接收到消息请求后,将消息赋予一个全局唯一的 64 位自增 id,叫做:zxid,通过 zxid 的大小比较即可实现因果有序这一特性。

Leader 通过先进先出队列(通过 TCP 协议来实现,以此实现了全局有序这一特性)将带有 zxid 的消息作为一个提案(proposal)分发给所有 follower。

当 follower 接收到 proposal,先将 proposal 写到硬盘,写硬盘成功后再向 leader 回一个 ACK。

当 leader 接收到合法数量的 ACKs 后,leader 就向所有 follower 发送 COMMIT 命令,同时会在本地执行该消息。

当 follower 收到消息的 COMMIT 命令时,就会执行该消息。

相比于完整的二阶段提交,Zab 协议最大的区别就是不能终止事务,follower 要么回 ACK 给 leader,要么抛弃 leader,在某一时刻,leader 的状态与 follower 的状态很可能不一致,因此它不能处理 leader 挂掉的情况,所以 Zab 协议引入了恢复模式来处理这一问题。

从另一角度看,正因为 Zab 的广播过程不需要终止事务,也就是说不需要所有 follower 都返回 ACK 才能进行 COMMIT,而是只需要合法数量(2n+1 台服务器中的 n+1 台) 的follower,也提升了整体的性能。

(commit类似两阶段提交的commit,一般错误概率非常小),可能出现的就是主节点挂了的情况,主节点commit,其他节点还没收到

Leader 服务器与每一个 Follower 服务器之间都维护了一个单独的 FIFO 消息队列进行收发消息,使用队列消息可以做到异步解耦。 Leader 和 Follower 之间只需要往队列中发消息即可。如果使用同步的方式会引起阻塞,性能要下降很多。

选举参数

在介绍选举流程之前,需要介绍几个参数,

myid: 服务器ID,这个是在安装Zookeeper时配置的,myid越大,该服务器在选举中被选为Leader的优先级会越大。ZAB算法中通过myid来规避了多个节点可能有相同zxid问题,注意可以对比之前的Raft算法,Raft算法中通过随机的timeout来规避多个节点可能同时成为Leader的问题。

zxid: 事务ID,这个是由Zookeeper集群中的Leader节点进行Proposal时生成的全局唯一的事务ID,由于只有Leader才能进行Proposal,所以这个zxid很容易做到全局唯一且自增。因为Follower没有生成zxid的权限。zxid越大,表示当前节点上提交成功了最新的事务,这也是为什么在崩溃恢复的时候,需要优先考虑zxid的原因。

epoch: 投票轮次,每完成一次Leader选举的投票,当前Leader节点的epoch会增加一次。在没有Leader时,本轮此的epoch会保持不变。

另外在选举的过程中,每个节点的当前状态会在以下几种状态之中进行转变。

LOOKING: 竞选状态。

FOLLOWING: 随从状态,同步Leader 状态,参与Leader选举的投票过程。

OBSERVING: 观察状态,同步Leader 状态,不参与Leader选举的投票过程。

LEADING: 领导者状态。

选举流程

选举的流程如下:

每个Server会发出一个投票,第一次都是投自己。投票信息:(myid,ZXID)

收集来自各个服务器的投票

处理投票并重新投票,处理逻辑:优先比较ZXID,然后比较myid

统计投票,只要超过半数的机器接收到同样的投票信息,就可以确定leader

改变服务器状态,进入正常的消息广播流程。

ZAB算法需要解决的两大问题

  1. 已经被处理的消息不能丢

这一情况会出现在以下场景:当 leader 收到合法数量 follower 的 ACKs 后,就向各个 follower 广播 COMMIT 命令,同时也会在本地执行 COMMIT 并向连接的客户端返回「成功」。但是如果在各个 follower 在收到 COMMIT 命令前 leader 就挂了,导致剩下的服务器并没有执行都这条消息。

为了实现已经被处理的消息不能丢这个目的,Zab 的恢复模式使用了以下的策略:

选举拥有 proposal 最大值(即 zxid 最大) 的节点作为新的 leader:由于所有提案被 COMMIT 之前必须有合法数量的 follower ACK,即必须有合法数量的服务器的事务日志上有该提案的 proposal,因此,只要有合法数量的节点正常工作,就必然有一个节点保存了所有被 COMMIT 的 proposal。 而在选举Leader的过程中,会比较zxid,因此选举出来的Leader必然会包含所有被COMMIT的proposal。

新的 leader 将自己事务日志中 proposal 但未 COMMIT 的消息处理。

新的 leader 与 follower 建立先进先出的队列, 先将自身有而 follower 没有的 proposal 发送给 follower,再将这些 proposal 的 COMMIT 命令发送给 follower,以保证所有的 follower 都保存了所有的 proposal、所有的 follower 都处理了所有的消息。

2. 被丢弃的消息不能再次出现

(通过没有发送ack可以判断是否要删除)

这一情况会出现在以下场景:当 leader 接收到消息请求生成 proposal 后就挂了,其他 follower 并没有收到此 proposal,因此经过恢复模式重新选了 leader 后,这条消息是被跳过的。 此时,之前挂了的 leader 重新启动并注册成了 follower,他保留了被跳过消息的 proposal 状态,与整个系统的状态是不一致的,需要将其删除。

Zab 通过巧妙的设计 zxid 来实现这一目的。一个 zxid 是64位,高 32 是纪元(epoch)编号,每经过一次 leader 选举产生一个新的 leader,新 leader 会将 epoch 号 +1。低 32 位是消息计数器,每接收到一条消息这个值 +1,新 leader 选举后这个值重置为 0。这样设计的好处是旧的 leader 挂了后重启,它不会被选举为 leader,因为此时它的 zxid 肯定小于当前的新 leader。当旧的 leader 作为 follower 接入新的 leader 后,新的 leader 会让它将所有的拥有旧的 epoch 号的未被 COMMIT 的 proposal 清除。

Zab 协议设计的优秀之处有两点,一是简化二阶段提交,提升了在正常工作情况下的性能;二是巧妙地利用率自增序列,简化了异常恢复的逻辑,也很好地保证了顺序处理这一特性