- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
该篇博客基于etcd v3.5.7版本,首先会简单介绍etcd/raft对Raft选举部分的算法优化,然后通过源码分析etcd/raft的选举实现.
该优化措施均在raft博士论文中有讲解 。
etcd/raft实现的与选举有关的优化有 Pre-Vote 、 Check Quorum 、和 Leader Lease 。在这三种优化中,只有 Pre-Vote 和 Leader Lease 最初是对选举过程的优化, Check Quorum 是为了更高效地实现线性一致性读(Linearizable Read)而做出的优化,但是由于 Leader Lease 需要依赖 Check Quorum ,因此也放在这讲.
如下图所示,当Raft集群的网络发生分区时,会出现节点数达不到quorum(达成共识至少需要的节点数)的分区,如图中的 Partition 1 .
在节点数能够达到quorum的分区中,选举流程会正常进行,该分区中的所有节点的term最终会稳定为新选举出的leader节点的term。不幸的是,在节点数无法达到quorum的分区中,如果该分区中没有leader节点,因为节点总是无法收到数量达到quorum的投票而不会选举出新的leader,所以该分区中的节点在 election timeout 超时后,会增大term并发起下一轮选举,这导致该分区中的节点的term会不断增大.
如果网络一直没有恢复,这是没有问题的。但是,如果网络分区恢复,此时,达不到quorum的分区中的节点的term值会远大于能够达到quorum的分区中的节点的term,这会导致能够达到quorum的分区的leader退位(step down)并增大自己的term到更大的term,使集群产生一轮不必要的选举.
Pre-Vote 机制就是为了解决这一问题而设计的,其解决的思路在于不允许达不到quorum的分区正常进入投票流程,也就避免了其term号的增大。为此, Pre-Vote 引入了“预投票”,也就是说,当节点 election timeout 超时时,它们不会立即增大自身的term并请求投票,而是先发起一轮预投票。收到预投票请求的节点不会退位。只有当节点收到了达到quorum的预投票响应时,节点才能增大自身term号并发起投票请求。这样,达不到quorum的分区中的节点永远无法增大term,也就不会在分区恢复后引起不必要的一轮投票.
在Raft算法中,保证线性一致性读取的最简单的方式,就是讲读请求同样当做一条Raft提议,通过与其它日志相同的方式执行,因此这种方式也叫作 Log Read 。显然, Log Read 的性能很差。而在很多系统中,读多写少的负载是很常见的场景。因此,为了提高读取的性能,就要试图绕过日志机制.
但是,直接绕过日志机制从leader读取,可能会读到陈旧的数据,也就是说存在 stale read 的问题。在下图的场景中,假设网络分区前, Node 5 是整个集群的leader。在网络发生分区后, Partition 0 分区中选举出了新leader,也就是图中的 Node 1 .
但是,由于网络分区, Node 5 无法收到 Partition 0 中节点的消息, Node 5 不会意识到集群中出现了新的leader。此时,虽然它不能成功地完成日志提交,但是如果读取时绕过了日志,它还是能够提供读取服务的。这会导致连接到 Node 5 的client读取到陈旧的数据.
Check Quorum 可以减轻这一问题带来的影响,其机制也非常简单:让leader每隔一段时间主动地检查follower是否活跃。如果活跃的follower数量达不到quorum,那么说明该leader可能是分区前的旧leader,所以此时该leader会主动退位转为follower.
需要注意的是, Check Quorum 并不能完全避免 stale read 的发生,只能减小其发生时间,降低影响。如果需要严格的线性一致性,需要通过其它机制实现.
分布式系统中的网络环境十分复杂,有时可能出现网络不完全分区的情况,即整个整个网络拓补图是一个连通图,但是可能并非任意的两个节点都能互相访问.
这种现象不止会出现在网络故障中,还会出现在成员变更中。在通过 ConfChange 移除节点时,不同节点应用该 ConfChange 的时间可能不同,这也可能导致这一现象发生——TODO (举个例子).
在上图的场景下, Node 1 与 Node 2 之间无法通信。如果它们之间的通信中断前, Node 1 是集群的leader,在通信中断后, Node 2 无法再收到来自 Node 1 的心跳。因此, Node 2 会开始选举。如果在 Node 2 发起选举前, Node 1 和 Node 3 中都没有新的日志,那么 Node 2 仍可以收到能达到quorum的投票(来自 Node 2 本身的投票和来自 Node 3 的投票),并成为leader.
Leader Lease 机制对投票引入了一条新的约束以解决这一问题:当节点在 election timeout 超时前,如果收到了leader的消息,那么它不会为其它发起投票或预投票请求的节点投票。也就是说, Leader Lease 机制会阻止了正常工作的集群中的节点给其它节点投票.
Leader Lease 需要依赖 Check Quorum 机制才能正常工作。接下来笔者通过一个例子说明其原因.
假如在一个5个节点组成的Raft集群中,出现了下图中的分区情况: Node 1 与 Node 2 互通, Node 3 、 Node 4 、 Node 5 之间两两互通、 Node 5 与任一节点不通。在网络分区前, Node 1 是集群的leader.
在既没有 Leader Lease 也没有 Check Quorum 的情况下, Node 3 、 Node 4 会因收不到leader的心跳而发起投票,因为 Node 2 、 Node 3 、 Node 4 互通,该分区节点数能达到quorum,因此它们可以选举出新的leader.
而在使用了 Leader Lease 而不使用 Check Quorum 的情况下,由于 Node 2 仍能够收到原leader Node 1 的心跳,受 Leader Lease 机制的约束,它不会为其它节点投票。这会导致即使整个集群中存在可用节点数达到quorum的分区,但是集群仍无法正常工作.
而如果同时使用了 Leader Lease 和 Check Quorum ,那么在上图的情况下, Node 1 会在 election timeout 超时后因检测不到数量达到quorum的活跃节点而退位为follower。这样, Node 2 、 Node 3 、 Node 4 之间的选举可以正常进行.
引入 Pre-Vote 和 Check Quorum (etcd/raft的实现中,开启 Check Quorum 会自动开启 Leader Lease )会为Raft算法引入一些新的问题.
当一个节点收到了term比自己低的消息时,原本的逻辑是直接忽略该消息,因为term比自己低的消息仅可能是因网络延迟的迟到的旧消息。然而,开启了这些机制后,在如下的场景中会出现问题:
场景1: 如上图所示,在开启了 Check Quorum / Leader Lease 后(假设没有开启 Pre-Vote , Pre-Vote 的问题在下一场景中讨论),数量达不到quorum的分区中的leader会退位,且该分区中的节点永远都无法选举出leader,因此该分区的节点的term会不断增大。当该分区与整个集群的网络恢复后,由于开启了 Check Quorum / Leader Lease ,即使该分区中的节点有更大的term,由于原分区的节点工作正常,它们的选举请求会被丢弃。同时,由于该节点的term比原分区的leader节点的term大,因此它会丢弃原分区的leader的请求。这样,该节点永远都无法重新加入集群,也无法当选新leader。(详见 issue #5451 、 issue #5468 ).
场景2: Pre-Vote 机制也有类似的问题。如上图所示,假如发起预投票的节点,在预投票通过后正要发起正式投票的请求时出现网络分区。此时,该节点的term会高于原集群的term。而原集群因没有收到真正的投票请求,不会更新term,继续正常运行。在网络分区恢复后,原集群的term低于分区节点的term,但是日志比分区节点更新。此时,该节点发起的预投票请求因没有日志落后会被丢弃,而原集群leader发给该节点的请求会因term比该节点小而被丢弃。同样,该节点永远都无法重新加入集群,也无法当选新leader。(详见 issue #8501 、 issue #8525 ).
场景3: 在更复杂的情况中,比如,在变更配置时,开启了原本没有开启的 Pre-Vote 机制。此时可能会出现与上一条类似的情况,即可能因term更高但是log更旧的节点的存在导致整个集群的死锁,所有节点都无法预投票成功。这种情况比上一种情况更危险,上一种情况只有之前分区的节点无法加入集群,在这种情况下,整个集群都会不可用。(详见 issue #8501 、 issue #8525 ).
为了解决以上问题,节点在收到term比自己低的请求时,需要做特殊的处理。处理逻辑也很简单:
在集群刚启动时,所有节点的状态都为 follower ,等待超时触发 leader election 。超时时间由 Config 设置。 etcd/raft 没有用真实时间而是使用逻辑时钟,当调用 tick 的次数超过指定次数时触发超时事件。 对于 follower 和 candidate 而言, tick 中会判断是否超时,若超时则会本地生成一个 MsgHup 类型的消息触发 leader election
// tickElection is run by followers and candidates after r.electionTimeout.
func (r *raft) tickElection() {
r.electionElapsed++
if r.promotable() && r.pastElectionTimeout() {
r.electionElapsed = 0
r.Step(pb.Message{From: r.id, Type: pb.MsgHup})
}
}
etcd/raft通过 raft 结构体的 Step 方法实现Raft状态机的状态转移。 Step 方法是消息处理的入口,不同 state 处理的消息不同且处理方式不同,所以有多个 step 方法
raft.Step()
: 消息处理的入口,做一些共性的检查,如 term
,或处理所有状态都需要处理的消息。若需要更进一步处理,会根据状态 调用下面的方法:
raft.stepLeader()
: leader
状态的消息处理方法; raft.stepFollower()
: follower
状态的消息处理方法; raft.stepCandidate()
: candidate
状态的消息处理方法。
func (r *raft) Step(m pb.Message) error {
// ... ...
switch m.Type {
case pb.MsgHup:
if r.preVote {
r.hup(campaignPreElection)
} else {
r.hup(campaignElection)
}
// ... ...
}
// ... ...
}
Step 方法在处理 MsgHup 消息时,会根据当前配置中是否开启了 Pre-Vote 机制,以不同的 CampaignType 调用 hup 方法。 CampaignType 是一种枚举类型(go语言的枚举实现方式),其可能值如下表所示.
值 | 描述 |
---|---|
campaignPreElection |
表示 Pre-Vote 的预选举阶段。 |
campaignElection |
表示正常的选举阶段(仅超时选举,不包括 Leader Transfer )。 |
campaignTransfer |
表示 Leader Transfer 阶段。 |
接下来对 hup 的实现进行分析.
func (r *raft) hup(t CampaignType) {
if r.state == StateLeader {
r.logger.Debugf("%x ignoring MsgHup because already leader", r.id)
return
}
if !r.promotable() {
r.logger.Warningf("%x is unpromotable and can not campaign", r.id)
return
}
ents, err := r.raftLog.slice(r.raftLog.applied+1, r.raftLog.committed+1, noLimit)
if err != nil {
r.logger.Panicf("unexpected error getting unapplied entries (%v)", err)
}
if n := numOfPendingConf(ents); n != 0 && r.raftLog.committed > r.raftLog.applied {
r.logger.Warningf("%x cannot campaign at term %d since there are still %d pending configuration changes to apply", r.id, r.Term, n)
return
}
r.logger.Infof("%x is starting a new election at term %d", r.id, r.Term)
r.campaign(t)
}
// promotable indicates whether state machine can be promoted to leader,
// which is true when its own id is in progress list.
func (r *raft) promotable() bool {
pr := r.prs.Progress[r.id]
return pr != nil && !pr.IsLearner && !r.raftLog.hasPendingSnapshot()
}
总结当节点出现以下情况时不能发起选举:
ConfChange
消息 官方注释很详细了,因此不多废笔墨解释 。
// campaign transitions the raft instance to candidate state. This must only be
// called after verifying that this is a legitimate transition.
func (r *raft) campaign(t CampaignType) {
// 因为调用campaign的方法不止有hup,campaign方法首先还是会检查promotable()是否为真。
if !r.promotable() {
// This path should not be hit (callers are supposed to check), but
// better safe than sorry.
r.logger.Warningf("%x is unpromotable; campaign() should have been called", r.id)
}
var term uint64
var voteMsg pb.MessageType
if t == campaignPreElection {
r.becomePreCandidate()
voteMsg = pb.MsgPreVote
// PreVote RPCs are sent for the next term before we've incremented r.Term.
term = r.Term + 1
} else {
r.becomeCandidate()
voteMsg = pb.MsgVote
term = r.Term
}
if _, _, res := r.poll(r.id, voteRespMsgType(voteMsg), true); res == quorum.VoteWon {
// We won the election after voting for ourselves (which must mean that
// this is a single-node cluster). Advance to the next state.
if t == campaignPreElection {
r.campaign(campaignElection)
} else {
r.becomeLeader()
}
return
}
var ids []uint64
{
//won't send requestVote to learners, beacause learners[] are not in incoming[] and outgoing[]
idMap := r.prs.Voters.IDs()
ids = make([]uint64, 0, len(idMap))
for id := range idMap {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
}
for _, id := range ids {
if id == r.id {
continue
}
r.logger.Infof("%x [logterm: %d, index: %d] sent %s request to %x at term %d",
r.id, r.raftLog.lastTerm(), r.raftLog.lastIndex(), voteMsg, id, r.Term)
var ctx []byte
if t == campaignTransfer {
ctx = []byte(t)
}
r.send(pb.Message{Term: term, To: id, Type: voteMsg, Index: r.raftLog.lastIndex(), LogTerm: r.raftLog.lastTerm(), Context: ctx})
}
}
至此,该节点已向其他节点发送MsgVote或MsgPreVote消息 。
处理vote或pre-vote消息都在 Step 方法内,不会进入各自的step方法,有效的 MsgPreVote 必须满足其中一个条件 (m.Term > r.Term) 。
官方注释很详细,简单易理解,因此不多废笔墨解释 。
func (r *raft) Step(m pb.Message) error {
// Handle the message term, which may result in our stepping down to a follower.
switch {
case m.Term == 0:
// local message
case m.Term > r.Term:
if m.Type == pb.MsgVote || m.Type == pb.MsgPreVote {
force := bytes.Equal(m.Context, []byte(campaignTransfer))
inLease := r.checkQuorum && r.lead != None && r.electionElapsed < r.electionTimeout
if !force && inLease {
// If a server receives a RequestVote request within the minimum election timeout
// of hearing from a current leader, it does not update its term or grant its vote
return nil
}
}
switch {
case m.Type == pb.MsgPreVote:
// Never change our term in response to a PreVote
case m.Type == pb.MsgPreVoteResp && !m.Reject:
// We send pre-vote requests with a term in our future. If the
// pre-vote is granted, we will increment our term when we get a
// quorum. If it is not, the term comes from the node that
// rejected our vote so we should become a follower at the new
// term.
default:
if m.Type == pb.MsgApp || m.Type == pb.MsgHeartbeat || m.Type == pb.MsgSnap {
r.becomeFollower(m.Term, m.From)
} else {
r.becomeFollower(m.Term, None)
}
}
case m.Term < r.Term:
// ........
}
switch m.Type {
case pb.MsgHup:
// ........
case pb.MsgVote, pb.MsgPreVote:
// We can vote if this is a repeat of a vote we've already cast...
canVote := r.Vote == m.From ||
// ...we haven't voted and we don't think there's a leader yet in this term...
(r.Vote == None && r.lead == None) ||
// ...or this is a PreVote for a future term...
(m.Type == pb.MsgPreVote && m.Term > r.Term)
// ...and we believe the candidate is up to date.
if canVote && r.raftLog.isUpToDate(m.Index, m.LogTerm) {
// Note: it turns out that that learners must be allowed to cast votes.
// This seems counter- intuitive but is necessary in the situation in which
// a learner has been promoted (i.e. is now a voter) but has not learned
// about this yet.
// For example, consider a group in which id=1 is a learner and id=2 and
// id=3 are voters. A configuration change promoting 1 can be committed on
// the quorum `{2,3}` without the config change being appended to the
// learner's log. If the leader (say 2) fails, there are de facto two
// voters remaining. Only 3 can win an election (due to its log containing
// all committed entries), but to do so it will need 1 to vote. But 1
// considers itself a learner and will continue to do so until 3 has
// stepped up as leader, replicates the conf change to 1, and 1 applies it.
// Ultimately, by receiving a request to vote, the learner realizes that
// the candidate believes it to be a voter, and that it should act
// accordingly. The candidate's config may be stale, too; but in that case
// it won't win the election, at least in the absence of the bug discussed
// in:
// https://github.com/etcd-io/etcd/issues/7625#issuecomment-488798263.
// When responding to Msg{Pre,}Vote messages we include the term
// from the message, not the local term. To see why, consider the
// case where a single node was previously partitioned away and
// it's local term is now out of date. If we include the local term
// (recall that for pre-votes we don't update the local term), the
// (pre-)campaigning node on the other end will proceed to ignore
// the message (it ignores all out of date messages).
// The term in the original message and current local term are the
// same in the case of regular votes, but different for pre-votes.
r.send(pb.Message{To: m.From, Term: m.Term, Type: voteRespMsgType(m.Type)})
if m.Type == pb.MsgVote {
// Only record real votes.
r.electionElapsed = 0
r.Vote = m.From
}
} else {
r.send(pb.Message{To: m.From, Term: r.Term, Type: voteRespMsgType(m.Type), Reject: true})
}
default:
// ...........
}
return nil
}
注意:节点同意投票消息带的是 m.Term ,拒绝投票消息是 r.Term ,如果拒接 MsgPreVote 消息,那么发送pre-vote消息的节点就变为 。
在 r.Term 的 follower ,在2.3.1节内体现 。
根据2.2节可以看到 Step 内有这样一段代码:在2.2节最后有解释,官方也给了详细注释 。
switch {
case m.Type == pb.MsgPreVote:
// Never change our term in response to a PreVote
case m.Type == pb.MsgPreVoteResp && !m.Reject:
// We send pre-vote requests with a term in our future. If the
// pre-vote is granted, we will increment our term when we get a
// quorum. If it is not, the term comes from the node that
// rejected our vote so we should become a follower at the new
// term.
default:
if m.Type == pb.MsgApp || m.Type == pb.MsgHeartbeat || m.Type == pb.MsgSnap {
r.becomeFollower(m.Term, m.From)
} else {
r.becomeFollower(m.Term, None)
}
}
case myVoteRespType:
gr, rj, res := r.poll(m.From, m.Type, !m.Reject)
r.logger.Infof("%x has received %d %s votes and %d vote rejections", r.id, gr, m.Type, rj)
switch res {
case quorum.VoteWon:
if r.state == StatePreCandidate {
r.campaign(campaignElection)
} else {
r.becomeLeader()
r.bcastAppend()
}
case quorum.VoteLost:
// pb.MsgPreVoteResp contains future term of pre-candidate
// m.Term > r.Term; reuse r.Term
r.becomeFollower(r.Term, None)
}
如果预投票成功,则发起新一轮正式投票。如果正式投票成功,则转为leader,接着后续操作 。
func (r *raft) becomeLeader() {
// TODO(xiangli) remove the panic when the raft implementation is stable
if r.state == StateFollower {
panic("invalid transition [follower -> leader]")
}
r.step = stepLeader
r.reset(r.Term)
r.tick = r.tickHeartbeat
r.lead = r.id
r.state = StateLeader
// Followers enter replicate mode when they've been successfully probed
// (perhaps after having received a snapshot as a result). The leader is
// trivially in this state. Note that r.reset() has initialized this
// progress with the last index already.
r.prs.Progress[r.id].BecomeReplicate()
// Conservatively set the pendingConfIndex to the last index in the
// log. There may or may not be a pending config change, but it's
// safe to delay any future proposals until we commit all our
// pending log entries, and scanning the entire tail of the log
// could be expensive.
r.pendingConfIndex = r.raftLog.lastIndex()
emptyEnt := pb.Entry{Data: nil}
if !r.appendEntry(emptyEnt) {
// This won't happen because we just called reset() above.
r.logger.Panic("empty entry was dropped")
}
// As a special case, don't count the initial empty entry towards the
// uncommitted log quota. This is because we want to preserve the
// behavior of allowing one entry larger than quota if the current
// usage is zero.
r.reduceUncommittedSize([]pb.Entry{emptyEnt})
r.logger.Infof("%x became leader at term %d", r.id, r.Term)
}
从 candidate 转变为 leader ,需要在自己的log中append一条当前term的日志,并广播给其他节点 。
最后此篇关于etcd/raft选举源码解读的文章就讲到这里了,如果你想了解更多关于etcd/raft选举源码解读的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
引言 深拷贝是指创建一个新对象,该对象的值与原始对象完全相同,但在内存中具有不同的地址。这意味着如果您对原始对象进行更改,则不会影响到复制的对象 常见的C#常见的深拷贝方式有以下4类:
人工智能是一种未来性的技术,目前正在致力于研究自己的一套工具。一系列的进展在过去的几年中发生了:无事故驾驶超过300000英里并在三个州合法行驶迎来了自动驾驶的一个里程碑;IBM Waston击败了
非法律建议。 欧盟《人工智能法案》 (EU AI Act) 是全球首部全面的人工智能立法,现已正式生效,它将影响我们开发和使用人工智能的方式——包括在开源社区中的实践。如果您是一位开源开发
我已经阅读了所有 HERE Maps API 文档,但找不到答案。 HERE实时流量REST API输出中的XML标签是什么意思? 有谁知道如何解释这个输出(我在我的请求中使用了接近参数)? 最佳答
我的 iPad 应用程序工作正常,我将其留在现场进行测试,但现在崩溃了[保存时?] 这是崩溃日志, Incident Identifier: 80FC6810-9604-4EBA-A982-2009A
我的程序需要 qsort 的功能才能运行,但到目前为止还没有完成它的工作。 我实际上是在对单个字符值的数组进行排序,以便将它们分组,这样我就可以遍历数组并确定每个属性的计数。我的问题是 qsort 返
就目前情况而言,这个问题不太适合我们的问答形式。我们希望答案得到事实、引用资料或专业知识的支持,但这个问题可能会引发辩论、争论、民意调查或扩展讨论。如果您觉得这个问题可以改进并可能重新开放,visit
我正在尝试使用 AVR 代码对 Arduino Uno 进行编程,因为我不被允许在 9 月份开始的高级项目中使用 Arduino 库。我找到了数据表,让数字引脚正常工作,然后尝试通过 USB 串行连接
我遇到了多次崩溃,似乎 native iOS 方法正在从第三方库调用函数。这是一个例子: Thread: Unknown Name (Crashed) 0 libsystem_kernel.d
我理解如何按照 Dijkstra 算法的解释找到从头到尾的最短路径,但我不明白的是解释。在这里,从图中的图形来看,从 A 到 E 添加到我已知集合的顺序是 A,C,B,D,F,H,G,E 我没有得到的
我正在查看一些 Django 源代码并遇到了 this . encoding = property(lambda self: self.file.encoding) 究竟是做什么的? 最佳答案 其他两
Sentry 提供了很好的图表来显示消息频率,但关于它们实际显示的内容的信息很少。 这些信息是每分钟吗? 5分钟? 15分钟?小时? 最佳答案 此图表按分钟显示。这是负责存储该图数据的模型。 http
我对 JavaScript 和 Uniswap 还很陌生。我正在使用 Uniswap V3 从 DAI/USDC 池中获取价格。我的“主要”功能如下所示: async function main()
我正在尝试弄清楚我下载的 Chrome 扩展程序是如何工作的(这是骗子用来窃取 CS:GO 元素的东西,并不重要...)。我想知道使用什么电子邮件地址(或使用什么其他通信方式)来提交被钓鱼的数据。 这
引言 今天同事问了我一个问题, System.Windows.Forms.Timer 是前台线程还是后台线程,我当时想的是它是跟着UI线程一起结束的,应该是前台线程吧? 我确实没有仔
我需要一些使用 scipy.stats.t.interval() 函数的帮助 http://docs.scipy.org/doc/scipy/reference/generated/scipy.sta
当我在 Oracle 查询计划中看到类似的内容时: HASH JOIN TABLE1 TABLE2 这两个表中的哪一个是 hashed ? Oracle 文档指的是通常被散列的“较小”
我想知道 GridSearchCV 返回的分数与按如下方式计算的 R2 指标之间的差异。在其他情况下,我收到的网格搜索分数非常负(同样适用于 cross_val_score),我将不胜感激解释它是什么
本文分享自华为云社区《 多主创新,让云数据库性能更卓越 》,作者: GaussDB 数据库。 华为《Taurus MM: bringing multi-master to the clou
我真的需要一些帮助来破译这个崩溃报告: Process: Farm Hand [616] Path: /Applications/Farm
我是一名优秀的程序员,十分优秀!