【go共识算法】-Raft

介绍

Raft 状态

一个 Raft 集群包含若干个服务器节点;通常是 5 个,这允许整个系统容忍 2 个节点的失效,每个节点处于以下三种状态之一:

  • follower :所有结点都以 follower 的状态开始。如果没收到leader消息则会变成 candidate状态。
  • candidate:会向其他结点“拉选票”,如果得到大部分的票则成为leader。这个过程就叫做Leader选举(Leader Election)。
  • leader:所有对系统的修改都会先经过leader。

Raft 一致性算法

Raft通过选出一个leader来简化日志副本的管理,例如,日志项(log entry)只允许从leader流向follower。

基于leader的方法,Raft算法可以分解成三个子问题:

  • Leader election (领导选举):原来的leader挂掉后,必须选出一个新的leader
  • Log replication (日志复制):leader从客户端接收日志,并复制到整个集群中
  • Safety (安全性):如果有任意的server将日志项回放到状态机中了,那么其他的server只会回放相同的日志项
Leader election (领导选举)

Raft 使用一种心跳机制来触发领导人选举。当服务器程序启动时,他们都是 follower(跟随者) 身份。如果一个跟随者在一段时间里没有接收到任何消息,也就是选举超时,然后他就会认为系统中没有可用的领导者然后开始进行选举以选出新的领导者。要开始一次选举过程,follower 会给当前term加1并且转换成candidate状态。

然后他会并行的向集群中的其他服务器节点发送请求投票的 RPCs 来给自己投票。候选人的状态维持直到发生以下任何一个条件发生的时候.

  • 他自己赢得了这次的选举

    • 如果这个节点赢得了半数以上的vote就会成为leader,每个节点会按照first-come-first-served的原则进行投票,并且一个term中只能投给一个节点, 这样就保证了一个term最多有一个节点赢得半数以上的vote
    • 当一个节点赢得选举, 他会成为leader, 并且给所有节点发送这个信息, 这样所有节点都会回退成follower。
  • 其他的服务器成为领导者,如果在等待选举期间,candidate接收到其他server要成为leader的RPC,分两种情况处理:

    • 如果leader的term大于或等于自身的term,那么改candidate 会转成follower 状态
    • 如果leader的term小于自身的term,那么会拒绝该 leader,并继续保持candidate 状态
  • 一段时间之后没有任何一个获胜的人

    • 有可能,很多follower同时变成candidate,导致没有candidate能获得大多数的选举,从而导致无法选出leader。当这个情况发生时,每个candidate会超时,然后重新发增加term,发起新一轮选举RPC。需要注意的是,如果没有特别处理,可能出导致无限地重复选主的情况。
    • Raft采用随机定时器的方法来避免上述情况,每个candidate选择一个时间间隔内的随机值,例如150-300ms,采用这种机制,一般只有一个server会进入candidate状态,然后获得大多数server的选举,最后成为主。每个candidate在收到leader的心跳信息后会重启定时器,从而避免在leader正常工作时,会发生选举的情况。
Log replication (日志复制)

当选出 leader 后,它会开始接受客户端请求,每个请求会带有一个指令,可以被回放到状态机中。leader 把指令追加成一个log entry,然后通过AppendEntries RPC并行的发送给其他的server,当该entry被多数派server复制后,leader 会把该entry回放到状态机中,然后把结果返回给客户端。

当 follower 宕机或者运行较慢时,leader 会无限地重发AppendEntries给这些follower,直到所有的follower都复制了该log entry。

Raft的log replication保证以下性质(Log Matching Property):

  • 如果两个log entry有相同的indexterm,那么它们存储相同的指令
  • 如果两个log entry在两份不同的日志中,并且有相同的indexterm,那么它们之前的log entry是完全相同的

其中特性一通过以下保证:

  • leader在一个特定的termindex下,只会创建一个log entry
  • log entry不会改变它们在日志中的位置

特性二通过以下保证:

  • AppendEntries会做log entry的一致性检查,当发送一个AppendEntriesRPC时,leader会带上需要复制的log entry前一个log entry的(index, iterm),如果follower没有发现与它一样的log entry,那么它会拒绝接受新的log entry 这样就能保证特性二得以满足。

动画演示Raft流程

http://thesecretlivesofdata.c...

go实现Raft

项目地址

https://github.com/goraft/raft

数据结构

https://blog.csdn.net/xiongwe...

goraft主要抽象了server、peer和log三个结构,分别代表服务节点、Follower节点和日志。

server

Raft作为一种多节点状态一致性维护协议,运行过程中必然涉及到多个物理节点,server就是用来抽象其中的每个节点,维护节点的状态信息。其结构如下:

type server struct {
    *eventDispatcher

    name        string
    path        string
    state       string          // 每个节点总是处于以下状态的一种:follower、candidate、leader
    transporter Transporter
    context     interface{}
    currentTerm uint64          // Raft协议关键概念,每个term内都会产生一个新的leader

    votedFor   string
    log        *Log
    leader     string
    peers      map[string]*Peer // raft中每个节点需要了解其他节点信息,尤其是leader节点
    mutex      sync.RWMutex
    syncedPeer map[string]bool  // 对于leader来说,该成员记录了日志已经被sync到了哪些follower

    stopped           chan bool
    c                 chan *ev  // 当前节点的命令通道,所有的命令都通过该channel来传递
    electionTimeout   time.Duration
    heartbeatInterval time.Duration

    snapshot *Snapshot

    // PendingSnapshot is an unfinished snapshot.
    // After the pendingSnapshot is saved to disk,
    // it will be set to snapshot and also will be
    // set to nil.
    pendingSnapshot *Snapshot

    stateMachine            StateMachine
    maxLogEntriesPerRequest uint64
    connectionString string
    routineGroup sync.WaitGroup
}

log

log是Raft协议的核心,Raft使用日志来存储客户发起的命令,并通过日志内容的同步来维护多节点上状态的一致性。

// A log is a collection of log entries that are persisted to durable storage.
type Log struct {
    ApplyFunc   func(*LogEntry, Command) (interface{}, error)    // 日志被应用至状态机的方法,这个应该由使用raft的客户决定
    file        *os.File // 日志文件句柄
    path        string   // 日志文件路径
    entries     []*LogEntry  // 内存日志项缓存
    commitIndex uint64  // 日志提交点,小于该提交点的日志均已经被应用至状态机
    mutex       sync.RWMutex
    startIndex  uint64  // 日志中起始日志项的index
    startTerm   uint64  // 日志中起始日志项的term
    initialized bool
}

log entry

log entry是客户发起的command存储在日志文件中的内容

 
// 编码后的日志项包含Index、Term,原始Command的名称以及Command具体内容
type LogEntry struct {
    Index            *uint64 `protobuf:"varint,1,req" json:"Index,omitempty"`
    Term             *uint64 `protobuf:"varint,2,req" json:"Term,omitempty"`
    CommandName      *string `protobuf:"bytes,3,req" json:"CommandName,omitempty"`
    Command          []byte  `protobuf:"bytes,4,opt" json:"Command,omitempty"`
    XXX_unrecognized []byte  `json:"-"`
}

// A log entry stores a single item in the log.
// LogEntry是日志项在内存中的描述结构,其最终存储在日志文件是经过protocol buffer编码以后的信息
type LogEntry struct {
    pb       *protobuf.LogEntry
    Position int64 // position in the log file Position代表日志项存储在日志文件内的偏移
    log      *Log
    event    *ev
}

来源:https://segmentfault.com/a/1190000016641336
原文地址:https://www.cnblogs.com/gao88/p/12582946.html