ETCD
1、什么是ETCD
etcd是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现。类似项目有zookeeper和consul。
etcd具有以下特点:
- 完全复制:集群中的每个节点都可以使用完整的存档
- 高可用性:Etcd可用于避免硬件的单点故障或网络问题
- 一致性:每次读取都会返回跨多主机的最新写入
- 简单:包括一个定义良好、面向用户的API(gRPC)
- 安全:实现了带有可选的客户端证书身份验证的自动化TLS
- 快速:每秒10000次写入的基准速度
- 可靠:使用Raft算法实现了强一致、高可用的服务存储目录
2、解析ETCD中的raft算法
2.1、什么是raft
客户raft是工程上使用较为广泛的强一致性、去中心化、高可用的分布式协议。本人因为对Paxos算法也不了解,但是归根一句话来说,两者协议差不多,但是Paxos更难理解,同时raft也有自己独到的特性:
- 客户强领导者(Strong leader):和其他一致性算法相比,Raft 使用一种更强的领导能力形式。比如,日志条目只从领导者发送给其他的服务器。这种方式简化了对复制日志的管理并且使得 Raft 算法更加易于理解。
- 客户领导选举(Leader election):Raft 算法使用一个随机计时器来选举领导者。这种方式只是在任何一致性算法都必须实现的心跳机制上增加了一点机制。在解决冲突的时候会更加简单快捷。
- 客户易于理解
2.2、raft节点之间采用的协议
客户raft是raft节点之间的通信采用的是rpc协议
2.2、raft的一些概念
2.2、复制状态机
客户一致性算法通常会在复制状态机的上下文中来描述,即多个机器拥有状态的多份copy,并能在一些机器故障时不中断的提供服务。复制状态用于解决分布式系统中的各种容错问题。复制状态的一些例子比如Chubby和Zookeeper,提供了少量数据的KV存储,除了基本的Put/Get操作之外,还加入了CAS等操作用于安全的处理并发问题。
Replicated state machine通常使用replicated log来实现,如图:
基于上面的图,我的理解如下:
- 客户端请求写入,此时写入转入到leader上面
- leader把第一步写入命令记录入日志,并把日志复制给给跟随者
- 跟随者执行这些命令日志命令,并把状态返回给leader,同时leader也执行最终的日志写入
- leader把写入状态返回给客户端
每一个server都有一个日志保存了一系列的指令,state machine会顺序执行这些指令。每一个日志都以相同顺序保存着相同的指令,因此每一个state machine处理相同的指令,state machine是一样的,所以最终会达到相同的状态及输出。
保证replicated log的一致是一致性算法的任务。server中的一致性模块接收客户端传来的指令并添加到自己的日志中,它也可以和其他server中的一致性模块沟通来确保每一条log都能有相同的内容和顺序,即使其中一些server宕机。 一旦指令被正确复制,就可以称作committed。每一个server中的状态机按日志顺序处理committed指令,并将输出返回客户端。
实际系统中的一致性算法通常有如下特性:
- 在任何情况下能够确保安全(不返回错误的结果),包括网络延迟、分区、丢包、重复、失序
- 只要大多数节点可以正常工作和通信就能够保证完整的可用性。因此,一个典型的拥有5台机器的集群可以容忍两台机器的故障。当server恢复后可以读取持久化的状态并重新加入集群中。
- 不依赖于时间来确保日志的一致。错误的时钟和极端的消息延迟会导致不可用,因此它是异步的方式来保证安全性,消息以任意的速度来执行。
- 通常情况下,只要大多数节点对对RPC调用进行了响应就可看作命令执行完成,一小部分速度慢的server也不会拖慢整个系统的性能。
复制状态机使用场景
复制状态机可以以各种方式来使用,这一节会讨论它的各种使用模式。
每通常会使用三或五台机器来部署一个replicated state machine集群,其他的server可以使用状态机来协调他们的活动,如图,这些系统通常使用replicated state machine来提供group membership、配置管理、锁等。比如一个具体的例子,replicated state machine可以提供一个容错的工作队列,其他server可以利用它来协调任务分配。
一个更简化的场景如图b,一个server作为leader,管理其他的server,leader将一些重要的数据存储在一致性系统中,如果leader故障,会从其他server竞争选出新的leader,一旦成功就可以继续使用一致性系统中的数据继续操作。许多大规模的存储系统都是这么做的,如GFS、HDFS等。
有时候以会用来复制超大的数据,如图2.3,大规模存储系统中,将数据分散到集群的多个server上,将数据划分成许多replicated state machine,使用two-phase commit protocol来管理一致性。
2.3、raft一致性算法
在设计Raft算法的时候有几个目标:
- 它必须可以支持实现一个完整的系统,需要极大的减少开发者的设计工作。
- 它必须在任何条件下都能保证安全和可用性。
- 必须效率足够高。
- 可理解性,它必须能够让更多的人可以理解,而且便于工程实现。
我们使用了两种技术来简化我们的算法:
1. 第一个就是众所周知的问题分解方法,在任何可能的地方将问题划分成几块来解决,可以独立的分析和理解,比如我们在Raft中我们设计了**Leader选举、日志复制、和安全**三部分。 2. 我们的第二个方法就是通过减少状态数来简化状态空间,尽可能消除系统中的不确定性,比如Raft限制log的使用方式来减少这种不确定性,当然有时候引入一些不确定性也会便于我们的理解,我们会使用随机化来简化Leader的选举过程。raft总括
Raft是一个管理replicated log的算法,图3.1总结了算法的核心内容便于参考,图3.2列出来了算法的一些关键特性,图中的内容我们会在后面的文章中详细的进行分析。
Raft首先选举出一个server作为Leader,然后赋予它管理日志的全部责任。Leader从客户端接收日志条目,复制给其他server,并告诉他们什么时候可以安全的将日志条目应用到自己的状态机上。拥有一个Leader可以简化replicated log的管理。例如,leader可以决定将新的日志条目放在什么位置,而无需询问其他节点,数据总是简单的从leader流向其他节点。Leader可能失败或者断开连接,这种情况下会选出一个新的leader。
通过leader,Raft将一致性问题分解成三个相当独立的子问题:
- Leader Election:当集群启动或者leader失效时必须选出一个新的leader。
- Log Replication:leader必须接收客户端提交的日志,并将其复制到集群中的其他节点,强制其他节点的日志与leader一样。
- Safety:最关键的安全点就是图3.2中的State Machine Safety Property。如果任何一个server已经在它的状态机apply了一条日志,其他的server不可能在相同的index处apply其他不同的日志条目。后面将会讲述raft如何实现这一点。
Raft基础
一个Raft集群会包含数个server,5是一个典型值,可以容忍两个节点失效。在任何时候每个server都会处于Leader、Candidate、Follower三种状态中的一种。在正常情况下会只有一个leader,其他节点都是follower,follower是消极的,他们不会主动发出请求而仅仅对来自leader和candidate的请求作出回应。leader处理所有来自客户端的请求(如果客户端访问follower,会把请求重定向到leader)。Candidate状态用来选举出一个leader。如图:
两个上图两个超时时间在选举中的用途:
follower上面的超时时间:主要的作用是到foller没有收到来自candidate和leader的消息的时候,超过这个时间,当前的follower将会成为候选者,当follower收到来此leader或者candidate消息的时候,当前follower会从零开始计时间。
candidate上面的超时时间:当candidate超过时间没有接受到来自follower或者candidate的投票的时候,那么当前的candidate将会重新发起投票,直到找出leader
Raft将时间划分为任意长度的term,用连续整数编号。每一个term都从选举开始,一个或多个candidate想要成为leader,如果一个candidate赢得选举,它将会在剩余的term中作为leader。在一些情况下选票可能会被瓜分,导致没有leader产生,这个term将会以没有leader结束,一个新的term将会很快产生。Raft确保每个term至多有一个leader。Term在Raft中起到了逻辑时钟的作用,它可以帮助server检测过期信息比如过期的leader。每一个server都存储有current term字段,会自动随时间增加。当server间通信的时候,会交换current term,如果一个节点的current term比另一个小,它会自动将其更新为较大者。如果candidate或者leader发现了自己的term过期了,它会立刻转为follower状态。如果一个节点收到了一个含有过期的term的请求,它会拒绝该请求。
Raft节点之间通过RPC进行通信,基本的一致性算法仅仅需要两种RPC。RequestVote RPC由candidate在选举过程中发出,AppendEntries RPC由leader发出,用于复制日志和提供心跳。每一个请求类型都有对应的response,Raft假定request和response都可能会丢失,因此要求请求者有超时重试的能力。为了性能,RPC请求会并行发出,而且不保证RPC的到达顺序。
2.3.1、raft实现一致性的步骤
2.3.1.1、领导者选举
1、选举描述:
刚开始启动分布式服务的时候,因为此时没有候选者和领导者,所以接受不到消息,因为只有候选者和领导者才能给跟随者发送消息,最先超时没有接收到消息的跟随者会转变成候选者,同时给自己投一票,同时任期加1。
候选者给跟随者发送投票通知,当候选者得到的票数超过半数的时候,就会成为领导者
有时候也会这样的问题,有多个跟随者同时成为了候选者,这样的问题怎么破呢,别怕,接着往下看,当有多个候选者的时候,这些候选者首先会发起一次投票,如果在超时范围之内有一个候选者得到了大多数票,则转变成领导者,如果讲过一轮投票后还是没有决出领导者,则最先超时的那个候选者将会重新发起一次投票,如此反复。
2.3.1.1、领导者选举通信流程
总结起来:
投票的契机:
(1)成为候选者
(2)选举超时
成为候选者的契机:
(1)跟随者直到超时也没有接收到来自领导者跟候选者的消息
成为领导者的契机:
(1)候选者选票超过半数
通信协议
rpc
请求参数
- | 参数 | 说明 |
---|---|---|
1 | term | 候选人的任期号 |
2 | candidateID | 请求选票的候选人的 Id |
3 | lastLogIndex | 候选人的最后日志条目的索引值 |
4 | lastLogTerm | 候选人最后日志条目的任期号 |
返回参数
- | 返回参数 | 说明 |
---|---|---|
1 | term | 当前任期号,以便于候选人去更新自己的任期号(当候选人的任期号小于返回的任期号的时候,则使用返回的任期号) |
2 | voteGranted | 候选人赢得了此张选票时为真 |
2.3.1.2、日志复制
日志复制契机
(1)客户端发起写命令请求时
(2)发送心跳时
(3)日志匹配失败时
请求参数
- | 返回参数 | 说明 |
---|---|---|
1 | term | 当前领导者的任期 |
2 | leaderId | 领导者ID 因此跟随者可以对客户端进行重定向 |
3 | prevLogIndex | 紧邻新日志条目之前的那个日志条目的索引 |
4 | prevLogTerm | 紧邻新日志条目之前的那个日志条目的任期 |
5 | entries[] | 需要被保存的日志条目(被当做心跳使用是 则日志条目内容为空;为了提高效率可能一次性发送多个) |
6 | leaderCommit | 领导者的已知已提交的最高的日志条目的索引 |
返回值
- | 返回参数 | 说明 |
---|---|---|
1 | term | 当前任期,对于领导者而言 它会更新自己的任期 |
2 | success | 结果为真 如果跟随者所含有的条目和prevLogIndex以及prevLogTerm匹配上了 |
日志复制的流程大体如下:
- 每个客户端的请求都会被重定向发送给leader,这些请求最后都会被输入到raft算法状态机中去执行。
- leader在收到这些请求之后,会首先在自己的日志中添加一条新的日志条目。
- 在本地添加完日志之后,leader将向集群中其他节点发送AppendEntries RPC请求同步这个日志条目,当这个日志条目被成功复制之后(什么是成功复制,下面会谈到),leader节点将会将这条日志输入到raft状态机中,然后应答客户端。
Raft日志的组织形式如下图所示。
每个日志条目包含以下成员:
- lindex:日志索引号,即图中最上方的数字,是严格递增的。
- lterm:日志任期号,就是在每个日志条目中上方的数字,表示这条日志在哪个任期生成的。
- lcommand:日志条目中对数据进行修改的操作。
一条日志如果被leader同步到集群中超过半数的节点,那么被称为“成功复制”,这个日志条目就是“已被提交(committed)”。如果一条日志已被提交,那么在这条日志之前的所有日志条目也是被提交的,包括之前其他任期内的leader提交的日志。如上图中索引为7的日志条目之前的所有日志都是已被提交的日志。
以下面的图示来说明日志复制的流程
在上图中,一个请求有以下步骤。
-
客户端发送SET a=1的命令到leader节点上。
-
leader节点在本地添加一条日志,其对应的命令为SET a=1。这里涉及到两个索引值,committedIndex存储的最后一条提交(commit)日志的索引,appliedIndex存储的是最后一条应用到状态机中的日志索引值,一条日志只有被提交了才能应用到状态机中,因此总有 committedIndex >= appliedIndex不等式成立。在这里只是添加一条日志还并没有提交,两个索引值还指向上一条日志。
-
leader节点向集群中其他节点广播AppendEntries消息,带上SET a=1命令。
接下来继续看,上图中经历了以下步骤。 -
收到AppendEntries请求的follower节点,同样在本地添加了一条新的日志,也还并没有提交。
-
follower节点向leader节点应答AppendEntries消息。
-
当leader节点收到集群半数以上节点的AppendEntries请求的应答消息时,认为SET a=1命令成功复制,可以进行提交,于是修改了本地committed日志的索引指向最新的存储SET a=1的日志,而appliedIndex还是保持着上一次的值,因为还没有应用该命令到状态机中。
当这个命令提交完成了之后,命令就可以提交给应用层了。 -
提交命令完成,给应用层说明这条命令已经提交。此时修改appliedIndex与committedIndex一样了。
-
leader节点在下一次给follower的AppendEntries请求中,会带上当前最新的committedIndex索引值,follower收到之后同样会修改本地日志的committedIndex索引。
需要说明的是,7和8这两个操作并没有严格的先后顺序,谁在前在后都没关系。
leader上保存着已被提交的最大日志索引信息,在每次向follower节点发送的AppendEntries RPC请求中都会带上这个索引信息,这样follower节点就知道哪个日志已经被提交了,被提交的日志将会输入Raft状态机中执行。
lRaft算法保持着以下两个属性,这两个属性共同作用满足前面提到的日志匹配(LogMatch)属性:
- 如果两个日志条目有相同的索引号和任期号,那么这两条日志存储的是同一个指令。
- 如果在两个不同的日志数据中,包含有相同索引和任期号的日志条目,那么在这两个不同的日志中,位于这条日志之前的日志数据是相同的。
leader与follower同步数据
在正常的情况下,follower节点和leader节点的日志一直保持一致,此时AppendEntries RPC请求将不会失败。但是,当leader节点宕机时日志就可能出现不一致的情况,比如在这个leader节点宕机之前同步的数据并没有得到超过半数以上节点都复制成功了。如下图所示就是一种出现前后日志不一致的情况。
在上图中,最上面的一排数字是日志的索引,盒子中的数据是该日志对应的任期号,左边的字母表示的是a-f这几个不同的节点。图中演示了好几种节点日志与leader节点日志不一致的情况,下面说明中以二元组<任期号,索引号>来说明各个节点的日志数据情况:
- leader节点:<6, 10>。
- a节点:<6,9>,缺少日志。
- b节点:<4,4>,任期号比leader小,因此缺少日志。
- c节点:<6,11>,任期号与leader相同,但是有比leader日志索引更大的日志,这部分日志是未提交的日志。
- d节点:<7,12>,任期号比leader大,这部分日志是未提交的日志。
- e节点:<4,7>,任期号与索引都比leader小,因此既缺少日志,也有未提交的日志。
- f节点:❤️,11>,任期号比leader小,所以缺少日志,而索引比leader大,这部分日志又是未提交的日志。
在Raft算法中,解决日志数据不一致的方式是Leader节点同步日志数据到follower上,覆盖follower上与leader不一致的数据。
为了解决与follower节点同步日志的问题,leader节点中存储着两个与每个follower节点日志相关的数据。
- nextIndex存储的是下一次给该节点同步日志时的日志索引。
- matchIndex存储的是该节点的最大日志索引。
从以上两个索引的定义可知,在follower与leader节点之间日志复制正常的情况下,nextIndex = matchIndex + 1。但是如果出现不一致的情况,则这个等式可能不成立。每个leader节点被选举出来时,将初始化nextIndex为leader节点最后一条日志,而matchIndex为0,这么做的原因在于:leader节点将从后往前探索follower节点当前存储的日志位置,而在不知道follower节点日志位置的情况下只能置空matchIndex了。
leader节点通过AppendEntries消息来与follower之间进行日志同步的,每次给follower带过去的日志就是以nextIndex来决定,如果follower节点的日志与这个值匹配,将返回成功;否则将返回失败,同时带上本节点当前的最大日志ID(假设这个索引为hintIndex),方便leader节点快速定位到follower的日志位置以下一次同步正确的日志数据。
而leader节点在收到返回失败的情况下,将置nextIndex = min(hintIndex+1,上一次append消息的索引),再次发出添加日志请求。
以上图的几个节点为例来说明情况。
-
初始状态下,leader节点将存储每个folower节点的nextIndex为10,matchIndex为0。因此在成为leader节点之后首次向follower节点同步日志数据时,将复制索引位置在10以后的日志数据,同时带上日志二元组<6,10>告知follower节点当前leader保存的follower日志状态。
-
a节点:由于节点的最大日志数据二元组是<6,9>,正好与leader发过来的日志<6,10>紧挨着,因此返回复制成功。
(注:在下面解释每个follower与leader同步流程的示意图中,follower节点回应的消息,格式是,比如<10,4>表示接收到leader发过来的append消息索引是10,而当前该follower最大消息索引是4。)
- b节点:由于节点的最大日志数据二元组是<4,4>,与leader发送过来的日志数据<6,10>不匹配,将返回失败同时带上自己最后的日志索引4(即hintIndex=4),leader节点在收到该拒绝消息之后,将修改保存该节点的nextIndex为min(10,4+1)=5,所以下一次leader节点将同步从索引5到10的数据给b节点。
- c节点:由于节点的最大日志数据二元组是<6,11>,与leader发送过来的日志数据<6,10>不匹配,由于两者任期号相同,节点C知道自己的索引11的数据需要删除。
- d节点:由于节点的最大日志数据二元组是<7,12>,与leader发送过来的日志数据<6,10>不匹配,索引11、12的数据将被删除。
- e节点:由于节点的最大日志数据二元组是<4,7>,与leader发送过来的日志数据<6,10>不匹配,将返回最后一个与节点数据一致的索引5给leader,于是leader从min(5+1,10)=6开始同步数据给节点e,最终e节点上索引7的数据被覆盖。
- f节点:由于节点的最大日志数据二元组是<3,11>,与leader发送过来的日志数据<6,10>不匹配,将返回最后一个与节点数据一致的索引3给leader,于是leader从min(3+1,10)=4开始同步数据给节点f,最终f节点上索引4之后的数据被覆盖。
2.3.1.3、安全
选举限制
raft算法中,并不是所有节点都能成为leader。一个节点要成为leader,需要得到集群中半数以上节点的投票,而一个节点会投票给一个节点,其中一个充分条件是:这个进行选举的节点的日志,比本节点的日志更新。之所以要求这个条件,是为了保证每个当选的节点都有当前最新的数据。为了达到这个检查日志的目的,RequestVote RPC请求中需要带上参加选举节点的日志信息,如果节点发现选举节点的日志信息并不比自己更新,将拒绝给这个节点投票。
如果判断日志的新旧?这通过对比日志的最后一个日志条目数据来决定,首先将对比条目的任期号,任期号更大的日志数据更新;如果任期号相同,那么索引号更大的数据更新。
以上处理RequestVote请求的流程伪代码表示如下。
对比RequestVote请求中带上的最后一条日志数据:
如果任期号比节点的最后一条数据任期号小:
拒绝投票给该节点
如果索引号比节点的最后一条数据索引小:
拒绝投票给该节点
其他情况:
说明选举节点的日志信息比本节点更新,投票给该节点。
提交前面任期的日志条目
如果leader在写入但是还没有提交一条日志之前崩溃,那么这条没有提交的日志是否能提交?有几种情况需要考虑,如下图所示。
在上图中,有以下的场景变更。
-
情况a:s1是leader,index 2位置写入了数据2,该值只写在了s1,s2上,但是还没有被提交。
-
情况b: s1崩溃,s5成为新的leader,该节点在index 2上面提交了另外一个值3,但是这个值只写在了s5上面,并没有被提交。
-
情况c: s5崩溃,s1重新成为leader,这一次,index 2的值2写到了集群的大多数节点上。
此时可能存在以下两种情况:
- 情况d1: s1崩溃,s5重新成为leader(投票给s5的是s4,s2和s5自身),那么index 2上的值3这一次成功的写入到集群的半数以上节点之上,并成功提交。
- 情况d2: s1不崩溃,而是将index 2为2的值成功提交。
从情况d的两种场景可以看出,在index 2值为2,且已经被写入到半数以上节点的情况下,同样存在被新的leader覆盖的可能性。
由于以上的原因,对于当前任期之前任期提交的日志,并不通过判断是否已经在半数以上集群节点写入成功来作为能否提交的依据。只有当前leader任期内的日志是通过比较写入数量是否超过半数来决定是否可以提交的。
对于任期之前的日志,Raft采用的方式,是只要提交成功了当前任期的日志,那么在日志之前的日志就认为提交成功了。这也是为什么etcd-Raft代码中,在成为leader之后,需要再提交一条dummy的日志的原因–只要该日志提交成功,leader上该日志之前的日志就可以提交成功。
2.4、集群成员变更
在上面描述Raft基本算法流程中,都假设集群中的节点是稳定不变的。但是在某些情况下,需要手动改变集群的配置。
2.4.1、安全性
安全性是变更集群成员时首先需要考虑到的问题,任何时候都不能出现集群中存在一个以上leader的情况。为了避免出现这种情况,每次变更成员时不能一次添加或者修改超过一个节点,集群不能直接切换到新的状态,如下图所示。
在上图中,server 1、2、3组成的是旧集群,server 4、5是准备新加入集群的节点。注意到如果直接尝试切换到新的状态,在某些时间点里,如图中所示,由于server 1、2上的配置还是旧的集群配置,那么可能这两个节点已经选定了一个leader;而server 3、4、5又是新的配置,它们也可能选定了一个leader,而这两个leader不是同一个,这就出现了集群中存在一个以上leader的情况了。
反之,下图所示是分别往奇数个以及偶数个集群节点中添加删除单个节点的场景。
可以看到,不论旧集群节点数量是奇数还是偶数个,都不会出现同时有两个超过半数以上子集群的存在,也就不可能选出超过一个leader。
raft采用将修改集群配置的命令放在日志条目中来处理,这样做的好处是:
- 可以继续沿用原来的AppendEntries命令来同步日志数据,只要把修改集群的命令做为一种特殊的命令就可以了。
- 在这个过程中,可以继续处理客户端请求。
2.4.2、 可用性
2.4.2.1、添加新节点到集群中
添加一个新的节点到集群时,需要考虑一种情况,即新节点可能落后当前集群日志很多的情况,在这种情况下集群出现故障的概率会大大提高,如下图所示。
上图中的情况a中,s1、s2、s3是原有的集群节点,这时把节点s4添加进来,而s4上又什么数据都没有。如果此时s3发生故障,在集群中原来有三个节点的情况下,本来可以容忍一个节点的失败的;但是当变成四个节点的集群时,s3和s4同时不可用整个集群就不可用了。
因此Raft算法针对这种新添加进来的节点,是如下处理的。
- 添加进来的新节点首先将不加入到集群中,而是等待数据追上集群的进度。
- leader同步数据给新节点的流程是,划分为多个轮次,每一轮同步一部分数据,而在同步的时候,leader仍然可以写入新的数据,只要等新的轮次到来继续同步就好。
以下图来说明同步数据的流程
如上图中,划分为多个轮次来同步数据。比如,在第一轮同步数据时,leader的最大数据索引为10,那么第一轮就同步10之前的数据。而在同步第一轮数据的同时,leader还能继续接收新的数据,假设当同步第一轮完毕时,最大数据索引变成了20,那么第二轮将继续同步从10到20的数据。以此类推。
这个同步的轮次并不能一直持续下去,一般会有一个限制的轮次数量,比如最多同步10轮
2.4.2.2、删除当前集群的leader节点
当需要下线当前集群的leader节点时,leader节点将发出一个变更节点配置的命令,只有在该命令被提交之后,原先的leader节点才下线,然后集群会自然有一个节点选举超时而进行新的一轮选举
2.4.2.3、处理移出集群的节点
如果某个节点在一次配置更新之后,被移出了新的集群,但是这个节点又不知道这个情况,那么按照前面描述的Raft算法流程来说,它应该在选举超时之后,将任期号递增1,发起一次新的选举。虽然最终这个节点不会赢得选举,但是毕竟对集群运行的状态造成了干扰。而且如果这个节点一直不下线,那么上面这个发起新选举的流程就会一直持续下去。
为了解决这个问题,Raft引入了一个成为“PreVote”的流程,在这个流程中,如果一个节点要发起一次新的选举,那么首先会广播给集群中的其它所有节点,询问下当前该节点上的日志是否足以赢下选举。只有在这个PreVote阶段赢得超过半数节点肯定的情况下,才真正发起一次新的选举。
然而,PreVote并不能解决所有的问题,因为很有可能该被移除节点上的日志也是最新的。
由于以上的原因,所以不能完全依靠判断日志的方式来决定是否允许一个节点发起新一轮的选举。
Raft采用了另一种机制。如果leader节点一直保持着与其它节点的心跳消息,那么就认为leader节点是存活的,此时不允许发起一轮新的选举。这样follower节点处理RequestVote请求时,就需要加上判断,除了判断请求进行选举的节点日志是否最新以外,如果当前在一段时间内还收到过来自leader节点的心跳消息,那么也不允许发起新的选举。然而这种情况与前面描述的leader迁移的情况相悖,在leader迁移时是强制要求发起新的选举的,因此RequestVote请求的处理还要加上这种情况的判断。
总结来说,RequestVote请求的处理逻辑大致是这样的。
follower处理RequestVote请求:
如果请求节点的日志不是最新的:
拒绝该请求,返回
如果此时是leader迁移的情况:
接收该请求,返回
如果最近一段时间还有收到来自leader节点的心跳消息:
拒绝该请求,返回
接收该请求
2.4、日志压缩
日志数据如果不进行压缩处理掉的话,会一直增长下去。为此Raft使用快照数据来进行日志压缩,比如针对键值a的几次操作日志a=1、删除a、a=3最后可以被压缩成为最后的结果数据即a=3。
快照数据和日志数据的组织形式如下图
在上图中:
- 未压缩日志前,日志数据保存到了<3,5>的位置,而在<2,3>的位置之前的数据都已经进行提交了,所以可以针对这部分数据进行压缩。
- 压缩日志之后,快照文件中存放了几个值:压缩时最后一条日志的二元数据是<2,3>,而针对a的几次操作最后的值为a=3,b的值为2。
3、ETCD的使用场景
- 配置管理
- 服务注册于发现
- 选主
- 应用调度
- 分布式队列
- 分布式锁