etcd 代码及部分问题分析 (草稿)

etcd 代码(3.6.0-pre)分析可以参考:etcd 注解版 ,也欢迎大家交流讨论

Raft message type

Raft集群中节点之间的通信都是通过传递不同的Message来完成的,Message类型 有很多种,下面介绍部分:

  • MsgHup

用于选举,对于follower或者candidate,其tick函数对应tickElection。如果follower或者candidate在选举超时时间都没有收到心跳信息,它将发送MsgHup到Step方法,其角色将变为candidate(对于follower节点)并发起新一轮选举。

  • MsgBeat

MsgApp是一个内部使用的类型,对于leader节点,其tick函数对应tickHeartbeat,周期性地向follower节点发送MsgHeartbeat消息。

  • MsgProp

MsgProp提议附加数据到log entries,而且它会重定向propose到leader节点。因此,send方法用hardState覆写了Message的term号。-

  • 当MsgProp传递到leader的step方法,leader首先会调用appendEntry方法将entries附加到它的log,接着会调用bcastAppend方法发送entries到peers。
  • 当MsgProp传递到candidate,则直接丢弃。
  • 当传递到follower,MsgProp存储到follower的mailbox,即msgs,它保存了发送者的ID,随后通过rafthttp包转发到leader。
  • MsgApp

复制log entries。leader调用bcastAppend方法,其会调用bcastAppend方法发送即将要复制的MsgApp类型的logs。当MsgApp被传递到candidate的Step方法,candidate将会revert back to follower,表示存在有效的leader在发送MsgApp消息。candidate和follower会回复MsgAppResp消息。

  • MsgAppResp

用于响应log复制请求。当MsgApp发送到candidate和follower的Step方法,它们会调用handleAppendEntries方法,其会发送MsgAppResp到raft mailbox。

  • MsgVote

请求投票选举。当MsgHup传递到follower或candidate的Step方法,将会调用campaign方法去竞选自己成为leader, 一旦campaign方法被调用,节点将会变成candidate,然后发送MsgVote到peers去请求投票。

  • 当消息传递到leader或candidate的Step方法,如果消息的term小于leader或candidate的term,则消息会被拒绝掉(MsgVoteResp将设置Reject为true),如果leader或candidate接收到term更大的MsgVote,他们将会revert back to follower。
  • 当'MsgVote'被传递给follower时,只有当sender的last term大于MsgVote的term或sender的last term等于MsgVote的term但sender的最后提交索引大于或等于follower的时,它才会投票给sender。
  • MsgVoteResp

投票选举相应消息。当candidate收到MsgVoteResp,它会统计自己的票数,如果票数超过quorum,它将会变成leader,然后调用bcastAppend,如果candidate收到大多数的拒绝投票,则revert back to follower。

  • MsgPreVote和MsgPreVoteResp

两阶段选举协议,是一个可选配置项。当Config.PreVote设置为true时,预选举过程与常规选举过程类似,只是不会增加term,除非它在第一个阶段竞选赢得了大多数票。加入这个选项可以将节点分区导致的影响降至最低。

  • MsgSnap

请求应用快照消息。当一个节点刚变为leader,或者leader接收到MsgProp消息,然后调用bcastAppend方法,这个方法会调用sendAppend方法到每个follower节点。在sendAppend方法中,如果leader获取term或者entries失败,则leader将会发送MsgSnap类型消息来请求快照。

  • MsgSnapStatus

告知快照消息应用的结果。当follower节点拒绝MsgSnap消息,表明因为网络问题导致网络层不能正常发送snapshot到follower,从而导致快照请求失败,leader将follower的progress设置为probe。当MsgSnap没有被拒绝,表明快照被正常接收,leader将follower的progress设置为probe,同时开始log复制。

  • MsgHeartbeat

leader发送心跳信息。

  • 当MsgHeartbeat消息发送到candidate,且消息的term大于candidate的term,则candidate将revert back to follower,同时会更新自己的提交索引号,然后发送消息到mailbox。
  • 当MsgHeartbeat消息发送到follower的Step方法,如果消息的term大于follower的term,则follower会更新leaderID
  • MsgHeartbeatResp

心跳响应消息。MsgHeartbeatResp传递到leader的Step方法,leader会知道哪些节点做了回复响应。只有当leader最后提交索引号大于follower的Match索引号时leader调用sendAppend方法。

  • MsgUnreachable

告知消息或请求不能被deliver,当MsgUnreachable传递到leader的Step方法,leader发现发送MsgUnreachable消息的follower节点不可达,这通常意味着MsgApp丢失了。如果此时follower的progress状态为replicate,则leader会将其重新设置回probe状态。

  • MsgCheckQuorum

如果开启CheckQuorum,则选举超时后会发送MsgCheckQuorum消息,leader节点判断其是否满足quorum,如果不满足则step down to follower节点。

  • MsgReadIndex
  • MsgReadIndexResp
  • MsgTransferLeader 和 MsgTimeoutNow

对于MsgTransferLeader消息,follower节点会将其转发到leader节点,leader节点在收到MsgTransferLeader消息后会首先记录lead被转移者,然后判断转移目标的日志是否跟上了。

  • 如果跟上了会向被转移者发送 MsgTimeoutNow 消息, 被转移者收到消息后会强制发起新一轮选举。
  • 如果没有跟上则先进行日志同步,等leader收到同步日志的MsgAppResp消息后会判断其是否已跟上,步骤同上。

Local message

  • MsgHup
  • MsgBeat
  • MsgUnreachable
  • MsgSnapStatus
  • MsgCheckQuorum

Response message

  • MsgAppResp
  • MsgVoteResp
  • MsgHeartbeatResp
  • MsgUnreachable
  • MsgPreVoteResp

Q & A

  1. 发送如下类型的消息时需要设置term,其他类型的消息都不需要设置term。
  • pb.MsgVote
  • pb.MsgVoteResp
  • pb.MsgPreVote
  • pb.MsgPreVoteResp

但是对于消息类型既不是MsgProp,又不是MsgReadIndex类型的,会为其加上raft.Term,具体实现参考raft/raft.go中的 send()方法。

  1. raft的tracker.ProgressTracker在什么地方赋值的?

初始化集群时,如果没有WAL,同时为新集群,同时指定了--initial-cluster参数,会将其解析为ServerConfig的InitialPeerURLsMap参数,然后初始化RaftCluster,并添加members。接着bootstrapRaftFromCluster方法会根据cluster的member ids生成peers。
Peer的信息如下:

type Peer struct {
    ID      uint64
    Context []byte
}

type Member struct {
    ID types.ID `json:"id"`                                                                         
    RaftAttributes
    Attributes
}

peer中的ContextMember序列化后的信息,其中包括 ID, RaftAttributes, Attributes信息。接着raft/node.go会调用StartNode方法,里面的Bootstrap方法会根据peers生成相应的pb.ConfChange entries,然后调用applyConfChange方法,这里会更新raft.prs,返回最新的tracker.Configtracker.ProgressMap信息。

(dlv) p cfg.Voters
go.etcd.io/etcd/raft/v3/quorum.JointConfig [
    [
        9372538179322589801: {}, 
        10501334649042878790: {}, 
        18249187646912138824: {}, 
    ],
    nil,
]
(dlv) p prs
go.etcd.io/etcd/raft/v3/tracker.ProgressMap [
    9372538179322589801: *{
        Match: 0,
        Next: 3,
        State: StateProbe (0),
        PendingSnapshot: 0,
        RecentActive: true,
        ProbeSent: false,
        Inflights: *(*"go.etcd.io/etcd/raft/v3/tracker.Inflights")(0xc00012f470),
        IsLearner: false,}, 
    10501334649042878790: *{
        Match: 0,
        Next: 3,
        State: StateProbe (0),
        PendingSnapshot: 0,
        RecentActive: true,
        ProbeSent: false,
        Inflights: *(*"go.etcd.io/etcd/raft/v3/tracker.Inflights")(0xc00012f620),
        IsLearner: false,}, 
    18249187646912138824: *{
        Match: 0,
        Next: 3,
        State: StateProbe (0),
        PendingSnapshot: 0,
        RecentActive: true,
        ProbeSent: false,
        Inflights: *(*"go.etcd.io/etcd/raft/v3/tracker.Inflights")(0xc00012f560),
        IsLearner: false,}, 
]
  1. apply的时候什么条件触发快照? unstable中的快照是什么时候赋值的?什么条件触发?

TODO

  1. 为什么raft.msgs读写时不需要加锁?

其实etcd里很多地方都是采用的单线程模式,比如apply也是。

  1. 对于 raftpb.Message中的MsgApp类型,其LogTermIndex, Commit, Entries的含义?

LogTerm通常用于append raft logs到follower节点。

例如:对于消息(type=MsgApp,index=100,logTerm=5),表示leader从index=101开始 append entries,而index=100对应的Term值为5。
对于消息(type=MsgAppResp,reject=true,index=100,logTerm=5),表示follower拒绝了leader的entries(可能是部分),由于follower节点已经包含index=100,term=5的entry。

CommitraftLogcommitted index。

IndexLogTerm字段是用于日志匹配的日志(即发送的日志的上一条日志)的index与term(用于日志匹配的term字段为LogTerm,消息的Term字段为该节点当前的term,部分消息需要自己指定,部分消息由send方法填充)。Entries字段保存了需要复制的日志条目。Commit字段为leader提交的最后一条日志的索引。

  1. 如何提高读性能,同时避免网络分区后重新选举出新leader出现的stale read

TODO

  1. CheckQuorum默认自动开启,同时开启Check Quorum会自动开启Leader Lease

  2. raft.maybeSendAppend在发送 Message 时,会对 Message 中的entries大小做限制,maxSizePerMsg为1MB,因此entries在超过此大小时会如何处理? raftLog.maybeAppend如何更新commit?

如果要发送的entries超过大小限制,则会发送多次

  1. pb.MsgSnapStatus消息以及ReportSnapshot的作用?

TODO

  1. candidate节点在赢得选举之后会append一条空的日志条目,其作用是什么?

candidate在当选leader后会在当前term为自己的日志追加一条空日志条目,并广播,以提交之前term的日志,具体可参考raft/raft.go中的handleAppendEntries()

  1. 日志复制不匹配时的回退优化算法
  1. 对于 follower 节点,无论是处理 MsgApp 消息还是处理 MsgSnap 消息,返回的消息都是 MsgAppResp

  2. 在通过 Transport 发送 Messages 时,会忽略 Message.To == 0 的消息。

  3. 由于etcd的模块化设计,raft模块和存储网络模块是分开的,因此send方法只是将消息放入mailbox,而不是立刻将其发出(etcd/raft也没有通信模块), 其与外界的交互都是通过Ready来进行处理的。因此,当follower收到MsgApp请求时,执行的操作实际上是(不考虑特殊情况):

  • 将新日志追加到unstable中。
  • 将包含unstablelast indexMsgAppResp消息放入信箱,等待发送。

对于Ready的处理,角色不同处理的次序也是有区别的:

  • 对于 follower 节点,是先将 entrieshardStatesnapshot 保存到稳定的存储后再发送 Messages
  • 对于 leader 节点,可以在发送 Messages 的同时将 entrieshardStatesnapshot持久化。
  1. raft中的异常处理,例如 添加成员时leader正在进行leadTransfer,如果收到MsgProp的消息,这时会返回 ErrProposalDropped 如何处理?

TODO

  1. applyEntryNormal时有V2和V3请求,分别对应pb.Requestpb.InternalRaftRequest,其log对应:
167 {"level":"debug","ts":"2021-12-08T10:49:48.188+0800","caller":"etcdserver/server.go:1835","msg":"Applying entry","index":8,"term":2,"type":"EntryNormal"}
168 {"level":"debug","ts":"2021-12-08T10:49:48.189+0800","caller":"etcdserver/server.go:1885","msg":"apply entry normal","consistent-index":7,"entry-index":8,"should-applyV3":true}
169 {"level":"debug","ts":"2021-12-08T10:49:48.189+0800","caller":"etcdserver/server.go:1908","msg":"applyEntryNormal","V2request":"ID:16732981032079369986 Method:\"PUT\" Path:\"/0/members/45d559f8148de837/attributes\" Val:\"{\\\"name\\\":\\\"infra4\\\",\\\"clientURLs\\\":[\\\"http://127.0.0.1:42379\\\"]}\" "}
206 {"level":"debug","ts":"2021-12-08T10:49:48.206+0800","caller":"etcdserver/server.go:1835","msg":"Applying entry","index":12,"term":2,"type":"EntryNormal"}
207 {"level":"debug","ts":"2021-12-08T10:49:48.206+0800","caller":"etcdserver/server.go:1885","msg":"apply entry normal","consistent-index":11,"entry-index":12,"should-applyV3":true}
208 {"level":"debug","ts":"2021-12-08T10:49:48.206+0800","caller":"etcdserver/server.go:1912","msg":"applyEntryNormal","raftReq":"header:<ID:13926956989250840580 > cluster_version_set:<ver:\"3.6.0\" > "}

261 {"level":"debug","ts":"2021-12-08T10:52:35.450+0800","caller":"etcdserver/server.go:1835","msg":"Applying entry","index":14,"term":2,"type":"EntryNormal"}
262 {"level":"debug","ts":"2021-12-08T10:52:35.450+0800","caller":"etcdserver/server.go:1885","msg":"apply entry normal","consistent-index":13,"entry-index":14,"should-applyV3":true}
263 {"level":"debug","ts":"2021-12-08T10:52:35.450+0800","caller":"etcdserver/server.go:1912","msg":"applyEntryNormal","raftReq":"header:<ID:3632572666012018439 > put:<key:\"\\000\\000\\000\\000\\000\\000\\000\\000\" value:\"\\026T\\316d\\230x\\326x\" > "}
  1. 新加入的节点或者落后很多的节点,leader 会尝试发送快照数据给follower节点,maybeSendAppend方法在处理时会生成快照消息,如下所示:
snapshot, _ := r.raftLog.snapshot()
pb.Message{
    To: to,
    Type: pb.MsgSnap,
    Snapshot: snapshot, // 看起来有点多余
}

应用逻辑层通过Ready获取messages之后会将快照消息单独处理(将其发送到msgSnapC),applyAll 在收到快照消息后会调用 createMergedSnapshotMessage 生成合并的snap.Message消息后将其发送到peer端。而 createMergedSnapshotMessage 方法会根据当前 etcd progressappliedtappliedi重新生成新的 MetadataData (v2 store序列化后的数据),所以上面raft层在生成 MsgSnap 消息时的 Snapshot 是多余的,虽然不影响。

  1. serializable read requestlinearizable read request ?

Linearizability is one of the strongest single-object consistency models, and implies that every operation appears to take place atomically, in some order, consistent with the real-time ordering of those operations: e.g., if operation A completes before operation B begins, then B should logically take effect after A.

etcd 中默认是linearizable read,如果需要客户端serializable read,可以通过WithSerializable()进行设置,Serializable 请求适用于低延迟。

Range为例,

if !r.Serializable {
    err = s.linearizableReadNotify(ctx)
    trace.Step("agreement among raft nodes before linearized reading")
    if err != nil {
        return nil, err
    }
}

ReadOnlyOption 包含两种类型:

  • ReadOnlySafe 通过与quorum个节点进行通信来保证只读请求的线性化,默认选项。
  • ReadOnlyLeaseBased 依赖领导者租约(leader lease)来保证只读请求的线性化,它会受clock drift的影响。

如果ReadOnlyOptionReadOnlyLeaseBased 的时候必须开启CheckQuorum

参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容