前言
大概从19年5月份的时候才接触Raft 算法,当时是在网上找相关分布式相关的东西,然后就和 Raft 算法'相识'了。当时自己从 github 上下载 mit6.824的代码 跟着课程详解做到了 lab2b ,然后就放弃了,然后紧接着开始浑浑噩噩的生活,最近觉得自己不能就这么堕落下去,才有了这篇文章。 代码地址
Raft 定义
Raft 是一种分布式、 多副本同步算法,解决分布式共识问题。学术界最出名的就是 Paxos 算法,但是由于其非常的难于理解,只能算是学术界的,而工程界的最出名的就是Raft,以简单易懂出名。理解起来可能是非常的简单,但是实际把它撸出来还是有点难度的。Raft 之所以简单,要得益于它将问题拆开来分析解决。Raft 将分布式共识问题拆解成:选举
、日志复制
、安全性
和成员变更
。
Raft 选举
Raft 算法中每个节点只会有三个状态 :
leader
follower
candidate
当节点启用时默认都为follower
状态。下面是三个状态之间的转换图:
借鉴
开源界中有很多实现 Raft 算法的工程,其中最广为人知的莫过于 Etcd ,因此我这边会借鉴 Etcd 的 架构以及关键的设计,你们也可以说抄。
选择语言
我本身是 Java 语言出身的,按理说我要选择 Java 语言,但是 mit6.824 以及 Etcd 都使用 go 语言,还有开源分布式数据库 TiDB 也是采用 go 的,因此我选择了 go,还有就是 go 拥有天生的并发性以及简单易学,就是还有 GC 这点不行,但是我相信自己会解决的。当然,我也考虑过使用 Rust 语言实现,但是这门语言是真的难,我害怕影响的我的编码体验就没有选择。最终,我选择使用 go 来实现。
项目结构

cli
模块的功能主要是连接 myraft 的客户端;config
模块的功能主要是封装myraft 所需要的配置以及一些配置帮助类;member
模块主要是封装成员以及集群;myRaft-server 模块是启动raft 实例入口;raft
模块就是实现 raft 核心算法的地方,raft
模块下的 transport
模块实现 raft 集群之间通信的的功能;types
模块主要封装一些自己定义的类型。
实现
配置文件&配置结构体(RaftConfig)

如图二,myraft 的配置文件简单,localAddr
表示本地地址即 ip:port
组合 ,clusterAddr
表示集群地址 即多个 ip:port
的组合并使用 英文,
隔开。配置结构体(RaftConfig)就只有两个属性 LocalAddr
和ClusterAddr
,两个属性均为 string 类型。上代码:
type RaftConfig struct {
LocalAddr string //类似 ‘127.0.0.1:6379’
ClusterAddr string //集群内地址 127.0.0.1:6379,127.0.0.1:6379
}
//通过配置文件路径生成RaftConfig 实例
func NewConfig(configPath string) (rf *RaftConfig, e error) {
rf = &RaftConfig{}
//解析配置文件返回map
configKv := InitConfig(configPath)
rf.LocalAddr = configKv["localAddr"]
rf.ClusterAddr = configKv["clusterAddr"]
return rf, nil
}
成员管理
Raft 集群包含多个成员,因此我这里抽象出两个实例:Member
和Cluster
。一个代表集群中的成员,另一个代表集群。Member
结构体同样有两个属性: ID
和peerAddr
,peerAddr
就是上面配置文件中的每个 raft 实例的地址,而 ID
代表每个 raft 实例唯一标识,是 uint64 类型的,生成的代码如下:
type ID uint64
//addr 为 ip:port 格式
func GenerateID(addr string) ID {
var b []byte
b = append(b, []byte(addr)...)
hash := sha1.Sum(b) //sha1 算法散列表得到散列值
return ID(binary.BigEndian.Uint64(hash[:8])) // 对散列值后8个字节进行取整
}
关于集群,我定义了一个接口:Cluster
type Cluster interface {
// ID returns the cluster ID
ID() types.ID
// Members returns a slice of members sorted by their ID
Members() []*Member
// Member retrieves a particular member based on ID, or nil if the
// member does not exist in the cluster
Member(id types.ID) *Member
}
并定义了 RaftConfig
结构体来实现 Cluster
接口:
type RaftCluster struct {
localID types.ID //当前节点唯一标识
cid types.ID //当前集群唯一标识
members map[types.ID]*Member //raft 集群成员
}
创建 RaftCluster
实例以及Member
实例:
//clusterAddr like 127.0.0.1:9009,127.0.0.1:9010,127.0.0.1:9011,localAddr like 127.0.0.1:9011
func NewRaftCluster(localAddr string, clusterAddr string) *RaftCluster {
//创建RaftCluster 实例
rc := &RaftCluster{
members: make(map[types.ID]*Member),
}
//根据localAddr 生成 当前 raft id
rc.localID = types.GenerateID(localAddr)
// 循环生成Member 实例 并放进 members 属性中
clusterAddrArrs := strings.Split(clusterAddr, ",")
for _, peerAddr := range clusterAddrArrs {
m := NewMember(peerAddr)
rc.members[m.ID] = m
}
//生成集群id 使用的是集群地址
rc.cid = types.GenerateID(clusterAddr)
return rc
}
Raft 节点之间的传输实现
根据 Raft 论文,Raft 主要有两种RPC 一种是选举请求,另外一种是 AppendEntries
即 Leader 节点向 follower 同步节点;leader 节点向 follower 广播心跳也是使用 AppendEntries
rpc 只不过日志为空罢了。我这边的传输实现借鉴了 Etcd 的传输实现。
首先我们来说消息结构体 RaftMessage
,代码如下:
type RaftMessage struct {
From uint64 //从哪里来
To uint64 //发送给谁
Type MessageType
Success bool //是否成功
Term uint64
LogIndex uint64
Entries []Entry
}
我这边将所有 Rpc 请求响应都抽象成为了结构体 RaftMessage
,
From
和To
代表着谁发的,然后发给谁的,它们的值使用Member
类中的ID
装填。- type 表示消息类型,其类型为
MessageType
, Raft 算法存在多种rpc
调用,比如投票请求、心跳请求等。 Success
表示是否成功Term
为每一轮Leader任期的任期号,几乎每个请求/响应都得带上自己的任期号LogIndex
日志条目索引。不同的rpc 传输代表的值不一样,eg:投票请求中代表候选者最后一个日志索引,而在添加日志请求中表示在新日志条目之前的日志条目索引。Entries
表示要添加的日志,这里是数组的原因就是为了性能