为啥基于Raft实现的etcd还会出现数据不一致
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为啥基于Raft实现的etcd还会出现数据不一致相关的知识,希望对你有一定的参考价值。
参考技术A Deployment滚动更新异常,节点莫名消失。根据想象产生的几个猜想:
1.etcd集群出现分裂,三个节点分裂成为两个集群。APIServer不会检测各节点集群ID是否一致,因此会出现数据消失现象。
2.Raft日志同步异常,其他节点会不会因为Raft模块存在特殊Bug导致未收到相关日志条目?通过etcd自带的WAL工具来判断,可以显示WAL日志中收到的命令。
3.如果日志同步没问题,有没有可能是Apply模块出现了问题,导致日志条目未被应用到Apply模块?
4.若Apply模块执行了相关日志条目到MVCC模块,MVCC模块的treeIndex子模块会不会出现特殊bug,导致更新失败?
5.若MVCC模块的treeIndex模块无异常,写请求到了boltdb存储模块,有没有可能boltdb出现了极端异常导致数据丢失?
通过使用日志工具来分析日志,出现required revision has been compacted信息,说明APIServer 请求etcd版本号被压缩了。
通过命令:
1.如果节点之间的cluster_id是一致的,就可以排除集群分裂。
2.初步判断集群Raft日志条目同步正常,raftIndex表示Raft日志索引号,raftAppliedIndex表示当前状态机应用的日志索引号。如果差距很小,说明没问题,
如果差距大,说明Raft同步出现异常。
3.观察节点的revision值,相互差距很大,明显偏离标准值,使用WAL工具etcd-dump-logs搜索关键字,例如Hello,如果都能找到,就是Raft日志同步没有异常。
在Apply模块出现问题,但是各节点raftAppliedIndex几乎无差异,Apply流程出现逻辑错误的时候,没有重试机制。Apply流程无论是成功还是失败,都会更新
raftAppliedIndex值。Apply输出的日志:auth:revision in header is old,出现鉴权版本号不一致的问题。
consistent index的未持久化最终导致鉴权命令重复执行。鉴权模块的RoleGrantPermission接口未实现幂等,一连串的bug导致鉴权出现不一致,随后有放大
成MVCC模块的key-value数据不一致,导致严重的数据损坏。
可能存在server应用日志条目到状态机失败,进而导致各个节点出现数据不一致,但是这个不一致并非Raft模块导致的,它已超过Raft模块的功能界限。
优化一致性算法,从学会Raft开始?
算法允许一组机器像一个整体一样工作,即使其中一些机器出现故障也能够继续工作下去,是构建云计算平台的重要基石。
当前比较流行的、用于共享配置和服务发现的高可用键值存储系统etcd就是基于Raft一致性算法实现的。
那么,到底什么是Raft一致性算法呢?
Raft一致性算法,即通过选举一个领导人,然后给予他全部的管理复制日志的责任来实现一致性。领导人从客户端接收日志条目,把日志条目复制到其他服务器上,并且当保证安全性的时候告诉其他的服务器应用日志条目到他们的状态机中。拥有一个领导人大大简化了对复制日志的管理。
通过领导人的方式,Raft将一致性问题分解为如下3个问题:
1.领导选举:当系统初始化或者先前的领导人宕机的时候,需要选举一个新的领导人。
2.日志复制:领导人必须从客户端接收日志然后复制到集群中的其他节点,并且强制要求其他节点的日志保持和自己相同。
3.安全性:在 Raft 中安全性的关键是状态机安全:如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其他服务器节点便不能在同一个日志索引位置应用一个不同的指令。
一个 Raft 集群包含若干个服务器节点。假如服务器节点数为n个(n通常是>=3的奇数),这允许整个系统容忍个(n-1)/2节点的失效。
在任何时刻,每一个服务器节点都处于这三个状态之一:领导人、跟随者或者候选人。
【图 1】:服务器状态。跟随者只响应来自其他服务器的请求。如果跟随者接收不到消息,那么他就会变成候选人并发起一次选举。获得集群中大多数选票的候选人将成为领导者。领导人一直都会是领导人直到自己宕机了。
在通常情况下,系统中只有一个领导人并且其他的节点全部都是跟随者。跟随者都是被动的,他们不会发送任何请求,只是简单的响应来自领导者或者候选人的请求。
领导人处理所有的客户端请求(如果一个客户端和跟随者联系,那么跟随者会把请求重定向给领导人)。
第三种状态——候选人,当跟随者在规定时间内没有收到领导人的心跳消息时,会作为候选人发起重新选举。
【图 2】时间被划分成一个个的任期,每个任期开始都是一次选举。在选举成功后,领导人会管理整个集群直到任期结束。有时候选举会失败,那么这个任期就会没有领导人而结束。任期之间的切换可以在不同的时间不同的服务器上观察到。
Raft 把时间分割成任意长度的任期,如图 2。任期用连续的整数标记。每一段任期从一次选举开始,一个或者多个候选人尝试成为领导者。如果一个候选人赢得选举,然后他就在接下来的任期内充当领导人的职责。
在某些情况下,一次选举过程会造成选票的瓜分。在这种情况下,这一任期会以没有领导人结束;一个新的任期(和一次新的选举)会很快重新开始。Raft 保证了在一个给定的任期内,最多只有一个领导者。
不同的服务器节点可能多次观察到任期之间的转换,但在某些情况下,一个节点也可能观察不到任何一次选举或者整个任期全程。
任期在 Raft 算法中充当逻辑时钟的作用,这会允许服务器节点查明一些过期的信息比如陈旧的领导者。
每一个节点存储一个当前任期号,这一编号在整个时期内单调的增长。当服务器之间通信的时候会交换当前任期号,如果一个服务器的当前任期号比其他人小,那么他会更新自己的编号到较大的编号值。
如果一个候选人或者领导者发现自己的任期号过期了,那么他会立即恢复成跟随者状态。如果一个节点接收到一个包含过期的任期号的请求,那么他会直接拒绝这个请求。
Raft 算法中服务器节点之间通信使用远程过程调用(RPCs),并且基本的一致性算法只需要两种类型的 RPCs。
请求投票(RequestVote)RPCs 由候选人在选举期间发起,附加条目(AppendEntries)RPCs 由领导人发起,用来复制日志和提供一种心跳机制。
当服务器没有及时的收到 RPC 的响应时,会进行重试,并且他们能够并行地发起 RPCs 来获得最佳的性能。
Raft 使用一种心跳机制来触发领导人选举。当服务器程序启动时,他们都是跟随者身份。如果一个服务器节点已经从领导人或者候选者处接收到有效的 RPCs,那么他会继续保持着跟随者状态。
领导者周期性的向所有跟随者发送心跳包(不包含日志项内容的附加日志项 RPCs)来维持自己的权威。如果一个跟随者在一段时间里没有接收到任何消息,也就是选举超时,然后他就会认为系统中没有可用的领导者然后开始进行选举以选出新的领导者。
在开始一次选举过程之前,跟随者先要增加自己的当前任期号并且转换到候选人状态。然后他会并行地向集群中的其他服务器节点发送请求投票的 RPCs 来给自己投票。
候选人会继续保持着当前状态直到以下三件事情之一发生:
● 他自己赢得了这次的选举。
当一个候选人从整个集群的大多数服务器节点获得了针对同一个任期号的选票,那么他就赢得了这次选举并成为领导人。每一个服务器最多会对一个任期号投出一张选票,按照“先来先服务”的原则。要求大多数选票的规则确保了最多只会有一个候选人赢得此次选举。
一旦候选人赢得选举,他就立即成为领导人。然后他会向其他的服务器发送心跳消息来建立自己的权威并且阻止新的领导人的产生。
● 其他的服务器成为领导者。
在等待投票的时候,候选人可能会从其他的服务器接收到声明它是领导人的附加日志项 RPC。
如果这个领导人的任期号(包含在此次的 RPC中)不小于候选人当前的任期号,那么候选人会承认领导人合法并回到跟随者状态。
● 一段时间之后没有任何一个获胜的人。
如果有多个跟随者同时成为候选人,那么选票可能会被瓜分以至于没有候选人可以赢得大多数人的支持。
当这种情况发生的时候,每一个候选人都会超时,然后通过增加当前任期号来开始一轮新的选举。然而,没有其他机制的话,选票可能会被无限的重复瓜分。
Raft 算法使用随机选举超时时间的方法来确保很少会发生选票瓜分的情况,就算发生也能很快的解决。为了阻止选票起初就被瓜分,选举超时时间是从一个固定的区间(例如 150-300毫秒)随机选择。
这样可以把服务器都分散开以至于在大多数情况下只有一个服务器会选举超时;然后他赢得选举并在其他服务器超时之前发送心跳包。
同样的机制被用在选票瓜分的情况下。每一个候选人在开始一次选举的时候会重置一个随机的选举超时时间,然后在下次选举之前一直等待;这样减少了在新的选举中另外的选票瓜分的可能性。
一旦一个领导人被选举出来,他就开始为客户端提供服务。
客户端的每一个请求都包含一条被复制状态机执行的指令。
领导人把这条指令作为一条新的日志条目附加到日志中去,然后并行地发起附加条目 RPCs 给其他的服务器,让他们复制这条日志条目。
当这条日志条目被安全的复制(下面会介绍),领导人会应用这条日志条目到它的状态机中然后把执行的结果返回给客户端。
如果跟随者崩溃或者运行缓慢,再或者网络丢包,领导人会不断的重复尝试附加日志条目 RPCs (尽管已经回复了客户端)直到所有的跟随者都最终存储了所有的日志条目。
【图 3】日志由有序序号标记的条目组成。每个条目都包含创建时的任期号(图中框中的数字),和一个状态机需要执行的指令。一个条目当可以安全的被应用到状态机中去的时候,就认为是可以提交了。
日志以图 3 展示的方式组织。
每一个日志条目存储一条状态机指令和从领导人收到这条指令时的任期号。在日志中的任期号是用来检查不一致情况,每一条日志条目同时也都有一个整数索引值来表明它在日志中的位置。
领导人来决定什么时候把日志条目应用到状态机中是安全的,而这种日志条目被称为“已提交”。Raft 算法保证所有已提交的日志条目都是持久化的并且最终会被所有可用的状态机执行。
在领导人将创建的日志条目复制到大多数的服务器上的时候,日志条目就会被提交(例如在图 3 中的条目 7)。同时,领导人的日志中之前的所有日志条目也都会被提交,包括由其他领导人创建的条目。
领导人跟踪了最大的将会被提交的日志项的索引,并且索引值会被包含在未来的所有附加日志 RPCs (包括心跳包),这样其他的服务器才能最终知道领导人的提交位置。一旦跟随者知道一条日志条目已经被提交,那么他也会将这个日志条目应用到本地的状态机中(按照日志的顺序)。
Raft 的日志机制来维护一个不同服务器的日志之间的最终一致性,是安全性保证的一个重要组件。Raft 维护着以下日志匹配特性:
● 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
● 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同。
第一个特性来自这样的一个事实,领导人最多在一个任期里在指定的一个日志索引位置创建一条日志条目,同时日志条目在日志中的位置也从来不会改变。
第二个特性由附加日志 RPC 的一个简单的一致性检查所保证。
在发送附加日志 RPC 的时候,领导人会把新的日志条目紧接着之前的条目的索引位置和任期号包含在里面。如果跟随者在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝接收新的日志条目。
一致性检查就像一个归纳步骤:一开始空的日志状态肯定是满足日志匹配特性的,然后一致性检查保护了日志匹配特性当日志扩展的时候。
因此,每当附加日志 RPC 返回成功时,领导人就知道跟随者的日志一定是和自己相同的了。
【图 4】当一个领导人成功当选时,跟随者可能是任何情况(a-f)。每一个盒子表示是一个日志条目;里面的数字表示任期号。跟随者可能会缺少一些日志条目(a-b),可能会有一些未被提交的日志条目(c-d),或者两种情况都存在(e-f)。
例如,场景 f 可能这样发生,那个服务器在任期 2 的时候是领导人,附加了一些日志条目到自己的日志中,在提交之前就崩溃了;很快这个机器就重启了,在任期 3 重新被选为领导人,并且又增加了一些日志条目到自己的日志中;在这些任期 2 和任期 3 重点日志被提交之前,这个服务器又宕机了,然后的几个任期里一直处于宕机状态。
在正常的操作中,领导人和跟随者的日志保持一致性,所以附加日志 RPC 的一致性检查从来不会失败。
然而,领导人崩溃的情况会使得日志处于不一致的状态(老的领导人可能还没有完全复制所有的日志条目)。这种不一致问题会在一系列的领导人和跟随者崩溃的情况下加剧。
图 4 展示了跟随者的日志可能和新的领导人不同的方式。
跟随者可能会丢失一些在新的领导人中有的日志条目,他也可能拥有一些领导人没有的日志条目,或者两者都发生。丢失或者多出日志条目可能会持续多个任期。
在 Raft 算法中,领导人处理不一致是通过强制跟随者直接复制自己的日志来解决了。这意味着在跟随者中的冲突的日志条目会被领导人的日志覆盖。
要使得跟随者的日志进入和自己一致的状态,领导人必须找到最后两者达成一致的地方,然后删除从那个点之后的所有日志条目,发送自己的日志给跟随者。所有的这些操作都在进行附加日志 RPCs 的一致性检查时完成。
当一个领导人刚获得权力的时候,他初始化所有的 nextIndex 值为自己日志中的最后一条(图 4 中的 11)。如果一个跟随者的日志和领导人不一致,那么在下一次的附加日志 RPC 时的一致性检查就会失败。
在被跟随者拒绝之后,领导人就会减小 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得领导人和跟随者的日志达成一致。当这种情况发生,附加日志 RPC 就会成功,这时就会把跟随者冲突的日志条目全部删除并且加上领导人的日志。
一旦附加日志 RPC 成功,那么跟随者的日志就会和领导人保持一致,并且在接下来的任期里一直继续保持。
Raft 能够接受、复制并应用新的日志条目,只要大部分的机器是工作的。在通常的情况下,新的日志条目可以在一次 RPC 中被复制给集群中的大多数机器,并且单个的缓慢的跟随者不会影响整体的性能。
前面的章节里描述了 Raft 算法是如何选举和复制日志的。然而,到目前为止描述的机制并不能充分的保证每一个状态机会按照相同的顺序执行相同的指令。
例如,一个跟随者可能会进入不可用状态同时领导人已经提交了若干的日志条目,然后这个跟随者可能会被选举为领导人并且覆盖这些日志条目;因此,不同的状态机可能会执行不同的指令序列。
针对这种情况,Raft 使用了一种简单的方法,它可以保证所有之前的任期号中已经提交的日志条目在选举的时候都会出现在新的领导人中,不需要传送这些日志条目给领导人。
这意味着日志条目的传送是单向的,只从领导人传给跟随者,并且领导人从不会覆盖本地日志中已经存在的条目。
Raft 使用投票的方式来阻止候选人赢得选举除非这个候选人包含了所有已经提交的日志条目。候选人为了赢得选举必须联系集群中的大部分节点,这意味着每一个已经提交的日志条目肯定在这些服务器节点中至少存在一个上面。
如果候选人的日志至少和大多数的服务器节点一样新,那么他一定持有了所有已经提交的日志条目。
请求投票 RPC 实现了这样的限制: RPC 中包含了候选人的日志信息,然后投票人会拒绝掉那些日志没有自己新的投票请求。
Raft 通过比较两份日志中最后一条日志条目的索引值和任期号定义谁的日志比较新。如果两份日志最后的条目的任期号不同,那么任期号大的日志更加新。如果两份日志最后的条目任期号相同,那么日志比较长的那个就更加新。
到目前为止,我们都只关注了领导人崩溃的情况。跟随者和候选人崩溃后的处理方式比领导人要简单的多,并且他们的处理方式是相同的。
如果跟随者或者候选人崩溃了,那么后续发送给他们的 RPCs 都会失败。Raft 中处理这种失败就是简单地通过无限的重试,如果崩溃的机器重启了,那么这些 RPC 就会完整的成功。
如果一个服务器在完成了一个 RPC,但是还没有响应的时候崩溃了,那么在他重新启动之后就会再次收到同样的请求。
Raft 的 RPCs 都是幂等的,所以这样重试不会造成任何问题。例如一个跟随者如果收到附加日志请求但是他已经包含了这一日志,那么他就会直接忽略这个新的请求。
Raft 的安全性不能依赖时间:整个系统不能因为某些事件运行得比预期快一点或者慢一点就产生了错误的结果。但是,可用性(系统可以及时的响应客户端)不可避免的要依赖于时间。
例如,如果消息交换在服务器崩溃时花费更多的时间,候选人将不会等待太长的时间来赢得选举;没有一个稳定的领导人,Raft 将无法工作。
领导人选举是 Raft 中对时间要求最为关键的方面。Raft 可以选举出并维持一个稳定的领导人除非整个系统满足下面的时间要求:
广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)
在这个不等式中,广播时间指的是从一个服务器并行的发送 RPCs 给集群中的其他服务器并接收响应的平均时间;选举超时时间就是在 第2 节中介绍的选举的超时时间限制;平均故障间隔时间就是对于一台服务器而言,两次故障之间的平均时间。
选举超时时间必须比选举超时时间小一个量级,这样领导人才能够发送稳定的心跳消息来阻止跟随者开始进入选举状态;通过随机化选举超时时间的方法,这个不等式也使得选票瓜分的情况变得不可能。
选举超时时间应该要比平均故障间隔时间小上几个数量级,这样整个系统才能稳定的运行。当领导人崩溃后,整个系统会大约相当于选举超时的时间里不可用;我们希望这种情况在整个系统上是很小的情况。
广播时间和平均故障间隔时间是由系统决定的,但是选举超时时间是我们自己选择的。
Raft 的 RPCs 需要接收方将信息持久化存储,所以广播时间大约是 0.5 毫秒到 20 毫秒,取决于存储的技术。
因此,选举超时时间可能需要在 10 毫秒到 500 毫秒之间。大多数的服务器的平均故障间隔时间一般都是以天甚至以月计的,很容易满足时间的需求。
以上是关于为啥基于Raft实现的etcd还会出现数据不一致的主要内容,如果未能解决你的问题,请参考以下文章