前言
大概从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
表示要添加的日志,这里是数组的原因就是为了性能