美文网首页
etcd 代码及部分问题分析 (草稿)

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

作者: 酱油王0901 | 来源:发表于2021-12-09 11:56 被阅读0次

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

参考链接

相关文章

网友评论

      本文标题:etcd 代码及部分问题分析 (草稿)

      本文链接:https://www.haomeiwen.com/subject/hylffrtx.html