这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战
一致性与共识
本章将讨论如何构建容错式分布式系统的相关协议与算法,在上一篇文章中已经阐述了可能会出现的问题
- 网络问题
- 时钟问题
- 节点失效
容错式分布式系统可以将底层的容错系统给提取出来,做成通用的一层抽象,在其他分布式系统中就不需要进行重复构建
而实现容错的最重要的一个部分就是分布式节点之间需要一种协议来达成共识
一致性的保证
如数据库中由于多节点存在,查询和写入数据到达不同节点的时间不一样,可能会出现数据不一致的情况
无法完全保证所有节点在同一瞬间的一致性,因此大部分情况下提供的是最终一致性
在等待一段时间后所有节点都会返回一样的数据,不一致是暂时的,但是需要等待的时间是未知的
——最终一致性
出现问题的时机以及原因是难以发现的,出线的时候往往只是一个契机
越强的一致性意味着更多的消耗,更多的性能的降低或者是容错降低
- 越多的准确性要求、一致性要求都像熵一样,需要外界施加一定的能量才可以减少
分布式一致性模型与数据库的事务性质有点像,都是要解决可能会出现的数据不一致的情况,但是两者出现数据不一致的原因不太一样
- 数据库事务强调的是并发下同一数据临界区带来的数据冲突问题,不受多事务影响数据的正确性
- 分布式系统更多的是强调多副本多节点下如何维护数据的一致性,防止上一章上所说的原因导致系统的数据出现问题,所有的客户端都可以实时对数据进行共同的修改,不受外界因素影响出现服务不一致的问题
我们会介绍以下几个方面
- 线性化
- 事件顺序问题,不同事务之间的因果与全局顺序问题
- 分布式事务以及共识问题
都需要考虑几种不同的分布式系统模型
- 主从复制
- 多主节点
- 无主节点
可线性化
能让系统看上去只有一份一样,让客户端都有一份一样的视图,也被称为强一致性、原子一致性,所有的操作都是原子的,定义比较模糊
注意区分可串行化与可线性化
如果数据是多维的,可线性化不仅仅要保证每个维度是可线性化的,同时需要保证不同维度之间的关系也是没有出错的
具体要求
- 当一个客户端获得返回的一个新值后,其他所有的客户端也应该获得新的值
使用场景
在一些对一致性要求不高的场景实际上是不需要实现可线性化的,不会造成多大的实质性的影响
但是某一些场景就需要强一致性,否则就会出现很大的问题,如:
- 分布式锁
- 向外的唯一性约束:ID、主节点
- 对信息关系有要求的场景:发送邮件的时间信息与邮件内容的关系应该是正确的(时间信息较小,处理会更快,不能出现时间信息与邮件内容对不上的情况)
如何实现
-
只使用一个节点:无法实现容错
-
复制机制
-
主从复制(部分可支持可线性化)
- 只有主节点进行数据写入,如果是写入后只能从完成同步的节点获取数据,能么是可线性化的,会损失一部分的可用性
-
共识算法(可线性化)
- 经过专门的设计后是可以支持可线性化的
-
多主复制(不可线性化)
- 每个节点写入的东西以及顺序都不一样,没有办法保证客户端能获取一样的数据,本身就需要额外的机制进行数据的同步以及解决冲突
-
无主复制(可能不可线性化)
- 不讨论
-
那么代价是什么
CAP
也即一致性、可用性、分区容错性
- 更加准确的称呼是,再出现网络问题的情况下是选择一致性还是可用性,分区容错性实际上在分布式系统中是一个前提
CAP有着较大的局限性,其考虑的一致性模型仅限于强一致性,而造成在可用性与一致性中进行选择的原因也只有网络问题
但是在我的理解中,一致性与可用性本身也是有各种程度的,一致性模型也也不仅限于强一致性模型。而一致性的程度就可以指各种一致性模型,可用性和一致性的程度可以相互协调
实际让我们放弃保证强一致性的是性能的损耗
顺序保证
其实我们想保证的一致性在大部分情况下是要维护数据之间的因果关系
而这种因果关系实际上也是数据库系统中事务隔离级别所要维护的东西,只不过两者出现这种问题的原因不太一样,一个是高并发多事务带来的问题,一个是多节点之间的网络时钟等问题
因果顺序并非全序
需要注意一个是可比较的,偏序与全序,可线性化、可串行化是全序的
在分布式系统中这两种关系也有比较明显的体现
- 全序:所有的事件操作我们都是可以指出哪一个在前哪一个在后,进行时间上的比较
- 偏序:同时发生的两个事件是有因果关系的因此可以进行比较的,否则是不可比较的
也就是说在可线性化中所有的操作都可以看作是没有多节点、没有并发的
而在并发、多节点的情况下需要进行偏序分支的合并
可线性化确实可以保证因果顺序,但是对性能会有较大的损耗,还存在着其他的一致性模型,因果一致性可以保证因果顺序的同时增加了系统的可用性(容错)
如何获取事件之间的因果关系
进行操作的时候需要知道当前操作是基于哪一个版本进行的,也就是操作的时候需要客户端将自己的数据版本进行回传,通过版本号的顺序可以知道时间的因果关系
序列发生器
主从复制
可以由系统提供一个简单的单节点单调增序号生成器,或者是一个特定的算法,但是需要注意的是如何处理单节点的失效以及网络问题
非因果序列发生器(多主节点、无主节点)
如果不存在主节点进行统一的序号分配,如多主节点系统或无主数据库,可以采用如下方式进行生成
- 每个节点都会产生自己的序号,但是每个节点产生的序号必须有自己的规则,相互之间不能冲突
- 时间戳或物理时钟进行标记,需要足够高的分辨率,或者采用LWW
- 每个节点拥有自己的区间
但是以上方式都没有办法进行跨节点的因果关系维护,但是我们需要的是整个系统的因果关系
Lamport时间戳(因果序列发生器)
可以解决上述非因果序列发生器的跨节点问题
- 每个节点有自己唯一的标识符,并且拥有记录自己请求数的计数器
- 请求会获得标识符与计数,再进行访问的时候需要回传这两个参数
在进行跨界点访问的时候,节点计数会变成请求中技术或本节点计数中较大的那一个,并且加1,并且返回的也是这个最大值
img
需要注意的是,Lamport时间戳可以保证原有的因果顺序,但是不意味着可以由Lamport时间戳进行判断两个事件是否具有因果关系
真的OK了吗?
由于操作本身是有时间长度的,有可能在一个请求还没有返回的时候,另外一个客户端发起了一样的请求,这个时候计数器可能并没有发生更新,导致数据出错
在这种情况下可能就需要其他节点是否在进行同样的操作
全序关系广播
在单节点主从复制上保证全序性是非常简单的,但是主从复制意味着对单节点有着较大的负担,需要解决两个问题
- 提高系统吞吐量
- 当主节点初出现故障的时候,如何进行故障切换
全序关系广播和主从复制很相似,但是希望能够解决上述提出的这些问题
全序关系广播指的是节点之间交换消息的某种协议,需要满足两个基本安全属性,无论节点或者网络出现了问题
- 可靠发送:消息如果没有丢失,发送到一个节点就是发送到了所有节点
- 严格有序:消息总是以相同的顺序发送给每一个节点
在主从复制中,写消息如果使用了全序关系广播,就能够保证每一个副本中的数据最终都是一致的,但是可能会有时间上的延迟
可线性化并不等同于全序关系广播,可线性化本身还要求了时间上的限制,而全序关系广播只是最终会保证副本的一致性,但是对于时间上而言并不保证,也被称为时间线一致性(也被称为顺序一致性)
可以在此基础上增加时间的限制,如
- 需要等待所有的节点都进行返回之后,才完成写入,有点像etcd的思路
- 采用日志的形式向外提供线性化服务,节点可以通过该日志查询当前读取所处的位置,等到之前的所有写入操作完成之后再进行读取,与ZooKeeper思路有点像
- 只从完成了同步的节点中进行数据的读取
实际上可线性化、全序关系广播以及共识问题是可以相互转换的,解决了一个意味着解决了另外问题,可以以彼此为基础实现自己的需求,说到底就是如何让分布式系统节点内部在有网络与其他风险的情况下向外提供一致的数据(共识)
分布式事务与共识
实际上就是让节点就某件事情达成一致,如
- 主节点选举
- 原子事务的提交
原子事务提交与两阶段提交协议
原子性对维护数据一致性以及系统安全很重要,防止突然的系统失败导致数据对不上、出错
- 在数据库中,事务的提交先需要进行事务的持久化,然后进行日志的追加写入,在日志未完成写入但是完成了事务持久化可以从日志中进行恢复,如果日志完成了写入那么该事务被认为完成了提交
那么在多节点分布式系统中应该怎么处理呢
- 如果只进行一次指令的发送,没有办法保证所有节点可以完成提交,会导致数据不一致,也可以进行补偿事务,但是这些事物之间本就应该是独立的
两阶段提交协议
实际上就是将事务划分为两个阶段
- 第一阶段:准备,协调者询问各个节点是否可以进行该事务的操作,如果ok则进入下一个阶段,否则向所有节点发送放弃请求
- 第二阶段:执行,协调者像每个节点发送执行命令
其中可能会出现的问题
- 未执行新的事务,如果有其他节点出问题,并不会影响协调者和其他节点
- 协调者向每个节点发送准备请求,如果节点ok,就要将该事务处于一个无论如何之后也要执行的地位(如数据库中将事务写入磁盘进行持久化),否则返回失败
- 当协调者收集到所有答复的时候,决定是否放弃或者执行
- 当协调者决定执行后,节点不可以以任何借口放弃执行
- 协调者如果在发送准备之后故障,则执行节点必须等到协调者回复发送指令才可以继续放弃或者执行
- 如果在协调者发送执行命令之后,节点出现故障,没有收到执行指令,可以在节点之间进行通信查看状态
实际上二阶段提交协议就是将多节点转换为了单节点上的原子操作
性能瓶颈
- 单协调者本身会有瓶颈
- 单节点阻塞问题,一个节点的失效延迟,会导致整个系统的等待,阻塞后续其他操作
- 实际操作中并不是所有的故障(协调者故障、节点故障)都能够保证数据的完全恢复、恢复后数据的正确性,会导致整个系统的可用性以及准确性降低
实践中的分布式事务
虽然2PC能够带来安全性上很大的提升,但是其要求的可靠性很难保证,并且带有协调节点的性能瓶颈
分布式事务本身有多种定义
- 数据库内部的分布式事务:支持数据库跨节点的内部事务,所有节点运行的软件是一样的
- 异构分布的分布式事务:节点之间使用的系统不一定一致
基于数据库内部的分布式事相比于数据库内部的非分布式事务效率要低得多,但是分布式的使用场景不仅限于数据库内部,同时内部的系统往往可以进行一定程度的优化
异构分布式系统面临着更多的挑战,需要进行多个系统的兼容
Exactly-once消息处理
可以通过增加独立的消息队列系统,利用自动提交成功后信息标记消息队列才算是执行完毕
也就说任何系统外都可以增加这么个系统,实现兼容
-
由于发送消息和该节点数据库事务的执行都可能出现问题,任何一个出现问题都需要停止全部,这个时候有可能消息已经发出去了,如果系统可以接受以下情况,则没问题
- 幂等性,多次执行同一个消息不会出错
- 可以全部回滚,包括发送的消息
前提是系统使用的都是原子提交协议
后续会提到
XA交易
异构环境下实施两阶段提交协议的工业标准,目前大部分传统关系型数据库、消息队列都支持XA
并不是一个具体的协议,而是一个C API
我理解就是一个API库,可以通过这个api库进行分布式事务的所有操作,比如说判断当前操作是否是分布式事务、协调者通过回调执行下一步通知节点执行或提交、崩溃后进行数据恢复
将分布式事务拆分成两个部分,事务管理以及本地资源管理,由XA来实现分布式相关的功能接口,实际上2PC协议也是可以通过XA的方式进行实现的
99941-20160805193216309-1768289491
在实际的实现中,协调者实际上也是一个类似数据库的实现,需要记录投票的结果详情,有着自己的使用限制
- 数据库的复制备份可以提高系统的可用性,避免单点失效问题
- 协调者的存在使得服务器之间并不是地位相等的,拥有协调者的服务器并不能随意的删除或者增加,因此服务器不再是无状态的
支持容错的共识
节点就某一些事情达成共识,整个过程为一个节点、多个节点发起某些提议值,由共识算法确认最后的取值
性质:
- 协商一致性:即当结果出来之后,所有节点都必须接受相同的决议
- 诚实性:所有节点不可反悔,并且对一个协议不能有两次决定
- 合法性:决定的最终值必须是来自某个节点的提议
- 可终止性:节点如果不崩溃最终一定可以达成决议,引入了容错的思想
共识算法与全序广播
VSR、Paxos、Raft、Zab
他们决定一系列的值,然后采用全序关系广播算法,在前文提到过,全序关系广播有两个要求
- 可靠性:消息没有丢失,则发送到一个节点就是发送到所有节点
- 严格有序:发送给所有节点的消息顺序都是一致的
在严格有序中,可以将决定消息顺序的过程看作是达成共识的过程,多个消息的顺序实际上就是多轮共识达成的共识结果
由共识算法性质中的协商一致性保证了严格有序,由诚实性、可终止性、合法性保证了可靠
那么我们可以说,全序关系广播符合共识算法的要求,也即共识算法可以达成全序关系广播
这体现了一致性与共识问题的本源性
主从复制与共识
其中用到共识算法的地方是如何选举主节点,此处所用到的共识算法与全序关系广播挂钩,用于解决主从复制中的主节点选举
此外我们知道全序关系广播与主从复制十分类似(在目前的篇幅中),那么上面那句话可以变成
用主从复制选举主节点,已以解决主从复制中的主节点选举问题
变成自己解决自己的嵌套逻辑了
Epoch与Quorum
在共识协议(如全序关系广播)中都存在着某种形式的主节点,
相比主从复制中由人工设置不变的主节点,共识协议弱化了主节点:
-
协议中有一个世代编号,每一个世代中需保证主节点的唯一性
-
因此共识协议中是既包含了两次投票
- 选举主节点
- 对决议进行投票
多主节点时:
每次主节点失效引起的新一轮选举会有一个递增的epoch号,出现多个主节点时,epoch号更大的主节点获胜
作出决议时:
主节点需要征询每一个从节点的意见,这个过程就包括了检查是否有更高epoch的节点,以及多数节点的选择结果
整个过程很像两阶段提交协议,但是2PC是不涉及主节点的选举的,并且2PC要求每个节点都给出一样的回复才继续进行
共识的局限性
- 在达成一致性决议之前,有大量的工作是同步的,性能不太行
- 需要多个节点才能进行容错,需要保证系统中二分之一以上的节点在正常运转
- 由于网络的原因,可能会多次判断主节点失效而开始主节点的选举,耗费大量不必要的时间
应用
ZooKeeper强的