一、前言
Raft算法是一种分布式一致性算法,由Diego Ongaro和John Ousterhout在2013年提出。它主要用于分布式系统中,保证系统中的数据在多个节点间保持一致性。
Raft算法被广泛应用于众多分布式系统中,尤其是在需要强一致性保证的场景中,例如:
-
分布式存储系统:如ETCD、Consul等键值存储系统,它们利用Raft算法来保证数据的强一致性和高可用性。
-
分布式数据库:一些分布式数据库管理系统(DBMS),如CockroachDB等。
-
分布式锁服务:例如Google的Chubby以及微软的Azure Service Bus等。
得物的多个内部中间件也是使用Raft算法作为多分片一致性的保证。
长期以来,大部分开发者都是将Raft作为一个黑盒使用,只知道它能保证多分片的一致性,对其运行原理也停留在纸面。当面临Raft性能调优或者奇怪的Raft问题排障的时候则束手无策。
费曼说过:“What I cannot create, I do not understand。” 我们中国先贤也强调“知行合一,以致良知”。如果我们不能亲手编写一次Raft算法,对这个东西就不能算作理解。
二、核心概念
在着手开始写之前,我们先介绍几个Raft算法中的核心概念。
日志复制状态机
如果说什么是分布式系统理论的基石,那一定“日志”。此处的日志与我们平时在应用中打印的给人阅读的信息不同,它是最简单的存储抽象,是按时间排序的append-only的、有序的记录序列。

“日志”揭示了系统当下正在发生的事实。
关于日志的更深入理解,可以参考博客The Log: What every software engineer should know about real-time data’s unifying abstraction,非常全面深刻。
当进入分布式的领域,日志就需要“状态机”的辅助:
-
如果两个相同的确定性过程以相同的状态开始并以相同的顺序获得相同的输入,它们将产生相同的输出并以相同的状态结束。
-
确定性意味着处理不依赖于时间,并且不会让任何其他输入影响其结果。例如,一个程序的输出受到线程特定执行顺序或调用gettimeofday或其他一些不可重复的东西的影响,通常最好被认为是非确定性的。
-
进程的状态是在处理结束时保留在机器上的任何数据,无论是在内存中还是在磁盘上。
关于以相同的顺序获得相同的输入的一点应该会引起人们的注意——这就是日志的用武之地。这是一个非常直观的概念:如果你将两段确定性代码提供给相同的输入日志,它们将产生相同的输出。
所以Raft最重要的任务就是解决如何在分布式系统中使多个副本的日志数据达成一致的问题。
Leader、Follower、Candidate
为了实现上述目标,Raft使用强领导模型,即要求集群中的一个副本充当Leader,其他副本充当Follower。
Leader负责根据客户端请求采取行动生成命令,将命令复制给Follower,并将响应返回给客户端。
多个副本根据选举算法推选合法的Leader。

这个方案解决了分布式系统中的以下问题:
-
容错性(Fault Tolerance):分布式系统中可能会出现节点故障,包括宕机、网络分区等问题。通过这些角色的分工和协作,系统可以在一定数量的节点出现故障时仍然正常运行。
-
领导者选举(Leader Election):在分布式系统中,通常需要一个节点(Leader)来协调其他节点(Followers)的工作,以确保一致性和效率。Leader负责管理日志复制、决策提议等任务。当Leader失效时,系统需要能够选举出新的Leader。
-
一致性(Consensus):为了保证系统状态的一致性,需要所有节点就某个值或状态达成共识。这通常通过一系列的投票和通信过程来实现,其中Candidate角色在选举过程中起到关键作用。
-
去中心化(Decentralization):在去中心化的系统中,没有中央权威来决定系统状态。这些角色的引入有助于在没有中心节点的情况下实现一致性和决策。
这种模型有其优点和缺点:
-
显著的优点是简单。数据总是从领导者流向Leader,只有Leader响应客户端请求(工程实践中有例外)。这使得Raft集群更容易分析、测试和调试。
-
缺点是性能——因为集群中只有一台服务器与客户端通信,这在客户端活动激增的情况下可能成为瓶颈。当然这个可以通过一些工程手段在一致性和吞吐性能中做出平衡,例如我们可以放弃强一致的要求,从Follower中读取数据,减轻Leader的负担,关于一致性的部分,可以参考文章《共识、线性一致性、顺序一致性、最终一致性、强一致性概念区分》。
三、Why Elixir
本系列中介绍的Raft实现是用Elixir编写的。在笔者看来,Elixir具有三个优势,使其成为本系列和网络服务的有前途的实现语言:
-
Raft框架需要大量的网络编程,而Elixir基于Erlang在网络编程方面体验一骑绝尘,甚至自带RPC实现;
-
Elixir自带一个功能强大的Shell,开发调试的难度大大降低;
-
Raft这类算法需要大量的并发编程,而并发编程在Elixir中就像呼吸一样简单;
-
Erlang语言的OTP框架大大降低的服务器编程的心智负担,常见的服务端编程场景都有合适的解决方案。
这个项目的开发中,笔者借鉴了这个生产级别的开源Raft库——龙舟的实现细节,如果对生产级的Raft实现感兴趣,可以阅读龙舟的代码。
我们的实现大致代码结构如下:
.
├── README.md
├── Taskfile.yaml
├── lib
│ └── ex_raft
│ ├── config.ex
│ ├── core # 各个状态下的处理逻辑
│ │ ├── candidate.ex
│ │ ├── common.ex
│ │ ├── follower.ex
│ │ ├── free.ex
│ │ ├── leader.ex
│ │ └── prevote.ex
│ ├── debug.ex
│ ├── exception.ex
│ ├── guards.ex
│ ├── log_store # 日志存储,本节暂时不用
│ │ ├── cub.ex
│ │ └── inmem.ex
│ ├── log_store.ex

最低0.47元/天 解锁文章
385

被折叠的 条评论
为什么被折叠?



