16 lines
2.3 KiB
Plaintext
16 lines
2.3 KiB
Plaintext
Raft 结构建议
|
||
|
||
一个 Raft 实例需要应对外部事件的到达(Start() 调用、AppendEntries 与 RequestVote RPC、以及 RPC 回复),并执行周期性任务(选举与心跳)。组织 Raft 代码来管理这些活动的方式很多;本文档简述几种思路。
|
||
|
||
每个 Raft 实例有一批状态(log、current index 等),需要在并发 goroutine 产生的事件下更新。Go 文档指出,goroutine 可以直接用共享数据结构和锁来更新,也可以通过 channel 传递消息。经验表明,对 Raft 而言最直接的是使用共享数据和锁。
|
||
|
||
一个 Raft 实例有两种由时间驱动的活动:leader 必须发送心跳,其他节点在超过一定时间未收到 leader 消息后必须发起选举。最好用各自独立的长驻 goroutine 驱动这两种活动,而不是把多种活动塞进一个 goroutine。
|
||
|
||
选举超时的管理常是头疼来源。或许最简单的做法是在 Raft 结构体里维护一个变量,记录该节点上次收到 leader 消息的时间,并让选举超时 goroutine 定期检查自那时起是否已超过超时时间。用带小常数的 time.Sleep() 驱动定期检查最简单。不要用 time.Ticker 和 time.Timer;它们容易用错。
|
||
|
||
你需要一个独立的长驻 goroutine,按顺序在 applyCh 上发送已提交的日志条目。它必须独立,因为向 applyCh 发送可能阻塞;且必须是单个 goroutine,否则很难保证按日志顺序发送。推进 commitIndex 的代码需要唤醒 apply goroutine;用条件变量(Go 的 sync.Cond)通常最简单。
|
||
|
||
每个 RPC 最好在各自的 goroutine 中发送(并处理回复),原因有二:让不可达的节点不会拖慢收集多数回复;让心跳和选举定时器随时都能继续走。在同一 goroutine 里处理 RPC 回复最简单,而不是通过 channel 传递回复信息。
|
||
|
||
要记住网络会延迟 RPC 和 RPC 回复,而且在并发发送 RPC 时,网络可能重排请求和回复。Figure 2 在指出 RPC handler 需要小心的地方(例如应忽略旧 term 的 RPC)方面写得不错。Figure 2 对 RPC 回复处理的说明并不总是显式。Leader 在处理回复时必须小心:必须检查自发送 RPC 以来 term 是否已变,并必须考虑对同一 follower 的并发 RPC 的回复可能已改变 leader 的状态(例如 nextIndex)。
|