Etcd 基于Raft的一致性保证

Etcd基于Raft的一致性


raft本身是一个指导性原则,etcd严格遵循这个指导性原则,做了go语言版本的实现。

etcd很多特性其实就是在学习raft协议的特性。 

选举方法

  • 初始启动时,节点处于 Follower 状态并被设定一个 election timeout,如果在这一时间周期内没有收到来自Leader 的 heartbeat,节点将发起选举∶将自己切换为candidate 之后,向集群中其它 Follower 节点发送请求,询问其是否选举自己成为Leader。
  • 当收到来自集群中过半数节点的接受投票后,节点即成为 Leader,开始接收保存client 的数据并向其它的 Follower节点同步日志。
  • 如果没有达成一致,则candidate 随机选择一个等待间隔(150ms~300ms)再次发起投票,得到集群中半数以上Follower 接受的 candidate 将成为 Leader,新任期的Leader是比老任期的leader有更大的权力的。

上面可以看到选举严格遵循了raft协议。

选举方法

  • Leader 节点依靠定时向 Follower 发送 heartbeat来保持其地位。
  • 任何时候如果其它 Follower在 election timeout期间都没有收到来自Leader 的 heartbeat,,同样会将自己的状态切换为 candidate 并发起选举。每成功选举一次,新Leader 的任期(Term)都会比之前 Leader 的任期大1。

日志复制


当接 Leader 收到客户端的日志(事务请求)后先把该日志追加到本地的 Log 中,然后通过heartbeat 把该Entry同步给其他 Follower,Follower 接收到日志后记录日志然后向 Leader 发送ACK,当Leader收到大多数(n/2+1)Follower 的ACK信息后将该日志设置为已提交并追加到本地磁盘中,通知客户端并在下个heartbeat 中 Leader 将通知所有的Follower 将该日志存储在自己的本地磁盘中。

任何数据的写入都要经过leader,你可以将请求发到follower上面去,但是follower接受到这个请求,它会在一致性模块里面将这个请求转发给leader让leader去处理。leader在接受到超过半数人同意之后才认为这次写是确认掉的。

安全性


安全性∶是用于保证每个节点都执行相同序列的安全机制。

如当某个Follower在当前Leader commit Log时变得不可用了,稍后可能该 Follower 又会被选举为Leader,这时新Leader 可能会用新的Log覆盖先前已committed 的Log,这就会导致节点执行不同序列;Safety就是用于保证选举出来的 Leader 一定包含先前committed LOg 的机制。

假设集群当中有个follower有段时间掉队了,比如leader已经写了10条日志,但是follower只有8条,接下来的一刻leader可能出现了问题,从这个集群当中出去了,掉队的follower可以发起投票,但是它的commit log比主leader少了2个,这里面就会有一个问题,如果它变为新的leader就丢数据了,其他candidate在投票的时候除了要看有没有leader,在接收到投票请求的时候,人比较好,先来先得,它还会去校验你有没有资格当leader,你的数据和我现在的数据是不是一致的,如果你落后于我是不能投票的。

所以这里有leader commitlog用来记录之前任期里面leader已经确认日志的index,如果一个candidate来拉票,但是它的log小于leader commitlog,那么它是没有资格做新的leader的,为了防止数据的丢失。

选举安全性(Election Safety)∶

每个任期(Term)只能选举出一个Leader,如果投票均等,那么需要发起重新投票。

Leader完整性(Leader Completeness)∶

指 Leader 日志的完整性,当 Log在任期Term1 被Commit后,那么以后任期Term2、Term3...等的 Leader 必须包含该 Log;Raft 在选举阶段就使用Term 的判断用干保证完整性。当请求投票的该 Candidate 的Term 较大或Term 相同Index更大则投票,否则拒绝该请求。

你有多次选举,有不同任期leader的时候,新的leader的commit log一定是最全的,它应该包含之前所有任期里面的commitlog,这是怎么保证的呢?如果一个candidate的commit log,低于当前leader的commit log,它没有办法做新的leader,通过这种机制永远确保新的leader永远包含以前完整的日志,这样保证了数据的完整。

失效处理


1.Leader 失效∶其他没有收到 heartbeat 的节点会发起新的选举,而当 Leader 恢复后由于步进

数小会自动成为 Follower(日志也会被新 Leader 的日志覆盖)。

假设一个leader因为脑裂被分出去了,那么其他人可能重新选举,选了一个新的leader,当leader又重新加入到集群当中,它就会去看我的标号和任期比别人小,所以它会自动降级为follower,它的日志也会被新的leader的日志覆盖掉。

2.Follower 节点不可用∶Follower 节点不可用的情况相对容易解决。因为集群中的日志内容始

终是从Leader 节点同步的,只要这一节点再次加入集群时重新从Leader节点处复制日志即可。

follower恢复之后,在leader发送心跳的时候将数据的差异带出去即可。

3.多个candidate∶冲突后candidate 将随机选择一个等待间隔(150ms~300ms)再次发起

投票,得到集群中半数以上 Follower 接受的 candidate 将成为 Leader。

如果是偶数个集群,投票变为2:2了,这种我们是不建议的。

wal 日志(write ahead log)


wal 日志是二进制的,解析出来后是以上数据结构 LogEntry。

  1. 其中第一个字段 type,一种是0表示Normal,1表示ConfChange(ConfChange表示etcd 本身的配置变更同步,比如有新的节点加入等)。
  2. 第二个字段是 term,每个term 代表一个主节点的任期,每次主节点变更term 就会变化。
  3. 第三个字段是 index,这个序号是严格有序递增的,代表变更序号。
  4. 第四个字段是二进制的 data,将raft request 对象的 pb结构整个保存下。

在数据写入的时候,etcd遵循raft协议,第一你要先写日志,再写到db,持久化存储是最终的状态,在这之前要写一个log,这个log叫做wal log,就是一直往前append这样的一个日志。

这个日志是一个二进制的文件,它解析出来是一个数据结构,是一个logentry,logentry有几个重要的字段,第一个是类型,代表是这个是什么日志,比如说有些配置变更的日志(加减节点),normal日志就是代表一个数据的写入。

第二个就是主节点的任期,也就是leader节点是第几个任期。

第三个就是index,就是有序递增的一个序号,就是你每一条变更,每条数据写入都会加1。它的作用是用来记录leader的commit id的,也就是数据结构会去维护leader的commit log id的,知道当前写到哪个标号了,这些标号对应什么数据我们都知道。

第四个字段就是data,就是将整个请求,比如你要去写一个键值对key=value,它会将整个请求当作data保存下来。

上面就是wal log的一个内容。

  • etcd源码下有个tools/etcd-dump-logs,可以将wal日志dump成文本查看,可以协助分析 Raft协议。(wal log本身是二进制的,不是文本,没办法去阅读)
  • Raft协议本身不关心应用数据,也就是 data中的部分,一致性都通过同步 wal日志来实现,每个节点将从主节点收到的 data apply 到本地的存储,Raft只关心日志的同步状态,如果本地存储实现的有 bug,比如没有正确地将 data apply 到本地,也可能会导致数据不一致。

raft协议本身是不关心数据的,它不管你数据是如何存储,它只管保证数据的一致性,那么数据如何存储的部分完全由etcd去实现。

猜你喜欢

转载自blog.csdn.net/qq_34556414/article/details/125581123