共识与一致性

此文为缝合的结果,来源见参考。
本文为学习所用,如有侵权请告知本人。

共识与一致性

共识描述了分布式系统中多个节点之间,彼此对某个状态达成一致结果的过程。 在实践中,要保障系统满足不同程度的一致性,核心过程往往需要通过共识算法来达成。

共识算法解决的是对某个提案(proposal)大家达成一致意见的过程。提案的含义在分布式系统中十分宽泛,如多个事件发生的顺序、某个键对应的值、谁是领导……等等。可以认为任何可以达成一致的信息都是一个提案。对于分布式系统来讲,各个节点通常都是相同的确定性状态机模型(又称为状态机复制问题,state-machine replication),从相同初始状态开始接收相同顺序的指令,则可以保证相同的结果状态。因此,系统中多个节点最关键的是对多个事件的顺序进行共识,即排序。

一致性往往指分布式系统中多个副本对外呈现的数据的状态。如后面提到的顺序一致性、线性一致性等,描述了多个节点对数据状态的维护能力。

共识则描述了分布式系统中多个节点之间,彼此对某个状态达成一致结果的过程。

因此,一致性描述的是结果状态共识则是一种手段达成某种共识并不意味着就保障了一致性(这里的一致性指强一致性)。只能说共识机制,能够实现某种程度上的一致性。

什么是一致性?

在谈到一致性这个词时,你会想到CAP理论的 consistency,或者 ACID 中的 consistency,或者 cache 一致性协议的 coherence,还是 Raft/Paxos 中的 consensus?

一致性大概是分布式系统中最容易造成困惑的概念之一,因为它已经被用烂了。在很多中文文献中,都将consistency,coherence,consensus 三个单词统一翻译为”一致性”。因此在谈一致性之前,我认为有必要对这几个概念做一个区分:

Consensus

准确的翻译是共识,关注的是多个提议者达成共识的过程。比如 Paxos,Raft 共识算法本质上解决的是如何在分布式系统下保证所有节点对某个结果达成共识,其中需要考虑节点宕机,网络时延,网络分区等分布式中各种问题。共识算法通常应用在复制状态机中,比如 etcd,zookeeper,用于构建高可用容错系统。在这种应用情景下,Raft/Paxos 共识算法被用来确保各个复制状态机(节点)的日志是一致的。类似的,区块链中非常重要的一部分也是共识算法,但通常使用是 POW(Proof of Work) 或 POS(Proof of Stake),这类共识算法通常适合用在公网,去中心化的情形下。

Coherence

Coherence 通只出现在 Cache Coherence 一词中,作为”缓存一致性”被提出。我们知道现代的多核 CPU Cache 都是多层结构,通常每个 CPU Core 都有一个私有的 LB/SB, L1, L2 级 Cache,多个 CPU Core 共享一个 L3 Cache。比如 CPU1 修改了全局变量 g 并写入了 CPU1 L1 Cache,此时 CPU2 要读取变量 g,然而 CPU2 L1 Cache 中的 g 仍然是旧值,这里就需要 Cache Coherence (以下简称 CC)机制来保证 CPU2 读取到的一定是 g 的最新值。因此,CC 的本质是让多组 Cache 看起来就像一组 Cache 一样。现代 CPU 都已经实现了 CC,通常程序员也不需要关心 CC 的具体实现策略,目前大部分 CPU 采用的是基于 MESI(Modified-Shared-Invalid-Exclusive) 协议的 CC,这里有一篇参考文章

解释完了Consensus 和 Coherence,剩下 ACID 和 CAP,两者的 C 都叫做 Consistency,你可能以为这两者应该就是我们常提到的分布式中的一致性了吧。其实并不是,两者也是完全不同的概念。

ACID Consistency

ACID 中的一致性是指数据库的一致性约束,ACID 一致性完全与数据库规则相关,包括约束,级联,触发器等。在事务开始之前和事务结束以后,都必须遵守这些不变量,保证数据库的完整性不被破坏,因此 ACID 中的 C 表示数据库执行事务前后状态的一致性,防止非法事务导致数据库被破坏。比如银行系统 A 和 B 两个账户的余额总和为 100,那么无论 A, B 之间怎么转换,这个余额和是不变,前后一致的。

CAP Consistency

CAP 理论中的 C 也就是我们常说的分布式系统中的一致性,更确切地说,指的是分布式一致性中的一种: 线性一致性(Linearizability),也叫做原子一致性(Atomic consistency)。

谈到 CAP,和一致性一样,CAP 理论也是个被滥用的词汇,关于 CAP 的正确定义可参考cap faq

简单总结CAP理论,在一个分布式的存储系统中,只能Consistency, Availability, Partition三选二,而由于在大规模的分布式系统中,网络不可靠几乎是不可避免的,即Partition网络分区容忍是必选项,因此对系统设计需要在AP(舍弃一致性,可能读出不一致)和CP(发生网络分区时系统不可用,即无法写入)之间权衡。

很多时候我们会用 CAP 模型去评估一个分布式系统,但这篇文章会告诉你 CAP 理论的局限性,因为 CAP 理论是一个被过度简化的理论(一个只能读写单个数据的寄存器),按照 CAP 理论,很多系统包括 MongoDB,ZooKeeper 既不满足一致性(线性一致性),也不满足可用性(任意一个工作中的节点都要可以处理请求),但这并不意味着它们不是优秀的系统,而是 CAP 定理本身并没有考虑处理延迟(为了做到强一致性的事务同步开销),硬件故障,可读/可写的部分可用性等。这里不再展开,推荐阅读原文。

正因为 CAP 中对C和A的定义过度理想化,后来又有人提出了BASE 理论,即基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventual Consistency)。BASE的核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方法来使系统达到最终一致性(关于最终一致性,这篇亚马逊CTO Werner Vogels的博客值得一读)。显然,最终一致性弱于 CAP 中的 线性一致性。很多分布式系统都是基于 BASE 中的”基本可用”和”最终一致性”来实现的,比如 MySQL/PostgreSQL Replication 异步复制。

以下我们不再纠结CAP理论本身,而聚焦于分布式一致性的定义和基本概念。

分布式“顺序”的概念

要理解分布式一致性,先来谈谈分布式中几个必要的概念: 时间,事件,和顺序。

分布式系统的时间主要分为物理时间和逻辑时间两种,物理时间是指程序能够直接获取到的 OS 时间,在分布式系统中,由于光速有限,你永远无法同步两台计算机的时间。想要在分布式中仅依靠物理时间来决定事件的客观先后顺序是不可能的。因此分布式系统中,通常使用逻辑时间来处理事件的先后关系。

分布式中的事件不是瞬间的概念,它有一个起始和结束时间,因此不同进程 发起的事件可能发生重叠,对于发生重叠的事件,我们说它们是并发执行的,在物理时间上是不分先后的。

理想情况下,我们希望整个分布式系统中的所有事件都有先后顺序,这种顺序就是全序,即整个事件集合中任意两个事件都有先后顺序。但 1.物理时间是很难同步的,2. 网络是异步的,因此在分布式系统中,想要维持全序是非常困难的。不同的节点对两个事件谁先发生可能具有不同的看法,并且大部分时候我只需要知道偏序关系,用于确保因果关系。所谓偏序关系是指:

  1. 如果a和b是同一个进程中的事件,并且a在b前面发生,那么 a->b
  2. 如果a代表了某个进程的消息发送事件,b代表另一进程中针对这同一个消息的接收事件,那么a->b
  3. 如果 a->b且b->c,那么a->c (传递性)

逻辑时间中的 Lamport Clock和Vector Clock等都可以用于建立偏序关系。

一致性的分类

强一致性

相比较严格一致性,强一致性对于时间上的限制不再那么苛刻。当满足强一致性时,需要满足以下要求 :

当分布式系统中更新操作完成之后,任何多个进程或线程,访问系统都会获得最新的值。

强一致性中分为两种情况 :

  • 顺序一致性
  • 线性一致性
顺序一致性

Leslie Lamport 在1979年提出了Sequential Consistency。

顺序一致性是指任何执行结果都是相同的,就好像所有进程对数据存储的读、写操作是按某种序列顺序执行的,并且每个进程的操作按照程序所指定的顺序出现在这个序列中 。

什么是顺序一致性 ? 我们这里举一个例子

假设现在存在两个线程A和B,有两个初始值x=y=0,两个线程分别执行以下两条指令。

线程A线程B
X=1Y=1
ThreadA=YThreadB=X

由于多线程的执行顺序并不一定,所以可能出现几种可能。

情况1情况2情况3
X=1Y=1X=1
ThreadA=YThreadB=XY=1
Y=1X=1ThreadA=X
ThreadB=XThread=YThreadB=Y
结果 : ThreadA = 0 ThreadB = 1结果 : ThreadA = 1 ThreadB = 0结果 : ThreadA = 1 ThreadB = 1

我们能够看到,上述过程虽然是正常执行,但是由于多个线程的执行顺序不同,最终结果也发生了变化。

**所谓的顺序一致性,**其实就是规定了一下两个条件:
(1)单个线程内部的指令都是按照程序规定的顺序(program order)执行的
(2)多个线程之间的执行顺序可以是任意的,但是所有线程所看见的整个程序的总体执行顺序都是一样的

可以将满足顺序一致性的分布式系统想象成一个调度器,将多个并发线程的请求看做是多个FIFO请求队列;调度器只能从队列首部逐条取出请求并执行,调度器不能保证多个队列的调度顺序,但是调度器保证所有服务节点对队列的调度顺序是相同的。

线性一致性(Linearizability)

也叫做strong consistency或者atomic consistency,于 1987年提出,线性一致性强于顺序一致性,是程序能实现的最高的一致性模型,也是分布式系统用户最期望的一致性。 线性一致性要求 Server 在执行 Operations 时需要满足以下三点:

  1. 瞬间完成(或者原子性)
  2. 任何 Operation 都必须在其发起调用,收到响应之间被执行。
  3. 一旦某个Operatio执行之后,所有后续的Operations都可以观测被操作对象最新的值(如果是写操作的话)。

如下例,线段表示一个Operation,左端点表示请求时间点,右端点表示相应时间点:
在这里插入图片描述

先下结论,上图表示的行为满足线性一致。

对于同一个对象 x,其初始值为 1,客户端 ABCD 并发地进行了请求,按照真实时间(real-time)顺序,各个事件的发生顺序如上图所示。对于任意一次请求都需要一段时间才能完成,例如 A,“x R() A” 到 “x Ok(1) A” 之间的那条线段就代表那次请求花费的时间段,而请求中的读操作在 Server 上的执行时间是很短的,相对于整个请求可以认为瞬间,读操作表示为点,并且在该线段上。线性一致性中没有规定读操作发生的时刻,也就说该点可以在线段上的任意位置,可以在中点,也可以在最后,当然在最开始也无妨。

第一点和第二点解释的差不多了,下面说第三点。

反映出“最新”的值?我觉得三点中最难理解就是它了。先不急于对“最新”下定义,来看看上图中 x 所有可能的值,显然只有 1 和 2。四个次请求中只有 B 进行了写请求,改变了 x 的值,我们从 B 着手分析,明确 x 在各个时刻的值。由于不能确定 B 的 W(写操作)在哪个时刻发生,能确定的只有一个区间,因此可以引入上下限的概念。对于 x=1,它的上下限为开始到事件“x W(2) B”,在这个范围内所有的读操作必定读到 1。对于 x=2,它的上下限为 事件“x Ok() B” 到结束,在这个范围内所有的读操作必定读到 2。那么“x W(2) B”到“x Ok() B”这段范围,x 的值是什么?1 或者 2。由此可以将 x 分为三个阶段,各阶段"最新"的值如下图所示:
在这里插入图片描述

清楚了 x 的变化后理解例子中 A C D 的读到结果就很容易了。

最后返回的 D 读到了 1,看起来是 “stale read”,其实并不是,它仍满足线性一致性。D 请求横跨了三个阶段,而读可能发生在任意时刻,所以 1 或 2 都行。同理,A 读到的值也可以是 2。C 就不太一样了,C 只有读到了 2 才能满足线性一致。因为 “x R() C” 发生在 “x Ok() B” 之后(happen before [3]),可以推出 R 发生在 W 之后,那么 R 一定得读到 W 完成之后的结果:2。

如果说顺序一致性只保证单个客户端多个请求的执行顺序,线性一致性还保证了多个客户端请求的执行先后顺序;即,如果Client B的请求b发起时间晚于Client A的请求a相应时间,那么Client B的请求b一定晚于Client A的请求a,顺序一致不能给出该保证。

弱一致性

弱一致性是指系统并不保证对于从节点的后续访问都会返回最新的更新的值。系统在数据成功写入主节点之后,不保证可以立即从其他节点读到最新写入的值,也不会具体承诺多久读到。但是会尽可能保证在某个时间级别(秒级)之后,让数据达到一致性状态。也就是说,如果能容忍后续的部分或者全部访问不到,则是弱一致性。

最终一致性

最终一致性是弱一致性的特定形式,最终一致性保证主从节点之间数据不一致的状态只是暂时的。

读写一致性

手机刷虎扑的时候经常遇到,回复某人的帖子然后想马上查看,但我刚提交的回复可能尚未到达从库,看起来好像是刚提交的数据丢失了,很不爽。

在这种情况下,我们需要读写一致性,也称为读己之写一致性。**它可以保证,如果用户刷新页面,他们总会看到自己刚提交的任何更新。**它不会对其他用户的写入做出承诺,其他用户的更新可能稍等才会看到,但它保证用户自己提交的数据能马上被自己看到。

如何实现读写一致性?

最简单的方案,**对于某些特定的内容,都从主库读。**举个例子,知乎个人主页信息只能由用户本人编辑,而不能由其他人编辑。因此,永远从主库读取用户自己的个人主页,从从库读取其他用户的个人主页。

如果应用中的大部分内容都可能被用户编辑,那这种方法就没用了。在这种情况下可以使用其他标准来决定是否从主库读取,例如可以记录每个用户最后一次写入主库的时间,一分钟内都从主库读,同时监控从库的最后同步时间,任何超过一分钟没有更新的从库不响应查询。

还有一种更好的方法是,客户端可以在本地记住最近一次写入的时间戳,发起请求时带着此时间戳。从库提供任何查询服务前,需确保该时间戳前的变更都已经同步到了本从库中。如果当前从库不够新,则可以从另一个从库读,或者等待从库追赶上来。

单调读

用户从某从库查询到了一条记录,再次刷新后发现此记录不见了,就像遇到时光倒流。如果用户从不同从库进行多次读取,就可能发生这种情况。

单调读可以保证这种异常不会发生。单调读意味着如果一个用户进行多次读取时,绝对不会遇到时光倒流,即如果先前读取到较新的数据,后续读取不会得到更旧的数据。单调读比强一致性更弱,比最终一致性更强。

实现单调读取的一种方式是确保每个用户总是从同一个节点进行读取(不同的用户可以从不同的节点读取),比如可以基于用户ID的哈希值来选择节点,而不是随机选择节点。

*因果一致性

在本文中阐述因果一致性可能并不是一个很好的时机,因为它往往发生在分区(也称为分片)的分布式数据库中。

分区后,每个节点并不包含全部数据。不同的节点独立运行,因此不存在**全局写入顺序。**如果用户A提交一个问题,用户B提交了回答。问题写入了节点A,回答写入了节点B。因为同步延迟,发起查询的用户可能会先看到回答,再看到问题。

为了防止这种异常,需要另一种类型的保证:因果一致性。 即如果一系列写入按某个逻辑顺序发生,那么任何人读取这些写入时,会看见它们以正确的逻辑顺序出现。

这是一个听起来简单,实际却很难解决的问题。一种方案是应用保证将问题和对应的回答写入相同的分区。但并不是所有的数据都能如此轻易地判断因果依赖关系。如果有兴趣可以搜索向量时钟深入此问题。

分布式一致性模型的局限性

分布式一致性模型是被简化过的模型,它将整个分布式系统简化为single-object(如 k-v store, queue),single-op(如 Read/Write, Enqueue/Dequeue),因此有些东西是它没有讨论到的:

  1. single-object: 分布式系统有多个节点,不同的节点可能提供的一致性并不相同,比如连 master 节点可能满足线性一致性,而 slave 节点则不是。
  2. single-op: 前面的例子中,我们简单将事件当做原子性的操作,而在实践中,往往需要事务(2PC, 3PC)来保证整个分布式系统的内部一致性,这个内部一致性和我们前面讨论的外部一致性是有区别的。同样,事务,串行化这些东西和一致性模型也是不同的东西。

因此一些分布式系统在不同的配置选项或不同的连接状态下,可能体现出不同的一致性。

参考

  1. 线性一致性和 Raft | PingCAP线性一致性和 Raft | PingCAP
  2. 通俗易懂 强一致性、弱一致性、最终一致性、读写一致性、单调读、因果一致性 的区别与联系 - 知乎 (zhihu.com)
  3. 一致性杂谈 | wudaijun’s blog
  4. 分布式系统理论学习总结 | 无咎 NOTE (wujiu.space)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值