An open API service indexing awesome lists of open source software.

https://github.com/impact-eintr/raft

raft学习
https://github.com/impact-eintr/raft

Last synced: 4 months ago
JSON representation

raft学习

Awesome Lists containing this project

README

          

# raft
raft学习

### 领导人选举
在一个领导人节点发射管故障之后必须重新给出一个新的领导人

### 日志复制
领导人节点从客户端接受操作请求,然后将操作日志复制到集群的其他服务器上,并且强制要求其他服务器的日志必须和自己的保持一致

### 安全性
Raft关键的安全特性是状态机安全原则(如果一个服务器已经将个顶索引位置的日志条目应用到状态机中,则所有其他服务器不会在该索引位置应用不同的条目)

### 成员关系变化
配置发生变化的时候,集群能够继续工作

> 出于简化操作和效率的个因素考虑,Raft算法采用的是非对称节点关系模型

## 三种角色
- Leader(领导人)
- Candidate(候选人)
- Follower(群众)

[一个任期开始]

1. 群众 =全部转为=> 候选人
2. 某个候选人 =获得半数以上的票=> 领导人
3. 其他候选人 =结束选举=> 群众

领导人 [继续剩下的任期] OR 没有选出领导人 [自动开始下一个任期]

## 任期

每个Raft节点在本地维护一个当前任期值,触发这个数字变化(增加)主要有两个场景:
1. 开始选举
2. 与其他节点交换信息

> 一些特殊场景

1. 当节点之间进行通信的时候,会互相交换当前的任期号,如果一个节点(包括Leader)的当前任期号比其他几点的任期号小,则将自己的任期号自觉更新为较大的任期号。

2. 如果一个候选人或者领导人意识到他的任期号过时了(比别人的小),他会立即变成群众。

3. 如果一个节点是偶到的请求所携带的任期号是过时的,那么该节点会拒绝本次请求

## 选举

### 心跳
Leader在任期内必须定期向集群的其他节点发送心跳包,昭告自己的存在

### 选举定时器
如果Follower在自己的选举定时器到期之后还没有收到Leader的心跳,就会认为苍天已死,黄天当立

> 一个Follower决定开始参加选举,会执行以下步骤

1. 将自己本地维护的当前任期号(current_term_id)加一
2. 将自己的状态切换为候选人状态,并为自己投票,也就是说每个候选人的第一张选票来自自己
3. 向其所在的集群中的其他节点发送`RequestVote RPC`(这里面会携带`current_term_id`),要求它们投票给自己

> 一个候选人有三种状态迁移的可能

1. 得到大多数节点的选票(包括自己),成为Leader
2. 发现其他节点赢得了选举,主动切换为Follower
3. 过了一段时间后发现没有人赢得选举,重新发起一次选举

### RequestVote RPC
> 调用方 候选人
> 接收方 集群中的所有其他节点(Leader Follwer Candidate)

| 参数 | 描述 |
|:------------:|:--------------------------------:|
| term | 选举人的任期号 |
| candidateId | 请求投票的候选人Id |
| lastLogIndex | 候选人最新日志条目的索引值(槽位) |
| lastLogTerm | 候选人最新日志条目对应的任期号 |

| 返回值 | 描述 |
|:-----------:|:----------------------------------------------:|
| term | 当前任期号,用于候选人更新自己本地的term值 |
| voteGranted | 如果候选人得到了这张选票,则为true,否则为false |

> RPC接收方需要实现的逻辑具体如下

1. 如果term < currentTerm,即RPC的第一个参数term的值小于接收方本地维护的term(currentTerm)值,则返回(currentTerm,false),以提醒调用方其term过时了,并且明确地告诉这位候选人这张选票不会投给他,否则执行步骤2
2. 如果之前没把选票投给任何人(包括自己)或者已经吧寻票投给当前候选人了,并且候选人的日志和自己的日志一样新,则返回(term,true),表示在这个任期,投票都投给这位候选人。如果之前已经吧选票投给其他人了,那么很遗憾,返回(term,false)

## 日志复制

### 过程
1. 客户端向Leader发送写请求
2. Leader将写请求解析成操作指令追加到本地日志文件中
3. Leader为每个Follower广播AppendEntries RPC
4. Follower通过一致性检查,选择从那个位置开始追加Leader的日志条目
5. 一旦日志提交成功,Leader就将该条目对应的指令应用(Apply)到本地将太极,并向客户端返回操作结果
6. Leader后续通过AppendEntrie RPC将已经成功(在大多数节点上)提交的日志项告知Follower
7. Follower收到提交的日志项之后,将其应用到本地状态机

从上面的步骤可以看出,针对Raft日志条目有两个操作,提交(Commit)和应用(Apply),应用必须发生在提交之后,即某个日志条目只有被提交之后才能应用到本地状态机上

### AppendEntries RPC

| 参数 | 描述 |
|:------------:|:-----------------------------------------------------------------------------------------:|
| term | 领导人的任期号 |
| leaderId | 领导人的ID,为了其他Raft节点能够重定向客户端请求 |
| prevLogIndex | 本次AppendEntries RPC新增日志的前一个位置的日志的索引值 |
| prevLogTerm | 本次AppendEntries RPC新增日志的前一个位置的日志的任期号 |
| entries[] | 将要追加到Follwer上的日志条目,发生心跳包的时候为空,有时会为了效率而向多个节点并发发送 |
| leaderCommit | 领导人会为每个Follwer都维护一个LeaderCommit,表示领导人认为Follwer已经提交的日志条目索引值 |

| 返回值 | 描述 |
|:-------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|
| term | 当前的任期号,即AppendEntries RPC参数中term(领导人的)与Follwer本地维护的当前任期号的较大值。用于领导人更新自己的任期号。一旦领导人发现当前任期号比自己的要大,就表明自己是一个"过时"的领导人,便停止发送AppendEntries RPC,主动切换为Follwer |
| success | 如果其他服务器包含能够匹配prevLogIndex和prevLogTerm的日志,则为真 |

> RPC的接收者需要实现如下的操作步骤

1. 如果 term < currentTerm,即领导人的任期号小于Follower本地维护当前任期号,则返回(currentTerm, false);否则继续步骤2
2. 如果Follower在prevLogIndex位置的之日的任期号与prevLogTerm不匹配,则返回(true, false);否则继续步骤3
3. Follower进行日志一致性检查
4. 添加任何在已有的日志中不存在的条目,删除多余的条目
5. 如果leaderCommit > commitIndex,则将commitIndex(Follower自己本地维护的已提交的日志条目)更新为min{leaderCommit,currentCommitIndex},即信任Leader的数据乐观地将不呢地已经提交日志的索引值”跃进“到领导人为该Follower跟踪的那个值(除非leaderCommit比本地最新的日志条目索引值还要大).这种场景通常发生在Follower刚从故障中恢复过来的场景。

## 安全性 Q & A
> 选举

- 没有包含所有已提交日志条目的节点成为不了领导人
- 日志条目只有一个流向:从Leader流向Follower。领导人永远不会覆盖已经存在的日志条目。
- 如果两个日志条目的任期号不同,则任期号大的更加新
- 如果任期号相同,则索引值更大(即日志条目更多)的日志更加新

> 提交日志

- 只要一个日志条目被存在了大多数服务器上,领导人就知道当前任期可以提交该条目了
- 如果领导人在提交日志前就崩溃了,之后的领导人会试着继续完成对日志的复制,但是,新上任的领导人无法断定大多数服务器上的日志一定在之前的任期中被提交了(即使日志保存在大部分的服务器上,也有可能没来的及提交)

## 可用性与时序
**RPC 调用&收到返回的平均用时 << 选举超时时间 << 单个节点宕机平均时长**

## 异常情况

### 群众/候选人 异常
如果一个群众/候选人崩溃了,那么领导人在这之后发送给他们的RequestVote RPC和 AppendEntries RPC就会失败。Raft通过领导人无限的重试来应对这些失败,直到故障的节点重新上号并处理了这些RPC为止,如果收到了相同的请求只会响应一次,其他会忽略。

### 领导人异常

1. 数据到达Leader前

2. 数据到达Leader节点,但未复制到Follower节点

3. 数据到达Leader节点,成功复制到Follower的部分节点上,但还未向Leader响应接收

4. 数据到达Leader节点,成功复制到Follower的全部节点上,但还未向Leader响应接收

5. 数据到达Leader节点,成功复制到Follower的全部/大多数节点上,数据在Leader上已经处于已提交状态,但在Follower上处于未提交状态

6. 数据到达Leader节点,成功复制到Follower的全部/大多数节点上,数据在Leader上已经处于已提交状态,且在所有节点上都处于已提交状态,但还没有响应client

7. 网络分区导致的脑裂情况,出现双Leader

## 日志压缩与快照
Raft的快照有如下特点
1. 每个节点独立创建,之包含已经被提交的日志条目
2. 存储了节点某一时刻复制状态机的状态
3. 全量式,非增量式(即使数据没有发生改变)
4. 在快照中存储少量元数据,比如,被快照取代的最后一个日志条目的索引位置和对应的任期号

### InstallSnapshot RPC

| 参数 | 描述 |
|:----------------:|:--------------------------------:|
| term | 领导人的任期号 |
| leaderId | 领导人的ID以便Follower重定向请求 |
| lastIncludedIndex | 快照中包含的最后一条日志的索引值 |
| lastIncludedTerm | 快照中包含的最后一条日志的任期号 |
| offset | 分块在快照文件中的偏移量 |
| data[] | 原始数据 |
| done | 如果这是最后一个分块则为true |

| 参数 | 描述 |
|:----:|:------------------------------------------------:|
| term | Follower当前的任期号,便于领导人更新自己的任期号 |

接收者实现快照的具体步骤如下:

1. 如果term < currentTerm ,则立即返回currentTerm,即如果节点的当前任期号大于Leader的任期号,则拒绝该快照,否则执行步骤2
2. 如果是第一个分块(offset=0),则创建一个新的快照
3. 在指定偏移量将分块数据写入快照文件,并响应Leader
4. 如果done为false ,则表示快照文件尚未传输完成,需要继续等待更多数据块
5. 当接收的done为true时,保存该快照文件,丢弃本地的lastIncludedIndex比较小的现存快照
6. 节点将根据快照包含的最后一条日志的索引值和任期号搜索与之匹配的日志项,如果存在,则继续保留后面该日志项之后的日志,前面的日志项将全部删除
7. 应用快照内容重置节点状态机,并且加载快照文件中的集群配置信息

Follower在收到快照的时候还要把它看作是一次心跳,重置自己的选举超时定时器