Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ls-2018/raft-demo
Golang implementation of the Raft consensus protocol
https://github.com/ls-2018/raft-demo
demo golang raft
Last synced: 5 days ago
JSON representation
Golang implementation of the Raft consensus protocol
- Host: GitHub
- URL: https://github.com/ls-2018/raft-demo
- Owner: ls-2018
- Created: 2022-01-07T11:01:09.000Z (almost 3 years ago)
- Default Branch: master
- Last Pushed: 2022-03-01T03:07:42.000Z (over 2 years ago)
- Last Synced: 2024-10-12T12:36:08.878Z (about 1 month ago)
- Topics: demo, golang, raft
- Language: Go
- Homepage:
- Size: 1.79 MB
- Stars: 1
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
### 一、Raft协议简介
* Raft 是一种为了管理复制日志的一致性协议
* 复制状态机概念,复制状态机是指每个状态机或系统如果初始状态一致,然后接受的改变状态的命令也一致,最后产生的结果状态也是相同。
#### 一致性!=完整性
```
一致性强调的不是数据完整,而是各节点间的数据一致。
```### 二、Raft选举过程
* 选举过程图1(单个节点视角)
![选举流程](./image/election.png)
* 选举过程图2(整体视角)
![选举流程2](./image/election_timeout.jpeg)
### 三、Raft日志复制流程
* 日志格式:term + index + cmd + type
![日志流程](./image/log_replicate.jpg)
* 请求处理整体流程
![日志流程](./image/machine_state.png)
* 请求处理详细流程(重点)
![日志流程](./image/request_process.png)
注:这里7. commit log 应该是在5. commit之后,但是因为commit策略的原因有一定延迟,所以从日志上看是在回复客户端以后
### 一条消息提交之后的流程
```- 1、Apply(cmd []byte, timeout time.Duration) ApplyFuture
- case r.applyCh <- logFuture:
- case newLog := <-r.applyCh:
- r.dispatchLogs(ready) 更新了lastIndex
- asyncNotifyCh(f.triggerCh) 触发每个follower的写操作
- case <-s.triggerCh:
- shouldStop = r.replicateTo(f, lastLogIdx)
- r.trans.AppendEntries(peer.ID, peer.Address, &req, &resp); follower会更新commit
- updateLastAppended(f, &req) follower写成功会触发
- f.commitment.match(f.peer.ID, last.Index) r.leaderState.commitment
- c.recalculate() 判断leader是否commit
- asyncNotifyCh(c.commitCh) newCommitment(r.leaderState.commitCh
- case <-r.leaderState.commitCh: 更新commitIndex
- r.processLogs(lastIdxInGroup, groupFutures)
- applyBatch(batch)
- case ptr := <-r.fsmMutateCh
- commitBatch(req) 存储到FSM- 2、ApplyFuture.Error()等待
```### 四、Raft协议动画演示
* Raft系统运行可视化1 [http://thesecretlivesofdata.com/raft](http://thesecretlivesofdata.com/raft/)
* Raft系统运行可视化2 [https://raft.github.io/#implementations](https://raft.github.io/#implementations)
### 五、hashicorp/raft源码讲解
* hashicorp/raft库 [https://github.com/hashicorp/raft](https://github.com/hashicorp/raft)
* raft启动流程
* raft监听事件
### 六、运行hashicorp/raft库搭建的简单kv服务
* 编译:go build -mod vendor
* 启动node1: ./raft-demo --http_addr=127.0.0.1:7001 --raft_addr=127.0.0.1:7000 --raft_id=1 --raft_cluster=1/127.0.0.1:
7000,2/127.0.0.1:8000,3/127.0.0.1:9000* 启动node2: ./raft-demo --http_addr=127.0.0.1:8001 --raft_addr=127.0.0.1:8000 --raft_id=2 --raft_cluster=1/127.0.0.1:
7000,2/127.0.0.1:8000,3/127.0.0.1:9000* 启动node3: ./raft-demo --http_addr=127.0.0.1:9001 --raft_addr=127.0.0.1:9000 --raft_id=3 --raft_cluster=1/127.0.0.1:
7000,2/127.0.0.1:8000,3/127.0.0.1:9000* set请求:curl http://127.0.0.1:7001/set?key=test_key&value=test_value
* get请求:curl http://127.0.0.1:7001/get?key=test_key
### 七、调试场景
##### 选举变化相关
1. 集群启动后,follower等待一个随机election timeout时间变成candidate,然后发起投票,如果不能获得majority票数,则任期term会一直增加(未pre-vote情况)
2. 集群启动后,follower等待一个随机election timeout时间变成candidate,然后发起投票,获得majority票数的节点变成leader
3. leader选举成功后发送heartbeat保持leader的地位
4. leader失去majority节点的heartbeat响应,退回到follower
##### 日志复制相关
1. leader接收客户端请求,向集群内所有节点发送复制RPC,所有都正常响应 -> 正常commit,然后apply到状态机,最后返回客户端处理成功
2. leader接收客户端请求,向集群内所有节点发送复制RPC,majority正常响应 -> 正常commit,然后apply到状态机,最后返回客户端处理成功
3. leader接收客户端请求,向集群内所有节点发送复制RPC,少于majority正常响应 -> 不能commit
### 八、收获
* hashicorp/raft源码
* raft选举与日志复制
### 九、QA
纠正:3个节点宕机2个,剩下一个不可用,怎么处理请求的强一致?
答:这个时候服务应该是不可用的,当然如果要强行提供查询的服务,强一致肯定是无法保证的。
### raft-stable.db
```azure
CurrentTerm:2
LastVoteCand: 127.0.0.1:10000
LastVoteTerm: 2
```### raft-log.db
```
key: 1,value :{Index:1 Term:1 Type:LogConfiguration Data:{Servers:[]} Extensions: AppendedAt:0001-01-01 00:00:00 +0000 UTC}
key: 2,value :{Index:2 Term:3 Type:LogNoop Data:{Servers:[]} Extensions: AppendedAt:2022-01-24 02:52:29.167131 +0000 UTC}
key: 3,value :{Index:3 Term:3 Type:LogCommand Data:{Servers:[]} Extensions: AppendedAt:2022-01-24 02:52:30.7774 +0000 UTC}
key: 4,value :{Index:4 Term:3 Type:LogCommand Data:{Servers:[]} Extensions: AppendedAt:2022-01-24 02:52:30.856799 +0000 UTC}
...
key: 84,value :&{Index:84 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 56 49 44 56 49] Extensions:[] AppendedAt:2022-01-20 11:02:35.959754 +0800 CST m=+8.364670689}
key: 85,value :&{Index:85 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 56 50 44 56 50] Extensions:[] AppendedAt:2022-01-20 11:02:35.998264 +0800 CST m=+8.403179990}
key: 86,value :&{Index:86 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 56 51 44 56 51] Extensions:[] AppendedAt:2022-01-20 11:02:36.036569 +0800 CST m=+8.441484039}
key: 87,value :&{Index:87 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 56 52 44 56 52] Extensions:[] AppendedAt:2022-01-20 11:02:36.075577 +0800 CST m=+8.480490624}
key: 88,value :&{Index:88 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 56 53 44 56 53] Extensions:[] AppendedAt:2022-01-20 11:02:36.113767 +0800 CST m=+8.518679029}
key: 89,value :&{Index:89 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 56 54 44 56 54] Extensions:[] AppendedAt:2022-01-20 11:02:36.153333 +0800 CST m=+8.558243818}
key: 90,value :&{Index:90 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 56 55 44 56 55] Extensions:[] AppendedAt:2022-01-20 11:02:36.192726 +0800 CST m=+8.597637183}
key: 91,value :&{Index:91 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 56 56 44 56 56] Extensions:[] AppendedAt:2022-01-20 11:02:36.232636 +0800 CST m=+8.637545273}
key: 92,value :&{Index:92 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 56 57 44 56 57] Extensions:[] AppendedAt:2022-01-20 11:02:36.246781 +0800 CST m=+8.651689157}
key: 93,value :&{Index:93 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 57 48 44 57 48] Extensions:[] AppendedAt:2022-01-20 11:02:36.275155 +0800 CST m=+8.680062738}
key: 94,value :&{Index:94 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 57 49 44 57 49] Extensions:[] AppendedAt:2022-01-20 11:02:36.314457 +0800 CST m=+8.719363894}
key: 95,value :&{Index:95 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 57 50 44 57 50] Extensions:[] AppendedAt:2022-01-20 11:02:36.354387 +0800 CST m=+8.759292103}
key: 96,value :&{Index:96 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 57 51 44 57 51] Extensions:[] AppendedAt:2022-01-20 11:02:36.393048 +0800 CST m=+8.797952296}
key: 97,value :&{Index:97 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 57 52 44 57 52] Extensions:[] AppendedAt:2022-01-20 11:02:36.431691 +0800 CST m=+8.836593571}
key: 98,value :&{Index:98 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 57 53 44 57 53] Extensions:[] AppendedAt:2022-01-20 11:02:36.471694 +0800 CST m=+8.876595735}
key: 99,value :&{Index:99 Term:2 Type:LogCommand Data:[115 101 116 44 97 43 57 54 44 57 54] Extensions:[] AppendedAt:2022-01-20 11:02:36.510897 +0800 CST m=+8.915797453}
```### rpcType
```azure
rpcAppendEntries
rpcRequestVote rpcInstallSnapshot
rpcTimeoutNow
```### Command Type
```
AppendEntriesRequest
RequestVoteRequest
InstallSnapshotRequest
TimeoutNowRequest
```### 竞选流程
- con 对每一个启动时制定好的节点进行rpc调用
```azure
req := &RequestVoteRequest{
RPCHeader: r.getRPCHeader(), // 只有协议版本
Term: r.getCurrentTerm(), // 当前任期
Candidate: r.trans.EncodePeer(r.localID, r.localAddr), // 竞选者,其实就是localAddr
LastLogIndex: lastIdx,// 最新的日志索引
LastLogTerm: lastTerm,// 当前的任期
LeadershipTransfer: r.candidateFromLeadershipTransfer, // 在这为false
}
RequestVote(本机的逻辑ID, 本机的通信地址, req, &resp.RequestVoteResponse)
```- 远端接收rpc调用
- handleCommand
- processRPC
- requestVote当某个节点得到了大多数的请求,自己就会变成leader,此时再有投票请求到来不会,不会对其投票【除非设置LeadershipTransfer=true】
问题:
- 1、集群最初,假设有两个节点都获得大多数投票,都使自身成为了leader?
```
在发送心跳的时候,会解决这个问题
if a.Term > r.getCurrentTerm() || r.getState() != Follower {
r.setState(Follower)
r.setCurrentTerm(a.Term)
resp.Term = a.Term
}
```- 2、问什么就算是日志开启了pipeline模式,也是每次都发送一条日志
```
因此每次有日志产生, 会 lastIndex++、调用replicateTo【串行】,也就是不会产生日志积累,也就不会在pipe里同时传输多条日志了
另外、 setNewLogs()函数是具体对AppendEntriesRequest进行日志封装的逻辑
min(nextIndex+uint64(maxAppendEntries)-1, lastIndex)-nextIndex+1 代表了此次请求携带的日志量另外、在非管道模式下,只要成功发送一次AppendEntriesRequest,就会进入到管道模式。也就说尽可能使用管道模式
```- 3、leader节点的崩溃可能会导致日志不一致:旧的leader可能没有完全复制日志中的所有条目。
```
在Raft下,leader通过强制followers复制它的日志来处理日志的不一致,不一致的日志会被强制覆盖。
follower首先找到最近(时间域)的一条日志条目,该日志条目在leader和follower的日志中为一致的。
然后删除follower上该日志条目所有日志,然后用leader的日志覆盖。
```- 4、打快照的时机
```
raft/config.go:137
SnapshotInterval 快照间隔 检测一次 && log db 增量条数 > SnapshotThreshold
```- 5、latestIndex、commitIndex、applyIndex的区别
```
// 最大被提交的日志项的索引值
commitIndex uint64
// 最新被应用到状态机的日志项的索引值
lastApplied uint64
// 存储中最新的日志项的索引值和任期编号
lastLogIndex uint64
lastLogTerm uint64
applyIndex<=commitIndex<=latestIndex
```
- 6、集群节点变更
```
通过调用 requestConfigChange
先将完整的 Configuration 异步同步给所有的节点
然后再setConfiguration时等待commit以及recalculate
在重新运行 RstartStopReplication 会对新增的节点启动心跳。。。 ;删除的节点取消心跳...如果集群本身已有数据,那么重新启动时设置的--raft_cluster没有作用; detail BootstrapCluster函数
```
- 7、快照复制时、会不会接受日志
```
1、replicateTo
2、sendLatestSnapshot
3、InstallSnapshot
会一直阻塞者
```
PrevLogTerm:表示当前要复制的日志项,前面一条日志项的任期编号。
PrevLogEntry:表示当前要复制的日志项,前面一条日志项的索引值。
// 领导者节点上的已提交的日志项的最大索引值