假设系统中有 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 节点出现网络分区,整个系统还是不可用的。
CA without P: 在分布式环境中,P 是不可避免的,天灾(某软公司的Azure被雷劈劈中)人祸(某里公司 A 和 B 机房之间的光缆被挖断)都能导致P
CP without A:相当于每个写请求都须在Server之前强一致。P (分区)会导致同步时间无限延长。这个是可以保证的。例如数据库的分布式事务,两阶段提交,三阶段提交等
AP without C: 当网络分区发生,A 和 B 集群失去联系。为了保证高可用,系统在写入时,系统写入部分节点就会返回成功,这会导致在一定时间之内,客户端从不同的机器上面读取到的是数据是不一样的。例如 redis 主从异步复制架构,当 master down 掉,系统会切换到 slave,由于是异步复制,salve 不是最新的数据,会导致一致性的问题。
二阶段提交( Two-phaseCommit )是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法( Algorithm )。通常,二阶段提交也被称为是一种协议( Protocol )。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的 ACID 特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
update table set status=1 where current_day=20181103
,那么参与者table
表的current_day=20181103
的记录都会被锁住,其他的要修改current_day=20181103
行的事务,都会被阻塞2PC 当时只考虑如果单机故障的情况,是可以勉强应付的。当遇到协调者和参与者同时故障的话,2PC 的理论是不完善的。此时 3PC 登场。
3PC 就是对 2PC 漏洞的补充协议。主要改动两点
pre commit
阶段,1~5 个节点都收到 Prepare 消息,但是节点1执行失败。协调者向1~5个节点发送回滚事务的消息。但是此时A,B机房的网络分区。1~3 号节点会回滚。但是 4~5 节点由于没收到回滚事务的消息,而提交了事务。待网络分区恢复后,会出现数据不一致的情况。由于 3PC 有超时机制的存在,2PC 中未解决的问题,参与者和协调者同时 down 掉,也就解决了。一旦参与者在超时时间内没有收到协调者的消息,就会自己提交。这样也能避免参与者一直占用共享资源。但是其在网络分区的情况下,不能保证数据的一致性
像 2PC 和 3PC 都需要引入一个协调者的角色,当协调者 down 掉之后,整个事务都无法提交,参与者的资源都出于锁定的状态,对于系统的影响是灾难性的,而且出现网络分区的情况,很有可能会出现数据不一致的情况。有没有不需要协调者角色,每个参与者来协调事务呢,在网络分区的情况下,又能最大程度保证一致性的解决方案呢。此时 Paxos 出现了。
Paxos 算法是 Lamport 于 1990 年提出的一种基于消息传递的一致性算法。由于算法难以理解起初并没有引起人们的重视,Lamport在八年后重新发表,即便如此Paxos算法还是没有得到重视。2006 年 Google 的三篇论文石破天惊,其中的 chubby 锁服务使用Paxos 作为 chubbycell 中的一致性,后来才得到关注。
由于 Paxos 和下文提到的 zookeeper 使用的 ZAB 协议过于相似,详细讲解参照下文,Zookeeper原理
部分
Paxos 是论证了一致性协议的可行性,但是论证的过程据说晦涩难懂,缺少必要的实现细节,而且工程实现难度比较高广为人知实现只有 zk 的实现 zab 协议。然后斯坦福大学RamCloud项目中提出了易实现,易理解的分布式一致性复制协议 Raft。Java,C++,Go 等都有其对应的实现
节点状态
如图raft-2
所示,Raft将时间分为多个 term(任期),term 以连续的整数来标识,每个 term 表示一个选举的开始。例如Follower 节点 1。在 term1 和 term2 连接处的时间,联系不到Leader,将currentTerm编号加1,变成2,进入了到term2任期,在term2的蓝色部分选举完成,绿色部分正常工作。当然一个任期不一定能选出Leader,那么会将currentTerm继续加1,然后继续进行选举,例如图中的t3。选举的原则是,每一轮选举每个选民一张选票,投票的请求先到且选民发现候选人节点的日志id大于等于自己的,就会投票,否者不会投票。获得半数以上的票的节点成为主节点(注意这并不是说选出来的事务id一定是最大的,。例如下图raft-1
a~f六个节点(正方形框里面的数字是选举的轮数term)。在第四轮选举中,a先发出投票,六台机器中,a~e都会投a,即使f不投a,a也会赢得选举。)。如果没有事务id(如刚启动时),就遵循投票请求先来先头。然后Leader将最新的日志复制到各个节点,再对外提供服务。 当然除了这些选举限制,还会有其他的情况。如commit限制等保证,Leader选举成功一定包含所有的commit和log
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有一样的日志。
Google 的粗粒度锁服务 Chubby 的设计开发者 Burrows 曾经说过:“所有一致性协议本质上要么是 Paxos 要么是其变体”。Paxos 虽然解决了分布式系统中,多个节点就某个值达成一致性的通信协议。但是还是引入了其他的问题。由于其每个节点,都可以提议提案,也可以批准提案。当有三个及以上的 proposer 在发送 prepare 请求后,很难有一个 proposer 收到半数以上的回复而不断地执行第一阶段的协议,在这种竞争下,会导致选举速度变慢。
所以 zookeeper 在 paxos 的基础上,提出了 ZAB 协议,本质上是,只有一台机器能提议提案(Proposer),而这台机器的名称称之为 Leader 角色。其他参与者扮演 Acceptor 角色。为了保证 Leader 的健壮性,引入了 Leader 选举机制。
ZAB协议还解决了这些问题
基本名词
核心角色
节点状态
ZAB 协议类似于两阶段提交,客户端有一个写请求过来,例如设置 /my/test
值为 1,Leader 会生成对应的事务提议(proposal)(当前 zxid为 0x5000010 提议的 zxid 为Ox5000011),现将set /my/test 1
(此处为伪代码)写入本地事务日志,然后set /my/test 1
日志同步到所有的follower。follower收到事务 proposal ,将 proposal 写入到事务日志。如果收到半数以上 follower 的回应,那么广播发起 commit 请求。follower 收到 commit 请求后。会将文件中的 zxid ox5000011 应用到内存中。
上面说的是正常的情况。有两种情况。第一种 Leader 写入本地事务日志后,没有发送同步请求,就 down 了。即使选主之后又作为 follower 启动。此时这种还是会日志会丢掉(原因是选出的 leader 无此日志,无法进行同步)。第二种 Leader 发出同步请求,但是还没有 commit 就 down 了。此时这个日志不会丢掉,会同步提交到其他节点中。
现在 5 台 zk 机器依次编号 1~5
1.节点 1 发起投票,第一轮投票先投自己,然后进入 Looking 等待的状态
2.其他的节点(如节点 2 )收到对方的投票信息。节点 2 在 Looking 状态,则将自己的投票结果广播出去(此时走的是上图中左侧的 Looking 分支);如果不在 Looking 状态,则直接告诉节点 1 当前的 Leader 是谁,就不要瞎折腾选举了(此时走的是上图右侧的 Leading/following 分支)
3.此时节点 1,收到了节点 2 的选举结果。如果节点 2 的 zxid 更大,那么清空投票箱,建立新的投票箱,广播自己最新的投票结果。在同一次选举中,如果在收到所有节点的投票结果后,如果投票箱中有一半以上的节点选出了某个节点,那么证明 leader 已经选出来了,投票也就终止了。否则一直循环
zookeeper 的选举,优先比较大 zxid,zxid 最大的节点代表拥有最新的数据。如果没有 zxid,如系统刚刚启动的时候,则比较机器的编号,优先选择编号大的
在选出 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
做出比较后做出不同的同步策略
场景:peerLastZxid
介于minCommittedLogZxid
和maxCommittedLogZxid
间
此种场景出现在,上文提到过的,Leader 发出了同步请求,但是还没有 commit 就 down 了。 leader 会发送 Proposal 数据包,以及 commit 指令数据包。新选出的 leader 继续完成上一任 leader 未完成的工作。
例如此刻Leader提议的缓存队列为 0x20001,0x20002,0x20003,0x20004,此处learn的peerLastZxid为0x20002,Leader会将0x20003和0x20004两个提议同步给learner
此种场景出现在,上文提到过的,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。
peerLastZxid
小于minCommittedLogZxid
或者leader上面没有缓存队列。leader直接使用SNAP命令进行全量同步
当前开源的缓存 kv 系统,大都是 AP 系统,例如设置主从同步集群 redis,master 异步同步到 slave。虽然在 master 停止服务后,slave 会顶上来。但是在 master 写入了数据,但是还没来得及同步到 slave 就 down 了,然后 slave 被选为主节点继续对外提供服务的情况下,会丢失部分数据。这对于要求强一致性的系统来说是不可接受的。例如很多场景下 redis 做分布式锁,有天然的缺陷在里面,如果 master 停止服务,这个锁不很不可靠的,虽然出现的几率很小,但一旦出现,将是致命的错误。
为了实现 CP 的 KV 存储系统,且要兼容现有的 redis 业务。有赞开发了 ZanKV(先已开源ZanRedisDB)。
底层的存储结构是 RocksDB(底层采用 LSM 数据结构)。一个set x=1
的会通过 redis protocol 协议传输,内容会通过 Raft 协议,同步写入到其他的节点的 RocksDB。有了Raft 理论的加持,RocksDB优秀的存储性能,即使遇到网络分区,master 节点 down 掉, slave 节点 down 掉,等一系列异常情况,其都能轻松应对。在扩容方面,系统用选择维护映射表的方式来建立分区和节点的关系,映射表会根据一定的算法并配合灵活的策略生成,来达到方便扩容。具体原理可参见使用开源技术构建有赞分布式KV存储服务
本文从三个方面介绍了一致性,首先是描述分布架构中的核心理论-CAP,以及其简单的证明。第二部分介绍了 CAP 里面协议,重点介绍了 Raft 协议。第三部分,重点介绍了常用的 zookeeper 原理。
为了保证数据 commit 之后不可丢,系统都会采用(WAL write ahead log)(在每次修改数据之前先写操作内容日志,然后再去修改数据。即使修改数据时异常,也可以通过操作内容日志恢复数据)
分布式存储系统中,是假设机器是不稳定,随时都有可能 down 掉的情况下来设计的。也就是说就算机器 down 掉了,用户写入的数据也不能丢,避免单点故障。为此每一份写入的数据,需要在多个副本中同时存放。例如 zk 节点数据复制,etcd 的数据复制。而复制数据给节点又会带来一致性的问题,例如主节点和从节点数据不一致改如何去同步数据。也会带来可用性的问题,如 leader 节点 down 掉,如何快速选主,恢复数据等。好在已有成熟的理论如 Paxos 协议,ZAB 协议 Raft 协议等做为支撑。
参考文章/书籍 《从 paxos 到 Zookeeper 分布式一致性原理与实践》
zookeeper leader 和 learner 的数据同步
[图解分布式协议- Raft ]( http://ifeve.com/raft/)
[Raft协议详解]( https://zhuanlan.zhihu.com/p/27207160)
欢迎关注我们的公众号
Original url: Access
Created at: 2019-09-26 16:14:57
Category: default
Tags: none
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论