在学习Raft之前,应该先了解Paxos,可以看 通俗的理解 Basic Paxos
背景
Raft 属于分布式共识算法,先明白什么是分布式共识,用一句话说就是“保证多节点下数据一致”,Raft的一致性是强一致性(线性一致性),看下图
假设我们服务有3个节点,分别为S1,S2,S3, 节点可以存储数据,以日志的方式存储,下图中表示都存储了x=1的日志。每个服务器都可以接口客户端的请求,然后修改数据,修改后会同步给其他节点。
这种方式有什么问题? 可以分为两类,无法满足线性一致性,也无法满足最终一致性。
先看下线性一致性的异常情况,存在一种情况,如下:
- t1时刻写完数据后,数据修改成功,客户端知道x=2了.
- 在t3数据进行复制操作前的t2时刻,有客户端来读数据,发现x=1.
假如这两个客户端是同一个人,知道x更新为2后,再次请求发现x又变成1,这十分奇怪
另一种情况,最终一致性:
- t1时刻对s1写数据,s1追加
x=2的日志. - t2时刻对s2写数据,s2追加
x=3的日志.
然后s1,s2 开始同步数据,这会导致两者的数据不一致,s1的x=3,s2的x=2。并且对于s3来说,x=2 or 3 都有可能。
这是分布式第二个要解决的问题:数据一致性(最终一致性)。
在目前,有许多的成熟手段可以选用,例如:
- 2pc、3pc
- Paxos
- ZAB
- Raft
- POW、POS (Bitcoin 领域)
其中Raft可以算是运用最广泛和成熟的之一,RocketMQ、Etcd、Nacos、Tidb/TiKv、Consul ,接下来看看Raft是如何解决这个问题的。
回到这个图,首先做的第一件事,就是修改请求的规则,在3个服务中选出一个领导人leader,所有的请求都由leader进行处理,其他非领导人的节点就成为follower(后面还会引入一个候选人角色,这里先不管).
leader的任务是处理客户端请求,当有请求时,s1先将数据写入到本地,然后同步给其他节点。这里的核心点是不需要所有节点都成功同步才表示成功,而是只要超过一半的节点同步数据成功,就可以返回给客户端成功响应。
图中有3个节点,,意味着2个以上节点同步了
x=2的日志时,s1即可返回给客户端。
这里注意,同步到节点数量包含s1自己的,也就是说,其实只要其他任意一个节点同步成功就可以了。
这种过半提交的机制专业术语叫做Quorum(仲裁) 。这个机制的好处是什么呢?
- leader不必要等待所有follower节点的确认,提高了响应速度。
- 即使有一半的服务发生故障,集群依然可以正常运行。
上面提到了两个步骤,leader选举和请求处理,下面就这两个点详细说明:
ledaer 选举
那假如是leader挂了呢,如何保证服务的可用性?这里就牵扯出Raft第一个概念,Leader选举。leader选举会发生于两个时间点:
- 整个集群启动时,没有leader,需要选举出来。
- 当follower认为leader发生故障时,会发起选举投票。
假设我们有3个节点为a,b,c,他们的身份启动时都为follower, term是一个自增值,表示每个节点当前的任期编号,如下图:
每个follower都会有一个心跳等待时间,这个时间是随机的,在150~300 ms 范围内。如下图,用这个白圈表示心跳倒计时。
当超过这个心跳等待时间后,follower就认为leader发生故障了,这时follower就会改变身份,变成候选人candidate。
下图中B的心跳倒计时已经到了,A和C还没到,B会最先变成候选人。
B会先给自己投一票,然后Term+1,向其他节点发送请求,开始竞选。
其他节点收到竞选请求时,如果发现term大于自己的term,就会同意这次投票,反之投拒绝票。图中A和C因为term=0 < 1 ,所以会投同意票。在投了同意票之后,A,C更新自己的Term的值为1,心跳倒计时也会重新计时。
B节点收到响应后,此次选举的赞同票数超过了半数,B会晋升为leader,leader要做的事情就是定时向其他节点发送心跳包,例如50ms一次。这样选举的过程就完成了。
无论是集群初始化的leader选举还是leader超时后的选举,都是follower发现心跳超时后,变成candidate进行选举的过程,两者没有任何区别。
关键词总结:
- follower随机心跳等待时间
- candidate过半晋升
- leader定时心跳
日志同步
当前的leader是C。现在有一个客户端准备发送请求
客户端发送了一个请求给leader,内容是set 5
leader不能马上对客户端的写请求进行响应,因为raft要保证强一致性,leader会按照下面的步骤进行:
- 将
set 5的请求发送给其他节点 - 其他节点收到leader的请求后,将数据写入到本地,然后响应给leader
- leader收到超过半数的写入成功响应后,commit这个日志,然后响应给客户端成功。
什么时候通知给其他节点这个日志可以commit呢?一种常见的做法是在leader的下次心跳中携带相关信息,通知所有节点进行commit。
这里也采用了Quorum的机制,为什么凭借这种方式可以保证日志的一致性呢?下面是一些设计细节。
首先,日志的存储有携带索引+当前leader的term,下图中最新的一条日志是在索引10位置,term=6。
follow节点的日志是可能和leader存在不同的,下面a~f的情况都有可能
raft采用了以leader的日志为标准的方式,所有follower必须无条件接受leader的日志,这里可能有一个疑惑,leader的日志就可以保证是最新的、最全的吗?结合选举和日志提交的规则再思考下,这里可能要画画图花一些时间理解下:
- 选举
- 节点只会给term大于自己,日志不落后自己的候选人投票
- 同时候选人需要有一半以上的节点同意,才可以当选为leader。
- 日志提交
- 需要有一半的节点同意,日志才可以提交。
- 日志以leader的为准,如果存在不同,进行覆盖。
主要要理解的一点是,无论如何,过半的机制,总会保证至少有一个log完整的节点参与到leader选举中,然后并当选为leader。
以上图中d的情况来说,可能有疑惑这个下标11,12位置的日志不是最新的吗,发生这个情况的原因大概率是这样:
- term=7的leader在收到客户端请求后,先将日志写入本地索引11,12。
- leader还未将新日志发送给其他节点,就宕机了。
- 然后其他节点竞选leader,这个日志就没了。
怎么推动出会是这种情况的,这里进行反推:
- 假如leader本地写入索引11,12后,向任何节点其一发送了成功了请求
- 那么当leader宕机后,新选举的leader要求是日志最新的,那么就一定是这个收到的请求的节点,因为他的日志最新。
可能有疑惑,这个日志没了?这合理吗。这个日志并未commit,就没有给客户端响应,仅仅属于消息丢失的问题,客户端可以使用重试机制重发。
其他一些细节点:
- 为什么节点推荐奇数个?
当发生节点网络分区时,一分为二时,可以保证有一边可以正常选举出leader。
引用参考
- 要搞懂,还是要看10遍以上论文:yuerblog.cc/wp-content/…
- 动画来源,非常好的可视化网站:www.kailing.pub/raft/index.…
- mit课程大纲:mit-public-courses-cn-translatio.gitbook.io/mit6-824/le…