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 commit log用来记录之前任期里面leader已经确认日志的index,如果一个candidate来拉票,但是它的log小于leader commit log,那么它是没有资格做新的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。
- 其中第一个字段 type,一种是0表示Normal,1表示ConfChange(ConfChange表示etcd 本身的配置变更同步,比如有新的节点加入等)。
- 第二个字段是 term,每个term 代表一个主节点的任期,每次主节点变更term 就会变化。
- 第三个字段是 index,这个序号是严格有序递增的,代表变更序号。
- 第四个字段是二进制的 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去实现。