【存储】etcd(4)-raft
创始人
2025-05-30 00:09:45
0

在前面几篇中,我们介绍了etcd存储相关的内容,包括预写日志、mvcc、事务等,可以认为对etcd单节点的存储有了相对全面的认识。但是etcd是一个基于raft协议实现的cp模型的分布式存储,只了解其状态机的工作原理是不够的。本文我们就来介绍etcd中的raft模块的具体实现。

关于raft协议本身,这里不做介绍,建议直接阅读原论文,这里给出中文翻译版。

最小实现原则

在介绍具体实现之前,我们先介绍一些软件设计上的内容。
etcd raft模块是基于开源的golang raft sdk实现。

该raft sdk基于最小实现原则,只实现了基本的功能,包括leader选举、日志处理、状态变更等逻辑,而raft运行所需要的存储层和传输层则依赖使用方自行实现。

其中存储层定义了storage接口用来管理raft log,同时提供了基本的实现raft.MemoryStorage,该实现是基于内存数组实现的非持久化的存储,在etcd系列的第一篇中提到过。用户也可以自行实现该接口,并作为参数传入。

raft节点间通信则完全依赖使用方实现,raft sdk没有做任何约束。该raft sdk仅通过channel对外输出要通信的消息,并对外提供方法来处理收到的消息。

该实现方式非常对我的胃口。我在工作中提供一些sdk给别的服务使用时,通常都会遵循最小实现原则。sdk中只实现基本的功能逻辑,sdk依赖的其他能力定义好接口,通过参数或者其他的方式进行注入。业务方在使用时,如果某项能力其本身已经具备,则只需要简单适配接口即可;如果不具备,则可以选择我提供相应实现。

相比于大而全的sdk实现方式,遵循最小实现原则的sdk实现方式可能会增加一些理解成本,但是不会引入冗余的依赖。同时,通过不同sdk的组合也可以更加灵活地对外提供丰富能力。

当然凡事不可一概而论,到底哪种方式更好还要看具体的场景。

说完设计原则,接下来会介绍具体的实现。raft sdk中按照分层的方式进行了实现,从底层到高层分别为raft -> rawNode -> node,我们会从底层开始依次介绍。

raft

raft对象是raft sdk的核心实现。其维护了raft节点的所有状态及参数,包括term、index、raft log、vote、peers state(leader对其他节点状态的追踪)、heartbeat、election timeout等raft必要的状态以及其他具体实现中的性能优化相关参数。同时,raft对象也实现了包括状态转换、日志追加、消息处理及发送等所有的raft节点所需要的方法。

raft的属性如下,我们挑选其中几个进行说明。

type raft struct {id uint64Term uint64Vote uint64readStates []ReadState// the lograftLog *raftLogmaxMsgSize         uint64maxUncommittedSize uint64// TODO(tbg): rename to trk.prs tracker.ProgressTrackerstate StateType// isLearner is true if the local raft node is a learner.isLearner boolmsgs []pb.Message// the leader idlead uint64// leadTransferee is id of the leader transfer target when its value is not zero.// Follow the procedure defined in raft thesis 3.10.leadTransferee uint64// Only one conf change may be pending (in the log, but not yet// applied) at a time. This is enforced via pendingConfIndex, which// is set to a value >= the log index of the latest pending// configuration change (if any). Config changes are only allowed to// be proposed if the leader's applied index is greater than this// value.pendingConfIndex uint64// an estimate of the size of the uncommitted tail of the Raft log. Used to// prevent unbounded log growth. Only maintained by the leader. Reset on// term changes.uncommittedSize uint64readOnly *readOnly// number of ticks since it reached last electionTimeout when it is leader// or candidate.// number of ticks since it reached last electionTimeout or received a// valid message from current leader when it is a follower.electionElapsed int// number of ticks since it reached last heartbeatTimeout.// only leader keeps heartbeatElapsed.heartbeatElapsed intcheckQuorum boolpreVote     boolheartbeatTimeout intelectionTimeout  int// randomizedElectionTimeout is a random number between// [electiontimeout, 2 * electiontimeout - 1]. It gets reset// when raft changes its state to follower or candidate.randomizedElectionTimeout intdisableProposalForwarding booltick func()step stepFunclogger Logger// pendingReadIndexMessages is used to store messages of type MsgReadIndex// that can't be answered as new leader didn't committed any log in// current term. Those will be handled as fast as first log is committed in// current term.pendingReadIndexMessages []pb.Message
}
  • raftlog
    raftlog是用来存储日志的部分,其构造如下。日志被追加到raft模块中时首先被会被添加的unstable中,当etcd将unstable中的日志追加至wal中以后,raft会将对应的日志追加到storage中,并从unstable中清除。storage就是第一小节中介绍的raft sdk定义的存储层接口,这里采用了raft.MemoryStorage的实现。
type raftLog struct {// storage contains all stable entries since the last snapshot.storage Storage// unstable contains all unstable entries and snapshot.// they will be saved into storage.unstable unstable// committed is the highest log position that is known to be in// stable storage on a quorum of nodes.committed uint64// applied is the highest log position that the application has// been instructed to apply to its state machine.// Invariant: applied <= committedapplied uint64logger Logger// maxNextCommittedEntsSize is the maximum number aggregate byte size of the// messages returned from calls to nextCommittedEnts.maxNextCommittedEntsSize uint64
}type unstable struct {// the incoming unstable snapshot, if any.snapshot *pb.Snapshot// all entries that have not yet been written to storage.entries []pb.Entryoffset  uint64logger Logger
}
  • maxMsgSize
    批量处理是非常常见的优化手段。raft在日志同步时就采用了批量处理的方式,一条消息携带多条日志。同时为了防止消息过大,设置了maxMsgSize参数。
  • prs
    raft中使用tracker.ProgressTracker来记录follower的状态,包括next index、commited index、active等。单独拆了一个小模块出来。
  • msgs
    raft需要发送的消息,会追加至msgs保存,算是某种程度的异步发送。当上层调用模块空闲时,会主动获取msgs然后进行发送。前面讲了,日志的同步是批量处理。这里msg的处理是异步批量处理。异步批量处理是常见的很有效的性能优化的手段。
  • prevote
    prevote也是实现中的一个优化。
    在raft算法中,follower在变为candidate时,会立刻将自身的term加一并发起选举。如果选举失败则进入election timeout然后重复该过程。正常情况下可以保证一轮选举一定会选出leader。但是在异常情况会存在问题。比如网络分区的情况下,某些节点的term会一直增长。当网络通信恢复时,其term会比leader大,这会导致leadership转移。
    针对上述问题,raft提供了prevote参数。当prevote为true时,选举时并不会直接将term加一,而是先发起prevote。当能拿到大多数选票时再将term加一并发起真正选举。
  • tick和step
    raft节点状态的驱动主要有两个地方,或者说两个方法。这里状态要和raft算法中的状态机区分,是指raft节点本身的状态,包括日志、任期、索引、节点的交互等。刚说了,raft节点的状态驱动有两个地方:一个是本身的计时,是节点自身的状态驱动,随着计时节点会根据角色不同有发送心跳、角色变更、开启选举等不同的行为;另一个是对外暴露的接口,以响应使用方的请求,同样的,针对同一种请求,不同角色的raft节点行为并不相同。抽象出来就是tick和step方法。
    针对类似上面描述的不同角色下行为不同的情况,通常会将接口抽象出来,针对不同的角色或者状态分别实现,在角色变更时设置相应的行为。这也是常见的设计思路。

介绍完属性,接下来再介绍相关的方法。对于方法,同样不会进行非常细节的介绍。因为相关方法里涉及到大量raft算法的逻辑实现,建议还是去看raft算法。我们会简单介绍主要方法的功能,然后关注一些在具体实现上的优化思路。

下面是raft发送消息相关的方法,最底层是send方法。我把send方法的具体实现贴了出来。可以看到,send方法只是将消息追加到msgs列表,以此实现异步批量处理。异步批量处理是常见的优化手段,可以极大的提升系统的吞吐和性能。但是在使用异步处理时必须要有所限制,必须对等待处理的消息的批次进行限制。
在send方法基础上,封装了sendAppend方法、sendHeartbeat方法,分别对指定的节点发送日志追加消息、发送心跳,以及在sendAppend和sendHeartbeat基础上封装广播方法。

func (r *raft) send(m pb.Message) {// 省略了参数校验r.msgs = append(r.msgs, m)
}func (r *raft) sendAppend(to uint64) {}func (r *raft) maybeSendAppend(to uint64, sendIfEmpty bool) bool {}func (r *raft) sendHeartbeat(to uint64, ctx []byte) {}func (r *raft) bcastAppend() {}func (r *raft) bcastHeartbeat() {}func (r *raft) bcastHeartbeatWithCtx(ctx []byte) {}

下面是状态变化相关的方法。状态变化的方法比较简单,这里不做展开。只是在具体实现时增加了prevote的状态,这个在前面已经提到过。

func (r *raft) becomeFollower(term uint64, lead uint64) {}func (r *raft) becomeCandidate() {}func (r *raft) becomePreCandidate() {}func (r *raft) becomeLeader() {}func (r *raft) hup(t CampaignType) {}func (r *raft) campaign(t CampaignType) {}

下面是状态驱动的方法。前面也提到,raft节点的状态分别受自身的时钟驱动以及外界请求驱动。

时钟驱动来说,leader会在时钟驱动下发送心跳以及检查qurom;follower及candidate则在时钟驱动下进行状态转换并发起选举。同样,raft也实现了不同角色响应外界请求的方法。

// tickElection is run by followers and candidates after r.electionTimeout.
func (r *raft) tickElection() {}// tickHeartbeat is run by leaders to send a MsgBeat after r.heartbeatTimeout.
func (r *raft) tickHeartbeat() {}func (r *raft) Step(m pb.Message) error {}func stepLeader(r *raft, m pb.Message) erro {}func stepCandidate(r *raft, m pb.Message) error {}func stepFollower(r *raft, m pb.Message) error {}

RawNode

RawNode是在raft基础上的封装,其中最主要的一点我认为就是ready的封装。

// RawNode is a thread-unsafe Node.
// The methods of this struct correspond to the methods of Node and are described
// more fully there.
type RawNode struct {raft       *raftprevSoftSt *SoftStateprevHardSt pb.HardState
}

ready和advance是raft节点和状态机的交互机制。前面多次提到,raft的实现采用了异步批量处理。状态机会主动调用ready方法,获取等待处理的数据,并在处理完成后调用advance方法通知raft节点相应内容已经处理完成。
先看下ready中都包含哪些数据。ready中包含了的数据有:

  • unstable的日志条目,在etcd将其写入wal后,raft才会认为相应的日志为stable;
  • 已经commit但是尚未apply的日志,apply后raft节点会更新applied状态;
  • 待发送的msgs;
  • softstate和hardstate,分别包括raft节点的状态、以及term、index、vote;
func newReady(r *raft, prevSoftSt *SoftState, prevHardSt pb.HardState) Ready {rd := Ready{Entries:          r.raftLog.unstableEntries(),CommittedEntries: r.raftLog.nextCommittedEnts(),Messages:         r.msgs,}if softSt := r.softState(); !softSt.equal(prevSoftSt) {rd.SoftState = softSt}if hardSt := r.hardState(); !isHardStateEqual(hardSt, prevHardSt) {rd.HardState = hardSt}if r.raftLog.unstable.snapshot != nil {rd.Snapshot = *r.raftLog.unstable.snapshot}if len(r.readStates) != 0 {rd.ReadStates = r.readStates}rd.MustSync = MustSync(r.hardState(), prevHardSt, len(rd.Entries))return rd
}

状态机在相应处理后会调用advance通知raft节点。

func (r *raft) advance(rd Ready) {r.reduceUncommittedSize(rd.CommittedEntries)if newApplied := rd.appliedCursor(); newApplied > 0 {r.raftLog.appliedTo(newApplied)if r.prs.Config.AutoLeave && newApplied >= r.pendingConfIndex && r.state == StateLeader {m, err := confChangeToMsg(nil)if err != nil {panic(err)}if err := r.Step(m); err != nil {r.logger.Debugf("not initiating automatic transition out of joint configuration %s: %v", r.prs.Config, err)} else {r.logger.Infof("initiating automatic transition out of joint configuration %s", r.prs.Config)}}}if len(rd.Entries) > 0 {e := rd.Entries[len(rd.Entries)-1]if r.id == r.lead {_ = r.Step(pb.Message{From: r.id, Type: pb.MsgAppResp, Index: e.Index})}r.raftLog.stableTo(e.Index, e.Term)}if !IsEmptySnap(rd.Snapshot) {r.raftLog.stableSnapTo(rd.Snapshot.Metadata.Index)}
}

Node

node仅是在rawNode上封装了一些chan用来做交互,不做介绍。

// node is the canonical implementation of the Node interface
type node struct {propc      chan msgWithResultrecvc      chan pb.Messageconfc      chan pb.ConfChangeV2confstatec chan pb.ConfStatereadyc     chan Readyadvancec   chan struct{}tickc      chan struct{}done       chan struct{}stop       chan struct{}status     chan chan Statusrn *RawNode
}

以上即是对raft部分的介绍,主要侧重在raft sdk的代码设计以及性能优化方面。一些技术细节以及连接层等没有提及,后面会再开一篇补充说明。

相关内容

热门资讯

谁的安卓系统好,谁家的安卓系统... 说到安卓系统,这可是个热门话题呢!你有没有想过,这么多安卓手机品牌,哪个的操作系统最让你心动?今天,...
安卓系统信付通,安全无忧的移动... 你知道吗?在安卓手机的世界里,有一个超级好用的支付工具,它就是信付通。今天,就让我带你来全方位了解一...
小米官方系统安卓包,深度解析与... 亲爱的数码爱好者们,你是否曾为手机系统而烦恼?市面上那么多手机品牌,各种操作系统让人眼花缭乱。今天,...
自制安卓手机双系统,自制安卓手... 你有没有想过,自己的手机可以同时运行两个操作系统呢?没错,就是那种安卓手机双系统!听起来是不是很酷?...
小米安卓系统怎么设置,科技前沿... 小米手机的用户们,是不是觉得安卓系统有点复杂,设置起来有点头疼呢?别担心,今天就来手把手教你如何轻松...
点歌系统支持安卓系统么,安卓用... 你有没有想过,在手机上点歌听歌,是不是也能像在KTV里那样随心所欲呢?现在,就让我来告诉你一个超级酷...
原版安卓系统刷机,解锁无限可能 你有没有想过,你的安卓手机其实可以焕然一新?没错,就是那种原汁原味的安卓系统,让你的手机重新找回当初...
欧尚改装安卓系统,打造智能驾驶... 你有没有想过,你的欧尚汽车其实也可以变身成为智能座驾呢?没错,就是那个你每天上下班的伙伴——欧尚,现...
安卓系统最新事件,揭秘最新重大... 你知道吗?最近安卓系统可是发生了一件超级大事件,简直让人兴奋得心跳加速!这不,我就迫不及待地来和你分...
早期电话手表安卓系统,安卓系统... 你有没有想过,小时候那些看似简单的玩具,现在竟然也能玩出花来?比如,早期的电话手表,那时候的功能可真...
安卓老系统手机游戏,安卓老系统... 你有没有发现,那些安卓老系统手机,虽然看起来有点古老,但它们在游戏界可是有着自己独特的魅力呢!想象那...
安卓系统重启还是开关,重启与开... 手机突然卡壳了,是不是又该给安卓系统来个重启大法了?别急,今天就来聊聊这个让人又爱又恨的“安卓系统重...
安卓系统刷入iso,轻松实现个... 你有没有想过,你的安卓手机其实可以像变形金刚一样,换上全新的“皮肤”?没错,就是刷入ISO系统!这可...
安卓机系统无法关机,探究原因与... 最近我的安卓手机怎么啦?总是关机不成功,真是让人头疼啊!这可怎么办呢?别急,让我来帮你分析找出解决这...
安卓什么系统广告最多,揭秘最新... 你有没有发现,每次打开安卓手机,广告就像无处不在的小精灵,跳来跳去,让人眼花缭乱?今天,就让我带你一...
禁止中国使用安卓系统,“安卓系... 你知道吗?最近互联网上掀起了一股热议,那就是关于中国是否应该禁止使用安卓系统的话题。这可不是闹着玩的...
如何分辨ios系统和安卓系统,... 你有没有想过,你的手机里装的是iOS系统还是安卓系统呢?这两种系统各有千秋,但分辨它们其实并不难。今...
如何查询安卓系统版本,安卓系统... 你有没有想过,你的安卓手机里隐藏着一个小秘密——那就是它的系统版本!知道这个秘密,不仅能让你更好地了...
lg电视系统和安卓系统比较,性... 你有没有发现,现在家里的电视已经不再是那个傻乎乎的“大盒子”了?它变得聪明起来,能和你互动,能上网,...
安卓系统增加主页按钮,Andr... 你知道吗?最近安卓系统又来了一次大更新,其中最引人注目的变化之一就是主页按钮的全新设计。这可不是一个...