总体设计

etcd/raft 将Raft算法的实现分成了三个模块:

Info

Storage接口定义的是稳定存储的读取方法。之所以etcd使用了基于内存的MemoryStorage,是因为etcd在写入MemoryStorage前,需要先写入预写日志(Write Ahead Log,WAL)或快照。而预写日志和快照是保存在稳定存储中的。这样,在每次重启时,etcd可以基于保存在稳定存储中的快照和预写日志恢复MemoryStorage的状态。也就是说,etcd的稳定存储是通过快照、预写日志、MemoryStorage三者共同实现的

开发者仅有的操作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结构体的内部时钟运行。
ReadyAdvance 这两个方法往往成对出现。准确的说,是Ready方法返回的Ready结构体信道的信号与Advance方法成对出现。每当从Ready结构体信道中收到来自raft的消息时,用户需要按照一定顺序对Ready结构体中的字段进行处理。在完成对Ready的处理后,需要调用Advance方法,通知raft这批数据已经处理完成,可以继续传入下一批。
其它方法 需要时随时调用。

对于Ready结构体,有几个重要的字段需要按照如下顺序处理:

  1. HardStateEntriesSnapshot写入稳定存储(其中,Snapshot的写入不需要严格按照此顺序,etcd/raft为快照的传输提供了另一套机制以优化执行效率)。
  2. 本条中的操作可以并行执行:
    • Messages中的消息发送给相应的节点。
    • SnapshotCommittedEntries应用到本地状态机中。
  3. 调用Advance方法。