Raft 选举实现(一)

前言

 大概从19年5月份的时候才接触Raft 算法,当时是在网上找相关分布式相关的东西,然后就和 Raft 算法'相识'了。当时自己从 github 上下载 mit6.824的代码 跟着课程详解做到了 lab2b ,然后就放弃了,然后紧接着开始浑浑噩噩的生活,最近觉得自己不能就这么堕落下去,才有了这篇文章。
代码地址

Raft 定义

  Raft 是一种分布式、 多副本同步算法,解决分布式共识问题。学术界最出名的就是 Paxos 算法,但是由于其非常的难于理解,只能算是学术界的,而工程界的最出名的就是Raft,以简单易懂出名。理解起来可能是非常的简单,但是实际把它撸出来还是有点难度的。Raft 之所以简单,要得益于它将问题拆开来分析解决。Raft 将分布式共识问题拆解成:选举日志复制安全性成员变更

Raft 选举

Raft 算法中每个节点只会有三个状态 :

  • leader
  • follower
  • candidate
    当节点启用时默认都为follower状态。下面是三个状态之间的转换图:
    Raft 选举实现(一)_第1张图片
    图一:raft 节点状态转换图

借鉴

 开源界中有很多实现 Raft 算法的工程,其中最广为人知的莫过于 Etcd ,因此我这边会借鉴 Etcd 的 架构以及关键的设计,你们也可以说抄。

选择语言

  我本身是 Java 语言出身的,按理说我要选择 Java 语言,但是 mit6.824 以及 Etcd 都使用 go 语言,还有开源分布式数据库 TiDB 也是采用 go 的,因此我选择了 go,还有就是 go 拥有天生的并发性以及简单易学,就是还有 GC 这点不行,但是我相信自己会解决的。当然,我也考虑过使用 Rust 语言实现,但是这门语言是真的难,我害怕影响的我的编码体验就没有选择。最终,我选择使用 go 来实现。

项目结构

Raft 选举实现(一)_第2张图片
图二:myraft 项目结构图

  该项目结构不算合理,但是还是比较清晰的。 cli 模块的功能主要是连接 myraft 的客户端; config 模块的功能主要是封装myraft 所需要的配置以及一些配置帮助类; member 模块主要是封装成员以及集群;myRaft-server 模块是启动raft 实例入口; raft 模块就是实现 raft 核心算法的地方, raft模块下的 transport 模块实现 raft 集群之间通信的的功能; types 模块主要封装一些自己定义的类型。

实现

配置文件&配置结构体(RaftConfig)

Raft 选举实现(一)_第3张图片
图三:myraft 配置图

  如图二,myraft 的配置文件简单,localAddr 表示本地地址即 ip:port组合 ,clusterAddr 表示集群地址 即多个 ip:port的组合并使用 英文,隔开。配置结构体(RaftConfig)就只有两个属性 LocalAddrClusterAddr,两个属性均为 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 集群包含多个成员,因此我这里抽象出两个实例:MemberCluster。一个代表集群中的成员,另一个代表集群。Member 结构体同样有两个属性: IDpeerAddrpeerAddr就是上面配置文件中的每个 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

  • FromTo 代表着谁发的,然后发给谁的,它们的值使用 Member 类中的ID 装填。
  • type 表示消息类型,其类型为MessageType , Raft 算法存在多种 rpc 调用,比如投票请求、心跳请求等。
  • Success 表示是否成功
  • Term 为每一轮Leader任期的任期号,几乎每个请求/响应都得带上自己的任期号
  • LogIndex 日志条目索引。不同的rpc 传输代表的值不一样,eg:投票请求中代表候选者最后一个日志索引,而在添加日志请求中表示在新日志条目之前的日志条目索引。
  • Entries 表示要添加的日志,这里是数组的原因就是为了性能

你可能感兴趣的:(Raft 选举实现(一))