总体设计
etcd/raft 将Raft算法的实现分成了三个模块:
- Raft状态机:Raft状态机完全由etcd/raft负责,
raft
结构体即为其实现。使用etcd/raft的开发者不能直接操作raft结构体,只能通过etcd/raft提供的Node
接口对其进行操作。 - 存储模块:存储模块可以划分为两部分,即对存储的读取与写入。etcd/raft只需要读取存储,etcd/raft依赖的
Storage
接口中只有读取存储的方法。而对存储的写入由用户负责,etcd/raft并不关心开发者如何写入存储,对存储的写入方法可以由开发者自己定义。etcd使用的存储模块是在与Storage
接口同一文件下的MemoryStorage
结构体。MemoryStorage
既实现了Storage
接口需要的读取存储的方法,也为用户提供了写入存储的方法。
Info
Storage
接口定义的是稳定存储的读取方法。之所以etcd使用了基于内存的MemoryStorage
,是因为etcd在写入MemoryStorage
前,需要先写入预写日志(Write Ahead Log,WAL)或快照。而预写日志和快照是保存在稳定存储中的。这样,在每次重启时,etcd可以基于保存在稳定存储中的快照和预写日志恢复MemoryStorage
的状态。也就是说,etcd的稳定存储是通过快照、预写日志、MemoryStorage
三者共同实现的。
- 通信模块是完全由使用etcd/raft的开发者负责的。etcd/raft不关心开发者如何实现通信模块。
开发者仅有的操作etcd/raft的方式——Node
及其相关数据结构实现
// Node represents a node in a raft cluster.
type Node interface {
// Tick increments the internal logical clock for the Node by a single tick. Election
// timeouts and heartbeat timeouts are in units of ticks.
Tick()
// Campaign causes the Node to transition to candidate state and start campaigning to become leader.
Campaign(ctx context.Context) error
// Propose proposes that data be appended to the log. Note that proposals can be lost without
// notice, therefore it is user's job to ensure proposal retries.
// proposals可能会没有任何报错的丢失,因此保证proposal的重传是开发者的职责
Propose(ctx context.Context, data []byte) error
// ProposeConfChange proposes a configuration change. Like any proposal, the
// configuration change may be dropped with or without an error being
// returned. In particular, configuration changes are dropped unless the
// leader has certainty that there is no prior unapplied configuration
// change in its log.
// 如果leader不能明确的知道在它的log中没有先前的未提交的configuration change,那么后续的configuration changes会被丢弃
//
// The method accepts either a pb.ConfChange (deprecated) or pb.ConfChangeV2
// message. The latter allows arbitrary configuration changes via joint
// consensus, notably including replacing a voter. Passing a ConfChangeV2
// message is only allowed if all Nodes participating in the cluster run a
// version of this library aware of the V2 API. See pb.ConfChangeV2 for
// usage details and semantics.
ProposeConfChange(ctx context.Context, cc pb.ConfChangeI) error
// Step advances the state machine using the given message. ctx.Err() will be returned, if any.
Step(ctx context.Context, msg pb.Message) error
// Ready returns a channel that returns the current point-in-time state.
// Users of the Node must call Advance after retrieving the state returned by Ready.
//
// NOTE: No committed entries from the next Ready may be applied until all committed entries
// and snapshots from the previous one have finished.
Ready() <-chan Ready
// Advance notifies the Node that the application has saved progress up to the last Ready.
// It prepares the node to return the next available Ready.
//
// The application should generally call Advance after it applies the entries in last Ready.
//
// However, as an optimization, the application may call Advance while it is applying the
// commands. For example. when the last Ready contains a snapshot, the application might take
// a long time to apply the snapshot data. To continue receiving Ready without blocking raft
// progress, it can call Advance before finishing applying the last ready.
Advance()
// ApplyConfChange applies a config change (previously passed to
// ProposeConfChange) to the node. This must be called whenever a config
// change is observed in Ready.CommittedEntries, except when the app decides
// to reject the configuration change (i.e. treats it as a noop instead), in
// which case it must not be called.
//
// Returns an opaque non-nil ConfState protobuf which must be recorded in
// snapshots.
ApplyConfChange(cc pb.ConfChangeI) *pb.ConfState
// TransferLeadership attempts to transfer leadership to the given transferee.
TransferLeadership(ctx context.Context, lead, transferee uint64)
// ReadIndex request a read state. The read state will be set in the ready.
// Read state has a read index. Once the application advances further than the read
// index, any linearizable read requests issued before the read request can be
// processed safely. The read state will have the same rctx attached.
// Note that request can be lost without notice, therefore it is user's job
// to ensure read index retries.
ReadIndex(ctx context.Context, rctx []byte) error
// Status returns the current status of the raft state machine.
Status() Status
// ReportUnreachable reports the given node is not reachable for the last send.
ReportUnreachable(id uint64)
// ReportSnapshot reports the status of the sent snapshot. The id is the raft ID of the follower
// who is meant to receive the snapshot, and the status is SnapshotFinish or SnapshotFailure.
// Calling ReportSnapshot with SnapshotFinish is a no-op. But, any failure in applying a
// snapshot (for e.g., while streaming it from leader to follower), should be reported to the
// leader with SnapshotFailure. When leader sends a snapshot to a follower, it pauses any raft
// log probes until the follower can apply the snapshot and advance its state. If the follower
// can't do that, for e.g., due to a crash, it could end up in a limbo, never getting any
// updates from the leader. Therefore, it is crucial that the application ensures that any
// failure in snapshot sending is caught and reported back to the leader; so it can resume raft
// log probing in the follower.
ReportSnapshot(id uint64, status SnapshotStatus)
// Stop performs any necessary termination of the Node.
Stop()
}
Node
结构中的方法按调用时机可以分为三类:
方法 | 描述 |
---|---|
Tick |
由时钟(循环定时器)驱动,每隔一定时间调用一次,驱动raft 结构体的内部时钟运行。 |
Ready 、Advance |
这两个方法往往成对出现。准确的说,是Ready 方法返回的Ready 结构体信道的信号与Advance 方法成对出现。每当从Ready 结构体信道中收到来自raft 的消息时,用户需要按照一定顺序对Ready 结构体中的字段进行处理。在完成对Ready 的处理后,需要调用Advance 方法,通知raft 这批数据已经处理完成,可以继续传入下一批。 |
其它方法 | 需要时随时调用。 |
对于Ready
结构体,有几个重要的字段需要按照如下顺序处理:
- 将
HardState
、Entries
、Snapshot
写入稳定存储(其中,Snapshot
的写入不需要严格按照此顺序,etcd/raft为快照的传输提供了另一套机制以优化执行效率)。 - 本条中的操作可以并行执行:
- 将
Messages
中的消息发送给相应的节点。 - 将
Snapshot
和CommittedEntries
应用到本地状态机中。
- 将
- 调用
Advance
方法。