一致性与共识
分布式系统所引发的很多问题其实都是数据资源异常所导致,比如数据丢失,数据重复等等。引起这些数据问题的原因也有很多种比如时钟问题,导致数据写入被覆盖丢失等等。本章作者把解决上述问题(前面几章的问题)做了方法论的抽象---分布式共识理论。感觉有点像黑暗森林,当然更贴切一点的描述是拜占庭将军。各个系统可以互相通信获取对方的状态,数据等等,但如果通信过程中出现种种问题,那就会造成作者所说的一系列分布式问题。如果能达成各个服务的共识,那出现异常时,会降低对正常业务的影响。
一致性保证
最终一致性虽然在性能上面牺牲较少。但在数据收敛达到最终一致前,业务会出现读取不一致情况。同时系统是无法告知何时收敛,因此作者认为在分布式系统中,最终一致性是一个很弱的保证,可以用在特殊场景,但要达到数据读写的一致性,保证上层逻辑更为简单,不容易出错,则需要强一致性。因此第九章讨论的一致性指的就是强一致性(可线性化)。
可线性化
可线性化的基本思想是其目标是使多副本对外看起来好像是单副本,然后所有操作以原子方式运行,就像一个单线程程序操作变量一样。
线性化依赖条件
- 加锁与主节点选举
对于数据加锁操作,是可以保证数据的一致性。在分布式系统中,涉及到主节点(有且只有一个主节点)选举的场景中,是需要线性化也就是强一致性。目的很简单就是整个系统达成共同认知,对主节点的认可。因此在加锁这个过程中,锁是必须满足可线性化。
- 约束与唯一性保证
数据库和一些业务场景需要进行约束。比如在数据库中用户名必须是唯一的,这是一个约束;那么注册用户的时候,相当于对这个用户名进行加锁,这是唯一性的保证。
- 跨通道的时间依赖
作者提供了一种假设,个人感觉就是最终一致性的场景,数据在未完成强一致性前,那么客户端不会主动查数据。也就是作者抽象出来的如果存在多通道竞争,强一致性是无法保证的。以作者的假设为例,数据在完成强制一致性前,如果有其他的通道通知客户端去查询,那么数据肯定是不一致的。这个角度是从客户端的立场所描述的,也就是要客户端自己保证能读到自己刚写入的数据。
实现线性化系统
最简单的方式就是保证只用到一个数据副本。作者梳理了主流的复制方案
- 主从复制
在部分场景下是可以满足的。比如主节点写入数据后,从节点同步更新数据,保证主从数据一致后,写入流程结束。
- 共识算法
所有节点做出一致决定,且不可撤销。这种是可以满足数据的强一致性.
- 多主复制
多个主节点都允许写入数据,会造成写入冲突,显然是不能保证线性化
- 无主复制
无法保证线性化。作者举了一个例子,虽然能满足w+r>n的quonum配置,但是读取存在一定可能会读到旧值。
CAP
如果要保证一致性,那么要么服务不可用,要么不存在分区(也就是不存在分布式系统)。所以在分布式系统中,CAP理论基本上是一个理想的模型,在现实场景中满足线性化没有太大的实际价值。以多核cpu的计算机模型来看,每个cpu都有自己的独立的cache和寄存器,在数据同步过程中,CAP理论是不能满足线性化。这块儿作者是这么解读的:
每个CPU核都有自己独立的cac 和寄存器。内存访问首先进cache 系统,所有修改默认会异步地刷新到主存。由于访问cac 比访问主存要快得多[45 ,所以这样的异步刷新特性对于现代CPU的性能至关重要。但是, 这就导致出现了多个数据副本( 个在主存,另外几个在不同级别的cac 中), 而副本更新是异步方式,无法保证线性化
顺序保证
可线性化的数据对外呈现单副本,能保障读的一致性。对数据写操作而言,需要保证原子性。在保障原子性的前提条件下,每次的操作便会有了顺序。
因果一致性
操作是服从因果关系所规定的顺序那就符合因果一致性。但是因果一致性并不是全序。
全序关系支持任何两个元素之间进行比较,即对于任意两个元素,总是可以指出哪个更大,哪个更小。例如,自然数符合全序关系,随便给出两个数字比如 ,都可以进行比较。
不符合全序的原因很简单。比如在事件c依赖于事件a和事件b,但是事件a和事件b是并行发生,那么事件a和事件b不能进行比较不符合全序的定义。书中这一块儿主要是对一些概念进行厘清。
可线性化和因果一致性
可线性化天然保证了因果一致性,但是可线性化降低了性能和可用性。如果系统只要求因果顺序,那么没必要采用数据的强一致性。实际处理中,需要理清操作的因果关系。作者举了个第五章的例子”检测并发写“,可以采用版本向量的方式进行解决
保障序列的方式
- 序列号排序
这里的序列号是一个泛指,不管是时间戳还是根据因果关系进行排列的序号都可以。
如果系统中存在一个主节点,类似主从复制数据库。由主节点为每个操作定义一个计数器,那么从节点在进行同步的同时,按照操作顺序执行,结果肯定会满足因果一致性。
如果系统中不存在这样一个主节点比如无主或者多主。那么可以采用下面这几种方式:
-
时间戳加到操作上面,但这个前提是需要保证时钟同步。但实际情况可能会受到时钟偏移影响,后面的操作分配的时间戳比前面的小,导致因果顺序乱掉。
-
预先分配节点的序号的范围,比如节点1的序号范围是1-10000,以此类推节点2、节点3等。同样如果前面一个操作分配到序列号较高的节点,后面一个操作则被路由到序列号较低的节点,同样会导致因果顺序乱掉
-
节点产生自己独立的序号,保证节点之间的序号不重复。比如奇偶分布。如果奇数节点处理的较慢,偶数处理的快,那么就出现前一个操作是偶数数字较大,后一个操作分配到奇数节点数字较小,因果顺序无法保证
- Lamport 时间戳
Lamport时间戳的原理不复杂,简单的说就是客户端和节点都会维护序号,客户端在请求过程中,发现节点的序号自己的大,会换成节点的序号;同样如果节点发现客户端的序号比自己的大,会更新自己的序号。这种做法有点类似与版本向量。
全序广播
保障了因果一致的全序关系后,但并不能保障分布式系统中出现的一些问题。比如注册用户名,节点A和节点B都在处理,但是两个节点并不知道对方收到请求的时间戳更小,因此还会造成数据冲突。但是在分布式系统中,要获知所有节点操作难度非常大。单主节点的分布式系统是可以避免,因为有主节点一个在写入,是很容易知道全序关系。针对多主或者无主的系统,是需要进行全序广播。
全序关系广播通常指节点之间交换消息的某种协议。下面是个非正式的定义,它要满足两个基本安全属性:
可靠发送---没有消息丢失,如果消息发送到了某一个节点,则它一定能发送到所有节点。
严格有序---消息总是以相同的顺序发送给每个节点。
全序广播实际上也是一种共识的传递,收到消息后,节点必须按照共识来处理数据。比较常见的就是数据库的通过日志进行复制,日志上记录的操作顺序也可以理解成是一种全序关系是一种共识,收到日志的从节点必须按照这个操作顺序执行。
分布式事务
分布式事务这一部分基本上没啥可记录的。主要是一些概念和事务解释的内容。比如作者描述了2pc 然后出现协调组故障引出了3pc。以此类推分布式系统的XA算法等等。网上关于分布式事务文章不要太多,略过了。
共识
正如作者书中所言,经过大部分章节内容的铺垫(复制,分区,事务,失效模型,线性化,全序等等),分布式系统最核心的要素就是共识。
共识就是让节点就某项提议达成一致。这包括了正常流程的处理,也包括异常容错的处理。共识算法需要满足以下性质:
协商一致--所有的节点都接受相同的决议。
诚实性--所有节点不能反悔,即对一项提议不能有两次决定。
合法性--如果决定了值v,则v一定是由某节点所提议的。
可终止性--节点如果不崩溃最终一定可以达成决议。
前两个属性是共识的核心思想。
第三个属性是系统数据的合法性。
第四个属性则是系统的容错。这里面其实可以这么理解。只要系统节点崩溃个数没有到容忍失败次数,或者这么理解加一个限制条件,发生崩愤或者不可用的节点数必须小于半数节点,那最终是能达到共识的。比如分布式系统的那些选主算法。
共识算法与全序广播
全序广播的特点是消息按照相同的顺序发送到所有节点,有且只有一次。那么在实际分布式系统中,共识算法就需要全序广播持续多轮的进行。每一轮的广播的消息就相当于一次共识的决定。针对共识算法的性质,那么可以解读为:由于协商一致,所有节点都会以相同顺序发送相同消息;由于诚实性,收到消息也就是收到了共识,不能反悔不去执行;由于合法性,消息本身必须是系统中节点发出来的;由于可终止性,共识不会丢失会达到每一个活着的节点。
主从复制与共识
同样主从复制也满足共识,所有写入操作由主节点进行,主节点以相同顺序记录操作然后同步给从节点,从节点按照共识去执行。主节点的选举也是符合共识算法。
Epoch和quorum
Epoch是选主算法里的轮次或者世代的编号,这一部分基本上是从上一部分从节点选主算法引申而来进一步阐释选主与共识算法。同分布式事务算法章节,网上文章又详细又多。略过。
共识的局限性
- 存在丢数据的可能。比如故障切换的时候,这一时刻数据可能会丢失
- 共识算法对系统的要求是多个节点才能运行。比如5个节点只能容忍2个节点挂掉,要形成多数。
- 拥有投票权的节点,动态添加或者动态删除,都会引起共识的混乱
- 节点失效是依靠超时机制进行,在网络不确定的环境中,比如跨区域分布式系统,会出现共识混乱
其实就是分布式系统的局限性。
成员与协调服务
这一部分其实是共识算法的应用。作者用zk等分布式服务中心进行举例。除了实现全序广播外,还有一些特性在构建分布式系统非常重要:线性化的原子操作(比如zk的加锁),操作全序(zk帮助业务服务进行协调),故障检测(zk帮助业务服务进行切换),更改通知(业务服务配置变更后,zk去通知订阅者)。当然还有任务调度,服务发现等等。
全书至此,理论部分已经完成,后面三个章节将会在实际场景中,描述如何构建系统。