分布式 - 协议和应用
总结
本文从三个方面介绍了一致性,首先是描述分布架构中的核心理论-CAP,以及其简单的证明。第二部分介绍了 CAP 里面协议,重点介绍了 Raft 协议。第三部分,重点介绍了常用的 zookeeper 原理。
为了保证数据 commit 之后不可丢,系统都会采用(WAL write ahead log)(在每次修改数据之前先写操作内容日志,然后再去修改数据。即使修改数据时异常,也可以通过操作内容日志恢复数据)
分布式存储系统中,是假设机器是不稳定,随时都有可能 down 掉的情况下来设计的。也就是说就算机器 down 掉了,用户写入的数据也不能丢,避免单点故障。为此每一份写入的数据,需要在多个副本中同时存放。例如 zk 节点数据复制,etcd 的数据复制。而复制数据给节点又会带来一致性的问题,例如主节点和从节点数据不一致改如何去同步数据。也会带来可用性的问题,如 leader 节点 down 掉,如何快速选主,恢复数据等。好在已有成熟的理论如 Paxos 协议,ZAB 协议 Raft 协议等做为支撑。
一、一致性
1.1 CAP 理论
C 一致性:分布式环境中,一致性是指多个副本之间,在同一时刻能否有同样的值
A 可用性:系统提供的服务必须一直处于可用的状态。即使集群中一部分节点故障。
P 分区容错性:系统在遇到节点故障,或者网络分区时,任然能对外提供一致性和可用性的服务。以实际效果而言,分区相当于通信的时限要求。系统如果不能在一定实现内达成数据一致性,也就意味着发生了分区的情况。必须就当前操作在 C 和 A 之前作出选择
1.2 CAP不能同时满足的证明
假设系统中有 5 个节点,n1~n5。n1,n2,n3 在A物理机房。n4,n5 在 B 物理机房。现在发生了网络分区,A 机房和 B 机房网络不通。
保证一致性:此时客户端在 A 机房写入数据,不能同步到B机房。写入失败。此时失去了可用性。
保证可用性:数据在 A 机房的 n1~n3 节点都写入成功后返回成功。数据在 B 机房的 n4~n5 节点也写入数据,返回成功。同一份数据在 A 机房和 B 机房出现了数据不一致的情况。聪明如你,可以想到 zookeeper,当一个节点 down 掉,系统会将其剔出节点,然其它一半以上的节点写入成功即可。是不是 zookeeper 同时满足了 CAP 呢。其实这里有一个误区,系统将其剔出节点。有一个隐含的条件是,系统引入了一个调度者,一个踢出坏节点的调度者。当调度者和 zookeeper 节点出现网络分区,整个系统还是不可用的。
1.3 常见场景
CA without P: 在分布式环境中,P 是不可避免的,天灾(某软公司的Azure被雷劈劈中)人祸(某里公司 A 和 B 机房之间的光缆被挖断)都能导致P。
CP without A:相当于每个写请求都须在Server之前强一致。P (分区)会导致同步时间无限延长。这个是可以保证的。例如数据库的分布式事务,两阶段提交,三阶段提交等。
AP without C: 当网络分区发生,A 和 B 集群失去联系。为了保证高可用,系统在写入时,系统写入部分节点就会返回成功,这会导致在一定时间之内,客户端从不同的机器上面读取到的是数据是不一样的。例如 redis 主从异步复制架构,当 master down 掉,系统会切换到 slave,由于是异步复制,salve 不是最新的数据,会导致一致性的问题。
二、一致性协议
2.1 两阶段提交协议(2PC)
二阶段提交( Two-phaseCommit )是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法( Algorithm )。通常,二阶段提交也被称为是一种协议( Protocol )。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的 ACID 特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
2.1.1 两种角色
协调者
参与者
2.1.2 处理阶段
询问投票阶段:事务协调者给每个参与者发送 Prepare 消息,参与者受到消息后,,要么在本地写入 redo 和 undo 日志成功后,返回同意的消息,否者一个终止事务的消息。
执行初始化(执行提交):协调者在收到所有参与者的消息后,如果有一个返回终止事务,那么协调者给每个参与者发送回滚的指令。否者发送 commit 消息
2.1.3 异常情况处理
协调者故障:备用协调者接管,并查询参与者执行到什么地址
参与者故障:协调者会等待他重启然后执行
协调者和参与者同时故障:协调者故障,然后参与者也故障。例如:有机器 1,2,3,4。其中 4 是协调者,1,2,3是参与者 4 给1,2 发完提交事务后故障了,正好3这个时候也故障了,注意这是 3 是没有提交事务数据的。在备用协调者启动了,去询问参与者,由于3死掉了,一直不知道它处于什么状态(接受了提交事务,还是反馈了能执行还是不能执行 3 个状态)。面对这种情况,2PC,是不能解决的,要解决需要下文介绍的 3PC。
2.1.4 缺点
同步阻塞问题:由于所有参与的节点都是事务阻塞型的,例如update tablesetstatus=1wherecurrent_day=20181103,那么参与者table表的current_day=20181103的记录都会被锁住,其他的要修改current_day=20181103行的事务,都会被阻塞
单点故障阻塞其他事务:协调者再执行提交的阶段 down 掉,所有的参与者出于锁定事务资源的状态中。无法完成相关的事务操作。
参与者和协调者同时 down 掉:协调者在发送完 commit 消息后 down 掉,而唯一接受到此消息的参与者也 down 掉了。新协调者接管,也是一个懵逼的状态,不知道此条事务的状态。无论提交或者回滚都是不合适的。这个是两阶段提交无法改变的
2.2 三阶段提交协议(3PC)
2PC 当时只考虑如果单机故障的情况,是可以勉强应付的。当遇到协调者和参与者同时故障的话,2PC 的理论是不完善的。此时 3PC 登场。
3PC 就是对 2PC 漏洞的补充协议。主要改动两点:
在 2PC 的第一阶段和第二阶段插入一个准备阶段,做到就算参与者和协调者同时故障也不阻塞,并且保证一致性。
在协调者和参与者之间引入超时机制
2.2.1 处理的三个阶段
事务询问阶段( can commit 阶段):协调者向参与者发送 commit 请求,然后等待参与者反应。这个和 2PC 阶段不同的是,此时参与者没有锁定资源,没有写 redo,undo,执行回滚日志。回滚代价低
事务准备阶段 (pre commit):如果参与者都返回ok,那么就发送Prepare消息,参与者本地执行redo和undo日志。否者就向参与者提交终止(abort)事务的请求。如果再发送Prepare消息的时候,等待超时,也会向参与者提交终止事务的请求。
执行事务阶段(do commit):如果所有发送Prepare都返回成功,那么此时变为执行事务阶段,向参与者发送commit事务的消息。否者回滚事务。在此阶段参与者如果在一定时间内没有收到docommit消息,触发超时机制,会自己提交事务。此番处理的逻辑是,能够进入此阶段,说明在事务询问阶段所有节点都是好的。即使在提交的时候部分失败,有理由相信,此时大部分节点都是好的。是可以提交的
2.2.2 缺点
不能解决网络分区的导致的数据不一致的问题:例如 1~5 五个参与者节点,1,2,3 个节点在A机房,4,5 节点在 B 机房。在pre commit阶段,1~5 个节点都收到 Prepare 消息,但是节点1执行失败。协调者向1~5个节点发送回滚事务的消息。但是此时A,B机房的网络分区。1~3 号节点会回滚。但是 4~5 节点由于没收到回滚事务的消息,而提交了事务。待网络分区恢复后,会出现数据不一致的情况。
不能解决 fail-recover 的问题:
由于 3PC 有超时机制的存在,2PC 中未解决的问题,参与者和协调者同时 down 掉,也就解决了。一旦参与者在超时时间内没有收到协调者的消息,就会自己提交。这样也能避免参与者一直占用共享资源。但是其在网络分区的情况下,不能保证数据的一致性。
2.3 Paxos协议
像 2PC 和 3PC 都需要引入一个协调者的角色,当协调者 down 掉之后,整个事务都无法提交,参与者的资源都出于锁定的状态,对于系统的影响是灾难性的,而且出现网络分区的情况,很有可能会出现数据不一致的情况。有没有不需要协调者角色,每个参与者来协调事务呢,在网络分区的情况下,又能最大程度保证一致性的解决方案呢。此时 Paxos 出现了。
Paxos 算法是 Lamport 于 1990 年提出的一种基于消息传递的一致性算法。由于算法难以理解起初并没有引起人们的重视,Lamport在八年后重新发表,即便如此Paxos算法还是没有得到重视。2006 年 Google 的三篇论文石破天惊,其中的 chubby 锁服务使用Paxos 作为 chubbycell 中的一致性,后来才得到关注。
2.3.1 解决了什么问题
Paxos 协议是一个解决分布式系统中,多个节点之间就某个值(提案)达成一致(决议)的通信协议。它能够处理在少数节点离线的情况下,剩余的多数节点仍然能够达成一致。即每个节点,既是参与者,也是决策者
2.3.2 两种角色(两者可以是同一台机器)
Proposer:提议提案的服务器
Acceptor:批准提案的服务器
由于 Paxos 和下文提到的 zookeeper 使用的 ZAB 协议过于相似,详细讲解参照下文, Zookeeper原理部分。
2.4 Raft协议
Paxos 是论证了一致性协议的可行性,但是论证的过程据说晦涩难懂,缺少必要的实现细节,而且工程实现难度比较高广为人知实现只有 zk 的实现 zab 协议。然后斯坦福大学 RamCloud 项目中提出了易实现,易理解的分布式一致性复制协议 Raft。Java,C++,Go 等都有其对应的实现。
2.4.1 基本名词
节点状态
Leader(主节点):接受 client 更新请求,写入本地后,然后同步到其他副本中
Follower(从节点):从 Leader 中接受更新请求,然后写入本地日志文件。对客户端提供读请求
Candidate(候选节点):如果 follower 在一段时间内未收到 leader 心跳。则判断 leader 可能故障,发起选主提议。节点状态从 Follower 变为 Candidate 状态,直到选主结束
termId:任期号,时间被划分成一个个任期,每次选举后都会产生一个新的 termId,一个任期内只有一个 leader。termId 相当于 paxos 的 proposalId。
RequestVote:请求投票,candidate 在选举过程中发起,收到 quorum (多数派)响应后,成为 leader。
AppendEntries:附加日志,leader 发送日志和心跳的机制
election timeout:选举超时,如果 follower 在一段时间内没有收到任何消息(追加日志或者心跳),就是选举超时。
2.4.2 特性
Leader 不会修改自身日志,只会做追加操作,日志只能由Leader转向Follower。例如即将要down掉的Leader节点已经提交日志1,未提交日志 2,3。down 掉之后,节点 2 启动最新日志只有 1,然后提交了日志 4。好巧不巧节点 1 又启动了。此时节点 2 的编号 4 日志会追加到节点 1 的编号 1 日志的后面。节点 1 编号 2,3 的日志会丢掉。
不依赖各个节点物理时序保证一致性,通过逻辑递增的 term-id 和 log-id 保证。
2.4.3 选主契机
在超时时间内没有收到 Leader 的心跳
启动时
2.4.4 选主过程
如图 raft-2所示,Raft将时间分为多个 term(任期),term 以连续的整数来标识,每个 term 表示一个选举的开始。例如 Follower 节点 1,在 term1 和 term2 连接处的时间,联系不到 Leader,将 currentTerm 编号加1,变成2,进入了到term2任期,在 term2 的蓝色部分选举完成,绿色部分正常工作。
当然一个任期不一定能选出 Leader,那么会将 currentTerm 继续加1,然后继续进行选举,例如图中的 t3。选举的原则是,每一轮选举每个选民一张选票,投票的请求先到且选民发现候选人节点的日志 id 大于等于自己的,就会投票,否者不会投票。获得半数以上的票的节点成为主节点,注意这并不是说选出来的事务 id 一定是最大的,例如下图 raft-1a~f六个节点(正方形框里面的数字是选举的轮数 term)。
在第四轮选举中,a 先发出投票,六台机器中,a~e 都会投 a,即使 f 不投 a,a 也会赢得选举。如果没有事务id(如刚启动时),就遵循投票请求先来先头。然后 Leader 将最新的日志复制到各个节点,再对外提供服务。
当然除了这些选举限制,还会有其他的情况。如 commit 限制等保证,Leader 选举成功一定包含所有的 commit 和 log。
2.4.5 日志复制过程
raft 日志写入过程,主节点收到一个 x=1的请求后,会写入本地日志,然后将 x=1的日志广播出去,follower 如果收到请求,会将日志写入本地 log ,然后返回成功。当 leader 收到半数以上的节点回应时,会将此日志的状态变为 commit,然后广播消息让 follwer 提交日志。节点在 commit 日志后,会更新状态机中的 logindex 。
firstLogIndex/lastLogIndex 为节点中开始和结束的索引位置(包含提交,未提交,写入状态机)commitIndex:已提交的索引。applyIndex:已写入状态机中的索引。
日志复制的本质是让 follwer 和 Leader 的已提交的日志顺序和内容都完全一样,用于保证一致性。
具体的原则就是:
原则1:两个日志在不同的 raft 节点中,如果有两个相同的 term 和 logIndex ,则保证两个日志的内容完全一样。
原则2:两段日志在不同的 raft 节点中,如果起始和终止的的 term,logIndex 都相同,那么两段日志中日志内容完全一样。
如何保证
第一个原则只需要在创建 logIndex 的时候使用新的 logIndex,保证 logIndex 的唯一性。而且创建之后不去更改。那么在 leader 复制到 follwer 之后,logIndex,term 和日志内容都没变。
第二个原则,在 Leader 复制给 Follower 时,要传递当前最新日志 currenTermId 和currentLogIndex,以及上一条日志 preCurrentTermId 和 preCurrentLogIndex。如图 raft-1,在 d 节点,term7,logIndex12。在给节点节点 a 同步时,发送(term7,logIndex11),(term7,logIndex12),a 节点没有找到(term7,logIndex11)的日志,会让Leader,d 节点重新发送。d 节点会重新发(term6,logIndex10)(term7,logIndex11),还是没有(term6,logIndex10)的日志,依然会拒绝同步。接着发(term6,logIndex9)(term6,logIndex10)。现在a节点有了(term6,logIndex9)。那么 leader 节点就会将(term6,logIndex9) ~ (term7,logIndex11)日志内容给节点 a,节点 a 将会和节点 d 有一样的日志。
三、Zookeeper 原理
3.1 概述
Google 的粗粒度锁服务 Chubby 的设计开发者 Burrows 曾经说过:“所有一致性协议本质上要么是 Paxos 要么是其变体”。Paxos 虽然解决了分布式系统中,多个节点就某个值达成一致性的通信协议。但是还是引入了其他的问题。由于其每个节点,都可以提议提案,也可以批准提案。当有三个及以上的 proposer 在发送 prepare 请求后,很难有一个 proposer 收到半数以上的回复而不断地执行第一阶段的协议,在这种竞争下,会导致选举速度变慢。
所以 zookeeper 在 paxos 的基础上,提出了 ZAB 协议,本质上是,只有一台机器能提议提案(Proposer),而这台机器的名称称之为 Leader 角色。其他参与者扮演 Acceptor 角色。为了保证 Leader 的健壮性,引入了 Leader 选举机制。
ZAB协议还解决了这些问题
在半数以下节点宕机,依然能对台提供服务
客户端所有的写请求,交由 Leader 来处理。写入成功后,需要同步给所有的 follower 和 observer
leader 宕机,或者集群重启。需要确保已经再 Leader 提交的事务最终都能被服务器提交,并且确保集群能快速回复到故障前的状态
3.2 基本概念
基本名词
数据节点(dataNode):zk 数据模型中的最小数据单元,数据模型是一棵树,由斜杠( / )分割的路径名唯一标识,数据节点可以存储数据内容及一系列属性信息,同时还可以挂载子节点,构成一个层次化的命名空间。
事务及 zxid:事务是指能够改变 Zookeeper 服务器状态的操作,一般包括数据节点的创建与删除、数据节点内容更新和客户端会话创建与失效等操作。对于每个事务请求,zk 都会为其分配一个全局唯一的事务 ID,即 zxid,是一个 64 位的数字,高 32 位表示该事务发生的集群选举周期(集群每发生一次 leader 选举,值加 1),低 32 位表示该事务在当前选择周期内的递增次序(leader 每处理一个事务请求,值加 1,发生一次 leader 选择,低 32 位要清 0)。
事务日志:所有事务操作都是需要记录到日志文件中的,可通过 dataLogDir 配置文件目录,文件是以写入的第一条事务 zxid 为后缀,方便后续的定位查找。zk 会采取“磁盘空间预分配”的策略,来避免磁盘 Seek 频率,提升 zk 服务器对事务请求的影响能力。默认设置下,每次事务日志写入操作都会实时刷入磁盘,也可以设置成非实时(写到内存文件流,定时批量写入磁盘),但那样断电时会带来丢失数据的风险。
事务快照:数据快照是 zk 数据存储中另一个非常核心的运行机制。数据快照用来记录 zk 服务器上某一时刻的全量内存数据内容,并将其写入到指定的磁盘文件中,可通过 dataDir 配置文件目录。可配置参数 snapCount,设置两次快照之间的事务操作个数,zk 节点记录完事务日志时,会统计判断是否需要做数据快照(距离上次快照,事务操作次数等于snapCount/2~snapCount 中的某个值时,会触发快照生成操作,随机值是为了避免所有节点同时生成快照,导致集群影响缓慢)。
核心角色
leader:系统刚启动时或者 Leader 崩溃后正处于选举状态;
follower:Follower 节点所处的状态,Follower 与 Leader 处于数据同步阶段;
observer:Leader 所处状态,当前集群中有一个 Leader 为主进程。
节点状态
LOOKING:节点正处于选主状态,不对外提供服务,直至选主结束;
FOLLOWING:作为系统的从节点,接受主节点的更新并写入本地日志;
LEADING:作为系统主节点,接受客户端更新,写入本地日志并复制到从节点
3.3 常见的误区
写入节点后的数据,立马就能被读到,这是错误的。* zk 写入是必须通过 leader 串行的写入,而且只要一半以上的节点写入成功即可。而任何节点都可提供读取服务。例如:zk,有 1~5 个节点,写入了一个最新的数据,最新数据写入到节点 1~3,会返回成功。然后读取请求过来要读取最新的节点数据,请求可能被分配到节点 4~5 。而此时最新数据还没有同步到节点4~5。会读取不到最近的数据。如果想要读取到最新的数据,可以在读取前使用 sync 命令*。
zk启动节点不能偶数台,这也是错误的。zk 是需要一半以上节点才能正常工作的。例如创建 4 个节点,半数以上正常节点数是 3。也就是最多只允许一台机器 down 掉。而 3 台节点,半数以上正常节点数是 2,也是最多允许一台机器 down 掉。4 个节点,多了一台机器的成本,但是健壮性和 3 个节点的集群一样。基于成本的考虑是不推荐的
3.4 选举同步过程
3.4.1 发起投票的契机
节点启动
节点运行期间无法与 Leader 保持连接,
Leader 失去一半以上节点的连接
3.4.2 如何保证事务
ZAB 协议类似于两阶段提交,客户端有一个写请求过来,例如设置 /my/test 值为 1,Leader 会生成对应的事务提议(proposal)(当前 zxid为 0x5000010 提议的 zxid 为Ox5000011),现将 set/my/test1(此处为伪代码)写入本地事务日志,然后 set/my/test1日志同步到所有的follower。follower收到事务 proposal ,将 proposal 写入到事务日志。如果收到半数以上 follower 的回应,那么广播发起 commit 请求。follower 收到 commit 请求后。会将文件中的 zxid ox5000011 应用到内存中。
上面说的是正常的情况。有两种情况。第一种 Leader 写入本地事务日志后,没有发送同步请求,就 down 了。即使选主之后又作为 follower 启动。此时这种还是会日志会丢掉(原因是选出的 leader 无此日志,无法进行同步)。第二种 Leader 发出同步请求,但是还没有 commit 就 down 了。此时这个日志不会丢掉,会同步提交到其他节点中。
3.4.3 服务器启动过程中的投票过程
现在 5 台 zk 机器依次编号 1~5
节点 1 启动,发出去的请求没有响应,此时是 Looking 的状态
节点 2 启动,与节点 1 进行通信,交换选举结果。由于两者没有历史数据,即 zxid 无法比较,此时 id 值较大的节点 2 胜出,但是由于还没有超过半数的节点,所以 1 和 2 都保持 looking 的状态
节点 3 启动,根据上面的分析,id 值最大的节点 3 胜出,而且超过半数的节点都参与了选举。节点 3 胜出成为了 Leader
节点 4 启动,和 1~3 个节点通信,得知最新的 leader 为节点 3,而此时 zxid 也小于节点 3,所以承认了节点 3 的 leader 的角色
节点 5 启动,和节点 4 一样,选取承认节点 3 的 leader 的角色
3.4.4 服务器运行过程中选主过程
1. 节点 1 发起投票,第一轮投票先投自己,然后进入 Looking 等待的状态。
2. 其他的节点(如节点 2 )收到对方的投票信息。节点 2 在 Looking 状态,则将自己的投票结果广播出去(此时走的是上图中左侧的 Looking 分支);如果不在 Looking 状态,则直接告诉节点 1 当前的 Leader 是谁,就不要瞎折腾选举了(此时走的是上图右侧的 Leading/following 分支)。
3. 此时节点 1,收到了节点 2 的选举结果。如果节点 2 的 zxid 更大,那么清空投票箱,建立新的投票箱,广播自己最新的投票结果。在同一次选举中,如果在收到所有节点的投票结果后,如果投票箱中有一半以上的节点选出了某个节点,那么证明 leader 已经选出来了,投票也就终止了。否则一直循环。
zookeeper 的选举,优先比较大 zxid,zxid 最大的节点代表拥有最新的数据。如果没有 zxid,如系统刚刚启动的时候,则比较机器的编号,优先选择编号大的。
3.5 同步的过程
在选出 Leader 之后,zk 就进入状态同步的过程。其实就是把最新的 zxid 对应的日志数据,应用到其他的节点中。此 zxid 包含 follower 中写入日志但是未提交的 zxid 。称之为服务器提议缓存队列 committedLog 中的 zxid。
同步会完成三个 zxid 值的初始化。
peerLastZxid:该 learner 服务器最后处理的 zxid。 minCommittedLog:leader服务器提议缓存队列 committedLog 中的最小 zxid。 maxCommittedLog:leader服务器提议缓存队列 committedLog 中的最大 zxid。
系统会根据 learner 的 peerLastZxid和 leader 的 minCommittedLog, maxCommittedLog做出比较后做出不同的同步策略。
3.5.1 直接差异化同步
场景: peerLastZxid介于 minCommittedLogZxid和 maxCommittedLogZxid间。
此种场景出现在,上文提到过的,Leader 发出了同步请求,但是还没有 commit 就 down 了。 leader 会发送 Proposal 数据包,以及 commit 指令数据包。新选出的 leader 继续完成上一任 leader 未完成的工作。
例如此刻 Leader 提议的缓存队列为 0x20001,0x20002,0x20003,0x20004,此处 learn 的 peerLastZxid 为 0x20002,Leader会将 0x20003 和 0x20004 两个提议同步给 learner。
3.5.2 先回滚在差异化同步/仅回滚同步
此种场景出现在,上文提到过的,Leader 写入本地事务日志后,还没发出同步请求,就 down 了,然后在同步日志的时候作为 learner 出现。
例如即将要 down 掉的 leader 节点 1,已经处理了 0x20001,0x20002,在处理 0x20003 时还没发出提议就 down 了。后来节点 2 当选为新 leader,同步数据的时候,节点 1 又神奇复活。如果新 leader 还没有处理新事务,新 leader 的队列为,0x20001, 0x20002,那么仅让节点 1 回滚到 0x20002 节点处,0x20003 日志废弃,称之为仅回滚同步。如果新 leader 已经处理 0x30001 , 0x30002 事务,那么新 leader 此处队列为0x20001,0x20002,0x30001,0x30002,那么让节点 1 先回滚,到 0x20002 处,再差异化同步0x30001,0x30002。
3.5.3 全量同步
peerLastZxid小于 minCommittedLogZxid或者 leader 上面没有缓存队列。leader 直接使用 SNAP 命令进行全量同步。
四、使用 Raft + RocksDB 分布式 KV 存储服务
当前开源的缓存 kv 系统,大都是 AP 系统,例如设置主从同步集群 redis,master 异步同步到 slave。虽然在 master 停止服务后,slave 会顶上来。但是在 master 写入了数据,但是还没来得及同步到 slave 就 down 了,然后 slave 被选为主节点继续对外提供服务的情况下,会丢失部分数据。这对于要求强一致性的系统来说是不可接受的。例如很多场景下 redis 做分布式锁,有天然的缺陷在里面,如果 master 停止服务,这个锁不很不可靠的,虽然出现的几率很小,但一旦出现,将是致命的错误。
为了实现 CP 的 KV 存储系统,且要兼容现有的 redis 业务。有赞开发了 ZanKV(先已开源ZanRedisDB)。
底层的存储结构是 RocksDB(底层采用 LSM 数据结构)。一个 setx=1的会通过 redis protocol 协议传输,内容会通过 Raft 协议,同步写入到其他的节点的 RocksDB。有了Raft 理论的加持,RocksDB优秀的存储性能,即使遇到网络分区,master 节点 down 掉, slave 节点 down 掉,等一系列异常情况,其都能轻松应对。在扩容方面,系统用选择维护映射表的方式来建立分区和节点的关系,映射表会根据一定的算法并配合灵活的策略生成,来达到方便扩容。