【分布式-6.824】Lecture5-Tolerance-RAFT-01

1.一些历史

关键词:
DRAM                  ---一般计算机的内存
RAFT                  ---算法
SPLIT BRAIN           ---脑裂,80年代末出现的问题
Majority Voting Scheme---多数投票方式
PAXOS
VSR


80年代如果两台机器之间的沟通要保证完全的不受硬件故障的影响,拥有很多钱,也许你能做到.

于是,我们想要设计构建出自动故障切换系统,面对不稳定的网络的时候,它也可以正常工作.
比如说遇到这种网络分裂的情况,它也能够正常工作.
在网络一分为二的情况下,两边都没有办法互相进行通信,这通常被称作网络分裂.人们为了构建
不受脑裂现象困扰的自动复制系统而想到的重要想法是少数服从多数,这个是RAFT那篇论文中提出
的一个概念,这是某种基本的处理方式,首先,我们的服务器数量得是奇数,而不是偶数,就像这里的
缺点一样,有点过于对称了,此处被拆分的两侧,看起来就像是一模一样,它们运行着相同的软件,
做一样的事情,这并不好.
但如果你所拥有的服务器数量是奇数的话,那么就不再对称了,至少,在遇上一次网络分裂的情况发生
的时候,我们可以想象出大致情况是这样的,这就是Majority Voting Scheme所吸引我们的地方.
它的基本思路就是我们的服务器数量得是奇数,为了获得进展,Raft得选出一个leader或者提交一个
日志条目,为了在每一步取得进展,你必须取得半数服务器以上的支持,为了批准该步骤,我们得选出
一个leader,或者接受一个新的日志条目的提交.最简单的方法就是使用三台中的两台服务器来做这个
事情(选leader),这种做法能够奏效的其中一个理由就是,那在分裂的几个分区中,不能有多个分区都
拥有半数以上的服务器投票.在一个分区中,可以有一个服务器在里面,它代表的是少数服务器(即服务器
中的很小一部分),或者,在这区域里也可以有两个服务器,但如果一个分区中有两个服务器,另一个分区
中必须只有一个服务器,因此,这样就永远无法取得多数支持,也没有办法取得任何进展.


Majority Voting Scheme
当我们在讨论大多数的时候,我们指的始终是所有服务器中的大多数服务器,而不是在线服务器中的大多
数服务器(比如说,有三台服务器,一个挂了,另外两个投票的时候还是按照三个模式来投票,比如1,2,3中
的1挂了,2和3也可以都向1投票,我们需要做的就是发起一次又一次选举,直到2或3拿到多数票).

如果你想要你的服务更可靠,可以构建一个使用更多服务器的系统.
比较通用的公式是:2f+1,如果你有2f+1台机器,那么你就可以承受f次故障,比如说,如果你拥有三台服务
器,那就意味着f是1,就说明你可以容忍在1台服务器发生故障的情况下,该系统继续运行.

通常,它们被称作Quorum系统,因为三分之二的服务器有时候会持有一个Quorum.

关于Majority Voting Scheme,我们已经提及的一个属性就是,最多只有一个分区是majority,所以如果
网络被分裂,我们不能让两个分裂的网络都能继续运作.


另一个比较微妙的事情就是,如果始终需要获取大多数人服务器的同意下去处理某事,需要经过一系列的
处理,在每一次操作中,有人获得了多数支持,比如说给leader投票,或者说给存活的leader投票,然后,每
一步中,这个所谓的大多数(过半),两次大多数中至少有一台服务器是两个term共有的,也就是重叠的,
(比如说有3台服务器那么过半就是2台,4台服务器过半就是3台,如果是大多数的话,就是1:3,前后两次
中的大多数,至少有一台服务器是重叠的).比起其他东西而言,Raft更加依赖这个属性,以此来避免脑裂
问题.比如说,当你成功选出了一个leader,该leader取得了多数投票,这个多数就是投票总数过半(提现
就是上一个leader锁获得的选票也有部分来自本轮投票的服务器),比如说,我们要保证新的leader能够
知道前一个leader所使用的term号,因为两次的选举中的绝大部分中总会有重叠的服务器.所有前一次
选举中的majority中的每个服务器都知道前一次leader锁代表的term号.类似的,在Raft中,上一次
majority中的服务器它们接受了,因此我们就可以以这个重叠的服务器来作为切入点进行新一轮的
leader选举.这个是Raft为什么是正确的重要原因.


1990年的时候,有人将这个想法落地,也就是有两个系统同一时间对它进行了使用,可以使用Majority 
Voting System,通过用三个服务器取代两个服务器发起majority vote来绕过这个不可避免的脑裂问题,
其中有一个比较早的系统,它的名字叫做PAXOS,在RAFT那批论文中,对此谈到了许多,还有一个非常古老
的系统,叫做viewstamp replication,它的缩写是VSR,尽管在这个领域,PAXOS比VSR更加广为人知,在实
际的设计上,Raft更接近于VSR(viewstamp replication).这个是由MIT的人发明的.这里有很多的历史,
直到15年前,它们才真正走到最前沿并开始在已经部署的大型分布式系统中大量使用.对它们来说,这个
是它们被发明出来之后的黄金15年.



2.RAFT

2.1 RAFT的一次无故障操作的执行过程

RAFT以一个库的形式被包含在某些服务应用程序中,如果你有一个复制服务,在该服务中的每一个副本可以
通过一个raft库在加一些代码来接受RPC之类的请求,raft库之间会进行合作,依次来维护replication.对于
一个单个的ratf replica软件,我们大致来思考一下这个replica所应该具备的应用程序代码,所以在lab3
中,我们可能会有一个key-value服务器,在应用程序中会有一个raft来帮助管理的replicated state,对于
一个key-value服务器来说,它里面会有一张key-value表,接下来,下面一层是RAFT层,key-value服务器会
在raft中进行函数调用,它们会反复进行通信,通过raft来保持状态,在figure2中可以看到,处于我们的目
的,state中最关键的一个部分就是raft会保存一份关于操作的日志.

有着三个副本的系统,实际上这三个副本是有着完全相同结构的服务器,我们希望三个副本在这两个层面上
所拥有的数据几乎是一模一样的.

除此之外,我们要说的就是客户端.假设我们有两个客户端Client1和Client2,所谓的客户端指的就是需要能
够使用该服务的外部代码,我们希望这些Client意识不到他们是和一个有着复制服务功能的服务器集群进行
通信的,对于客户端来说,它仿佛是在和一台服务器进行通信.Client端实际上将请求发送给当前leader所在
的应用层,即发给raft中作为当前leader的那个副本,这些可能是属于应用层面的请求,比如针对基于数据库
或者key-value服务器所发出的put或者get请求,通过put请求讲一个key-value更新到表中,通过get请求让
服务器去取得当前key所对应的value,这看起来好像和我们的raft没有什么关系,但实际上是不对的,这只是
看起来而已.


对于一个没有自动容错功能的服务器来说,这只是客户端与服务器端之间的交互,一旦这些请求从客户端发
送到服务器端,实际上所发生的事情就是在一个没有复制服务功能的服务器上,应用程序代码会执行put这个
请求,更新这张表上的内容,并且对这个put请求进行处理响应,但这个过程中没有复制服务的存在.


但是对于有复制服务存在的集群上,客户端与服务器的通信过程就不是这么回事了.它更复杂一些.
假设服务器端中leader接受到客户端的请求之后,这里需要发生的事情就是,应用层会直接把Client
端的请求下发给Raft层,对(它Raft层)说,这里有一个请求,请将它提交到replicated Log中,并且当你
做完了以后,请告诉我你做完了.因此,在此时,Raft会和每一个副本进行通信,直到所有的副本中有半数
以上的副本把这个新的操作添加到日志中,并表示,它已经复制完了,那么Raft的Leader就知道所有已完
成日志记录的副本,只有在这个前提之下,Raft层会发送给key-value应用层一个通知,说,hi,你发给我
操作,我已经做好日志记录了,它现在已经提交到所有副本中了,所以它已经被安全地复制了,这时候应
用层就可以开始执行(已记录日志的)操作了,客户端发送请求给key-value层,key-value层还没有执行
请求,我们不确定,因为这个请求没有被复制,仅仅当这个请求都记录在所有副本的日志上时,Raft会
通知Leader,这时候Leader才会真正执行这个put请求,以此来更新key-value表,通过一个get请求来
读取表中正确的数据.最终发送返回结果给客户端.以上就是以此普通操作要经历的细节.



如果我们想去构建一个具备容错能力的系统,即使某些服务器已经挂了,它也必须能够继续进行下去,
所以正如黑板上所示,大多数副本都记录日志后,日志状态才能变为提交状态.


当操作最终提交的时候,每一个副本都会让raft层将操作向上发送给本地的应用层,在本地业务层执行该
业务操作.所以我们希望所有的副本都能看见相同的操作流.它们以相同的顺序接受这些操作,即它们接收
到的state(这里的state指的是提交的业务操作)的顺序是相同的.假设这些操作都是确定性的,我们也希
望它是这样的.所以在通常情况下,当paper在讨论state的时候,指的就是对这个表进行操作.

我们换一种方式来看这个交互过程,在本课程中会多次出现的一个概念就是,我将给你们画一种基于时间
线的任务执行图来标识消息是如何传递工作的.
想象一下这样一个场景,我们有一个Client和Server1,Server2,Server3,并且服务器1是leader,想象一
下,Client发送一个原始请求个Server1(普通的put请求),随后Server1的Raft层会发送一个
AppendEntries(添加日志条目)RPC请求给两位两个副本,服务器正在等待回复,这个S1(leader)会等待
其他副本和自己的AppendEntries请求响应(只要有绝大多数服务器响应即可,即majority响应),所以在
这个只有三个副本的系统里面,leader服务器只需要等待一个副本的AppendEntries响应算上自己的就能
够符合majority这个要求了.之后leader会执行客户端的请求,得到GET/PUT的执行结果,然后返回给客户
端.当然S3如果实际上也在线的话,它会发送响应服务给Server1,但是我们不需要等待它的返回结果,因
为我们已经有majority结果了,这对于理解Figure 2来说非常有用.这就是系统在没有异常的情况下,
所执行的一串的普通操作.leader执行客户端的请求,得到GET/PUT的执行结果,然后返回给客户端后,需
要告诉其他副本它已经完成了操作,所以这里就有一条额外的消息,这个额外的消息到底是怎么样的,这
个还需要取决于其他条件,在Raft中,没有一个明确的提交报文,而是把这个信息状态待Leader发出来的
下一个AppendEntriesRPC请求中,在AppendEntriesRPC请求中保存了一些诸如leaderCommit的标识信
息,下一个时间点leader要发送一个心跳或者一个新的客户端请求,因为leader状态发生变化时(包括
leader的更替,日志状态更新等),需要发送给副本一些必要的信息,如最新的leaderCommit,或者做选举
操作等.此时这些副本就会执行该操作,并且修改它们的状态.



这个协议下会有一定量的交互,在这个协议下,响应不是秒回,客户端发送请求,请求必须到达服务器,
服务器至少要与另外一个服务实力进行通信,这些小区请求要等等响应发送回来,所以raft这个请求内含
了很多条信息等待的时间(用于每个rpc请求的超时机制).

当leader发送更新后的commitIndex后,如果客户端只是偶尔发送一次请求,那么,leader会发送一个心跳
或者发送一个特殊的AppendEntries消息,如果客户端请求十分频繁,也没啥事,如果每秒进来数以千计的
请求,那就要花很长时间了,所以我们将它们放在一个请求中,这样就不会产生其他消息,消息来回传输是
要耗费很长时间的,也造成了资源的浪费,一个AppendEntry就好了,总之你能在下一条消息中得到你所
需要的信息,实际上我不认为副本提交的时间点有什么重要的(只要关注leader就可以了),因为
至少在没有发生故障的情况下,没有人会等待它(副本)的响应.如果没有发生故障,副本所执行的请求的
结果不会出现在关键路径上,这些客户端不会等待副本对它们进行响应,S3做的事情,客户端是无视的,
所以感受不到它的延时.



需要考虑以下这个问题,为什么系统如此关注日志,日志它做了什么呢?
试着找出答案是非常有价值的.
对于系统为什么专注于日志的问题,答案是:这个日志是一种机制,记录了ledaer按顺序所执行的操作,
日志对于这些replicated state machine来说是至关重要的,所有的副本不但提交了相同的客户端的
操作,而且连执行的操作顺序也是一样的,这些副本都以相同的顺序提交来自客户端的操作,日志及其他
许多东西只是该机制的一部分,通过leader来指定由client端传入的操作的顺序.比如说有10个客户端
同时发送操作给leader,leader必须制定出某种顺序,以确保所有副本都是按照该顺序执行操作.
日志会对执行的指令顺序进行标序号,以此来标识leader锁选择执行指令的顺序;
日志的另一个用途是S3在接收到AE1和AE2之间这段时间,它并不确定该操作是否已经被提交了,S3还不能
去执行该操作,S3不得不把这次操作先放到某个地方,直到接收到leader提交的新的状态值,日志所做的
另一件事情就是,在follower处,follower会将这些待定的操作放在日志里,虽然follower已经收到了
这些操作,但是我们并不知道它们是否已经被提交了,这些操作可能被丢弃,这种情况我们之后会看到.

从leader的角度来看,日志有什么作用呢?leader需要将他的操作记录在日志中,因为leader可能需要
将这些操作转发给follower,如果某些follower处于离线状态,可能是由于网络操作造成短暂失联或者
丢失了一些信息,leader需要重新发送给follower漏接的日志信息,因此leader需要一个保存客户端请
求信息副本的地方,即使这些请求已经被执行过了,为了能够讲这些操作重新发送给客户端,所以我们需
要这样的地方来保存这些请求信息的副本,我的意思是,我们会将这些副本所错过的操作重新发给副本,
对于它们来说,保存日志的最后一个理由就是:至少在figure 2中,如果一台服务器挂了,然后重启,
并且想要重新加入raft集群的话,需要这个日志(如果不重启这个集群就不能再容错了),重启后的服务器
会使用它之前保存在磁盘中的日志,
因为其中的一条规则是这样的:每个raft服务区都需要将它的日志写入它的磁盘上,当该服务器挂掉
并重启后,也依然要这么做,重启后的服务器可以将日记中的记录的操作从头开始全部重新执行一遍直到
该服务器奔溃前的记录位置,以此来创建该服务器的状态,然后服务器会从那里开始执行任务.所以日志
被用于持久化这一方面,通过一系列命令顺序来重建状态.



问题,假设leader能够每秒钟执行1000个client端的命令,然而每个follower每秒只能执行100个client
端的命令,这个是它们全速处理下的能力,这种情况该怎么处理?(好问题啊)

我们需要注意的一件事情就是这些follower在执行命令之前,先确认命令,所以我们并不对它们在日志中
所确认并累计的操作数量进行限制,它们可以每秒钟确认1000个请求,如果它们永远这样做下去,那么它
们会创建出大小无限的日志文件,因为根据我们的游戏规则,follower的执行速度无限落后于leader给出
消息的速度,这就以为着,在某些时候,它们会耗尽自己服务器上的内存,所以,当follower落后于leader
十亿条数据后,它们就会调用内存分配器来分配新的空间,以此来容纳新的日志条目,但它会失败,所以
raft并不具备解决该问题所需要的流量控制能力,在实际系统中,实际上你的提交命令可能携带在下一次
日志追加请求中,并不需要实时(提交执行),但在这里可能需要某些额外的通信,以此来表示,这里是
我目前的执行进度,所以leader就会这样说:"我比follower所执行的进度要领先数千个请求",作为一个
生产者-消费者模式,生产速度远远大于消费速度,这个可能会引发存储生产元素的容器溢出,如果leader
在执行进度领先follower太多,那么你可能需要通过一条额外的消息来让leader刹刹车.



问题,如果一个服务器挂掉了,它将日志都保存在了磁盘上,因为这个是figure 2里面的一条规则,所以服
务器能从磁盘中拿回日志,但当然,该服务器并不知道它所执行的日志进度到了哪里,当该服务器第一次被
重启后,根据Figure2中的规则,它甚至不清楚其中有多少条日志已经被提交了.

第一个问题的答案是:d当一个服务器挂掉重启以后,并当它读取它的日志的时候,我们不允许它对日志做
任何事情,因为该服务器并不知道,在他的日志中,该系统的提交进度是多少,可能在日志中,它有1000条
未提交的条目和0条已经提交的条目.

在figure2中,它们所拥有的状态被标记为非易失性的,它里面包含了日志,还可能包含了最新的term,
如果发生了一次奔溃,如果这些服务器全挂了,并且全部都重启了,这些服务器中没有哪个服务器知道
它们在挂掉前,它们所执行的进度到哪里了,所以接下来的工作就是选举leader.如果你看下Figure2里
所说的AppendEntries是如何工作的,Leader实际上也会把AppendEntries作为心跳向外发送,通过心跳
来确定Majority所包含的副本的最新的日志提交状况,因为那里就是提交点,换种方式来看,一旦你选出
了一个leader,通过这个AppendEntries机制,leader会强制所有其他副本所有的日志与leader的日志
必须完全一直,在paper中,它里面稍微解释了下这一点,因为leader知道,它强制让所有的副本所拥有的
日志与它完全相同,在日志中所有的执行日志条现在必须完全相同,并且他们也都已经被提交了.
因为它们是保存在大多数副本中的,正如Figure 2 AppendEntriesRPC中所说的那样,我们可以根据
leaderCommit来确定日志中的命令提交点,现在所有的服务器就可以从头开始执行整个日志上的操作.
从0开始重建它们的状态(大家用的日志都是一样的),这种方式是最稳的.但是这个方式不是很吸引人.
但是这个就是基本协议所做的事情.我们明天会看到checkpoint版本要比这个更高效,这个之后我们会
讨论.
(比如说redis中的AOF和RDB之间的关系,RDB可以任务是存档checkpoint,加载后,再读AOF的追加,这样
更快).

以上就是一次无故障操作的执行过程.

2.2 RAFT的设计

如图所示,上面是key/value层,里面包含了该副本的state,在它下面是raft层,在每个副本中,它们
之间主要有两个接口,我画的这个方法是当client端发送请求的时候,key-value需要转发这个请求
给raft层,并表示,请将这个请求放在日志的某个地方,这个是start()函数,你们会在raft.go(读作
raft-dot-go),它仅仅接受一个参数,即来自于客户端的命令,key-value层表示,请把我接收到的这
条命令记录到log中,当提交之后请告诉我一下.另一个方法是,不久之后Raft层会告诉key-value层,
hey~伙计,你刚刚发给我的操作不是最新的strat命令(start其实就是执行,参数就是命令,AppendEntries
每次携带的未提交命令才是最新的,但无法执行,只能执行它携带的committed命令),当有100个客户端的
命令传入的时候,并且在它们中的任一一个命令被提交前,调用了start()方法,这个向上的通信将会将
一条消息放入到一个GO CHANNEL中,这里是通过RAFT库将它放入到channel,key/value层会从这个
长了中对其进行读取,所以这里有一个applyChannel,通过applyChannel你可以发送ApplyMessage,
key/value层可以从applyChannel中接受收到的信息,该信息是我可以用来处理的命令,即该Start()
方法的传入参数,START(command)实际上执行后返回的信息是这条命令的index,START(command)返回
的这个索引index就是如果该请求命令提交log后,该命令在log中的索引号.我想start也许会返回
其他的信息,如:当前的term版本和一些我不太关心的其他信息(比如说是否是leader等),然后这个
ApplyMessage结构体中,包含了index以及command,所有的副本都将取得这些ApplyMsg信息,它们都
会知道我会提交这个命令,并且将它提交到我的Local State中,这些副本也会得到index,leader上
的这个index是真的很有用,这样通过它(index),就可以弄清楚我们所讨论的客户端请求是什么了.

 

3.概念注释:

3.1 split brain

推荐阅读链接(感谢)


一.脑裂概述

# What does "split-brain" mean?
"Split brain" is a condition whereby two or more computers or groups of computers lose 
contact with one another but still act as if the cluster were intact. This is like 
having two governments trying to rule the same country. If multiple computers are 
allowed to write to the same file system without knowledge of what the other nodes are 
doing, it will quickly lead to data corruption and other serious problems.
脑裂就是集群内各节点间的心跳出现故障,但各节点还处于active状态,多个节点分别接管服务并且写入共享文件资源导致数据损坏或者其它问题。


二.解决办法
冗余心跳,但是该方式治标不治本,只能减少脑裂发生的概率.

1.踢出集群

(1)Quorum Algorithm
Quorum Algorithm(选举算法):
集群内各节点通过心跳收集彼此的健康状况,收集到一个心跳就获得一票,假设集群内A,B,C这3个节点,
节点A获得B和自己一票,节点B获得自己和A一票,而节点C只有自己,则节点C被踢出集群.
(2)Quorum Device
Quorum Algorithm有个缺陷:集群内如果只有2个节点,那就很麻烦了,因此,需要引入第3个设备来解决此
问题,此时Quorum Device出现了.Quorum Device(Quorum Disk)这个设备也占一票,这一票由先到请求
者获得,这样就能顺利的踢出另一个节点.

2.IO隔离
做了上面的操作后,还有问题,节点虽然被踢出,但是仍然有可能处于active状态,这样它还能操作共享文
件资源,于是有了IO隔离(IO Fencing),它可以阻止"灰太狼来羊村".
IO Fencing主要有两种方式,分别如下:
(1)硬件方式
(A)SCSI Reserve/Release设备
正常节点能够使用SCSI Reserve/Release命令锁住存储设备,不正常节点发现存储设备被锁后,就用自杀
的方式来了结自己以使自己恢复正常。
(B)STONITH(Shoot The Other Node In The Head)
当一个节点发生故障时,另一个节点如果检测到了,就会通过串口来控制故障节点的电源开关以暂时断电
然后又上电来重启故障节点.手好黑啊,好在还没彻底落井下石.

(2)软件方式
ORACLE RAC就是典型代表,直接重启故障节点以保证故障节点不能继续访问共享数据,具体可参ORACLE.

3.2 推荐阅读

RAM相关阅读(感谢原作者)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值