简介
陈现麟:最早研究一致性的场景并不是分布式系统,而是多路处理器。不过我们可以将多路处理器理解为单机计算机系统内部的分布式场景,它有多个执行单元,每一个执行单元都有自己的存储(缓存),一个执行单元修改了自己存储中的一个数据后,这个数据在其他执行单元里面的副本就面临数据一致的问题。对于数据的一致性,最理想的模型当然是表现得和一份数据完全一样,修改没有延迟,即所有的数据修改后立即被同步,但是这在现实世界中,数据的传播是需要时间的,所以理想的一致性模型是不存在的。不过从应用层的角度来看,我们并不需要理想的一致性模型,只需要一致性模型能满足业务场景的需求就足够了,同时由于一致性要求越高,实现的难度和性能消耗就越大,所以我们可以通过评估业务场景来降低数据一致性的要求,都是正确性和性能之间的衡权。
《大规模数据处理实践》
- 强一致性:系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值。所以在任意时刻,同一系统所有节点中的数据是一样的。在强一致性系统中,只要某个数据的值有更新,这个数据的副本都要进行同步,以保证这个更新被传播到所有备份数据库中。在这个同步进程结束之后,才允许服务器来读取这个数据。
- 弱一致性:系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。但经过“不一致时间窗口”这段时间后,后续对该数据的读取都是更新后的值。
- 最终一致性:是弱一致性的特殊形式。存储系统保证,在没有新的更新的条件下,最终所有的访问都是最后更新的值。在最终一致性系统中,我们无需等到数据更新被所有节点同步就可以读取。尽管不同的进程读同一数据可能会读到不同的结果,但是最终所有的更新会被按时间顺序同步到所有节点。
从读写角度
线性一致性
线性一致性也被称为原子一致性(Atomic Consistency)、强一致性(Strong Consistency)、立即一致性(Immediate Consistency)和外部一致性(External Consistency)。
那线性一致性是什么意思呢?它精确的形式化定义非常抽象,且难以理解。具体到一个分布式存储系统来说,线性一致性的含义可以用一个具体的描述来取代:对于任何一个数据对象来说,系统表现得就像它只有一个副本一样。显然,如果系统对于每个数据对象真的只存一个副本,那么肯定是满足线性一致性的。但是单一副本不具有容错性,所以分布式存储系统一般都会对数据进行复制(replication),也就是保存多个副本。这时,在一个分布式多副本的存储系统中,要提供线性一致性的保证,就需要付出额外的成本了。
- 对于写操作来说,任意两个写操作 x1 和 x2:
- 如果写 x1 操作和写 x2 操作有重叠,那么可能 x1 覆盖 x2,也可能 x2 覆盖 x1;
- 如果写 x1 操作在写 x2 开始前完成,那么 x2 一定覆盖 x1。
- 对于读操作来说:
- 写操作完成后,所有的客户端都能立即观察到;
- 对于多个客户端来说,必须读取到一样的顺序。
线性一致性保证了所有的读取都可以读到最新写入的值,即一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。在线性一致性模型中不论是数据的覆盖顺序还是读取顺序,都是按时间线从旧值向新值移动,而不会出现旧值反转的情况。
从实践来说:如果在主节点或同步副本的从节点上读取数据,那么就是线性一致性的。比如数据库的读为快照读,由于不能读到最新版本的数据,这个情况下就不是线性一致性的。
顺序一致性
- 对于写操作来说,任意两个写操作 x1 和 x2:
- 如果写 x1 操作和写 x2 操作有重叠,那么可能 x1 覆盖 x2,也可能 x2 覆盖 x1;
- 当写 x1 操作在写 x2 开始前完成,如果两个写操作没有因果关系,当写 x1 操作在写 x2 开始前完成,那么有可能 x1 覆盖 x2,也有可能 x2 覆盖 x1;如果两个写操作有因果关系,即同一台机器节点先写 x1,或者先看到 x1 然后再写 x2,则所有节点必须用 x2 覆盖 x1。
- 对于读操作来说:
- 如果写操作 x2 覆盖 x1 完成,那么如果一个客户端到 x2 后,它就无法读取到 x1 了,但是这个时候,其他的客户端还可以观察到 x1;
- 对于多个客户端来说,必须观察到一样的顺序。
相对于线性一致性来说,顺序一致性在一致性方面有两点放松:
- 对于写操作,对没有因果关系的非并发写入操作,不要求严格按时间排序;
- 对于读操作,只要求所有的客户端观察到的顺序一致性,不要求写入后,所有的客户端都必须读取新值。
因果一致性
- 对于写操作来说,任意两个写操作 x1 和 x2:
- 如果两个写操作没有因果关系,那么写 x1 操作在写 x2 开始前完成,有的节点是 x1 覆盖 x2,有的节点则 x2 可能覆盖 x1;
- 如果两个写操作有因果关系,即同一台机器节点先写 x1,或者先看到 x1 然后再写 x2,则所有节点必须用 x2 覆盖 x1。
- 对于读操作来说:
- 如果写操作 x2 覆盖 x1 完成,那么如果一个客户端到 x2 后,它就无法读取到 x1 了,但是这个时候,其他的客户端还可以观察到 x1。
相对于顺序一致性来说,因果一致性在一致性方面有两点放松:
- 对于写操作,对没有因果关系的非并发写入操作,不仅不要求按时间排序,还不再要求节点之间的写入顺序一致了;
- 对于读操作,由于对非并发写入顺序不再要求一致性,所以自然也无法要求多个客户端必须观察到一样的顺序。
最终一致性
对于同一台机器的两个写操作 x1 和 x2 来说:
- 如果写 x1 操作在写 x2 开始前完成,那么所有节点在最终某时间点后,都会用 x2 覆盖 x1。 对于读操作来说:
- 在数据达到最终一致性的过程中,客户端的多次观察可以看到的结果是 x1 和 x2 中的任意值;
- 在数据达到最终一致性的过程后,所有客户端都将只能观察到 x2。
从其它维度
- 现在可以实现的一致性级别最强的是线性一致性,它是指所有进程看到的事件历史一致有序,并符合时间先后顺序, 单个进程遵守 program order,并且有 total order。
- 顺序一致性,它是指所有进程看到的事件历史一致有序,但不需要符合时间先后顺序, 单个进程遵守 program order,也有 total order。
- 因果一致性,它是指所有进程看到的因果事件历史一致有序,单个进程遵守 program order,不对没有因果关系的并发排序。
- 最终一致性,它是指所有进程互相看到的写无序,但最终一致。不对跨进程的消息排序。
从“允许/不允许哪些操作顺序发生”角度
分布式系统中的一致性模型一个系统是由状态和一些导致状态转移的操作组成的。在系统运行期间,它将随着操作的演进从一个状态转移到另一个状态。一致性模型是所有被允许的操作记录的集合。当我们运行一个程序,经过一系列集合中允许的操作,特定的执行结果总是一致的。如果程序意外地执行了非集合中的操作,我们就称执行记录是非一致的。如果任意可能的执行操作都在这个被允许的操作集合内,那么系统就满足一致性模型。
系统的状态可以是个变量,操作可以是对这个变量的读和写。一旦我们把变量写为某个值,比如a,那么读操作就应该返回a,直到我们再次改变变量。读到的值应该总是返回最近写入的值。我们把这种系统称为——单值变量——单一寄存器。并发会让一切表现的不同,如果我们用2个进程(top和bottom)运行这个并发程序
- Top写入a,读到a,接着读到b——这不再是它写入的值。我们必须使一致性模型更宽松来有效描述并发。现在,进程可以从其他任意进程读到最近写入的值。寄存器变成了两个进程之间的协调地:它们共享了状态。
- 我们的操作不再是瞬时的。在几乎每个实际的系统中,进程之间都有一定的距离。一个没有被缓存的值(指没有被CPU的local cache缓存),通常在距离CPU30厘米的DIMM内存条上。光需要整整一个纳秒来传播这么长的距离,实际的内存访问会比光速慢得多。位于不同数据中心某台计算机上的值可以相距几千公里——意味着需要几百毫秒的传播时间。bottom发起一个读请求的时候,值为a,但在读请求的传播过程中,top将值写为b——写操作偶然地比读请求先到达寄存器。Bottom最终读到了b而不是a,Bottom并没有读到它在发起读请求时的值。有人会考虑使用完成时间而不是调用时间作为操作的真实时间,但反过来想想,这同样行不通:当读请求比写操作先到达时,进程会在当前值为b时读到a。PS:当你以为你读a(你之前写的)的时候有人正试图写入b,所以你想读到a还是b? 在分布式系统中,操作的耗时被放大了,我们必须使一致性模型更宽松:允许这些有歧义的顺序发生。我们该如何确定宽松的程度?我们必须允许所有可能的顺序吗?或许我们还是应该强加一些合理性约束?
- 线性一致性,基于硬件提供可线性化的操作CAS,当且仅当寄存器持有某个值的时候,我们可以往它写入新值(原子性约束来安全地修改状态)。线性一致性的时间界限保证了操作完成后,所有变更都对其他参与者可见。线性一致性禁止了过时的读。每次读都会读到某一介于调用时间与完成时间的状态,但永远不会读到读请求调用之前的状态。线性一致性同样禁止了非单调的读,比如一个读请求先读到了一个新值,后读到一个旧值。线性一致性模型提供了这样的保证:
- 对于观察者来说,所有的读和写都在一个单调递增的时间线上串行地向前推进。
- 所有的读总能返回最近的写操作的值。
- 顺序一致性放松了对一致性的要求:
- 不要求操作按照真实的时间序发生。
- 不同进程间的操作执行先后顺序也没有强制要求,但必须是原子的。
- 单个进程内的操作顺序必须和编码时的顺序一致。很多缓存的行为和顺序一致性系统一致。如果我在Twitter上写了一条推文,或是在Facebook发布了一篇帖子,都会耗费一定的时间渗透进一层层的缓存系统。不同的用户将在不同的时间看到我的信息,但每个用户都以同一个顺序看到我的操作。一旦看到,这篇帖子便不会消失。如果我写了多条评论,其他人也会按顺序的看见,而非乱序。PS:我10点写一篇帖子,你可以10点去看的时候没看到。
- 因果一致性。我们不必对一个进程内的每个操作都施加顺序约束,只有因果相关的操作必须按顺序发生。因果一致性比同一进程下对每个操作严格排序的一致性(即顺序一致性)来的更宽松——属于同一进程但不同因果关系链的操作能以相对的顺序执行(也就是说按因果关系隔离,无因果关系的操作可以并发执行),这能防止许多不直观的行为发生。PS:我10点、11点都发了一篇帖子并对帖子1评论,你可以11点看到了帖子2但没看到帖子1,但是不能帖子1没看到,帖子1的评论看到了。
“弱”一致性模型比“强”一致性模型允许更多的操作记录发生(这里的强与弱是相对的)。比如线性一致性保证操作在调用时间与完成时间之间发生。不管怎样,需要协调来达成对顺序的强制约束。不严格地说,执行越多的记录,系统中的参与者就必须越谨慎且通信频繁。
自己理解: top编码顺序abc,bottom编码顺序123,假设3必须在2之后,b和2读写同一个变量。 完全乱序有6*5*4*3*2*1
种可能;因果一致性限定了3必须在2之后,有6*5*4*3*2/2
种可能;顺序一致性限定了abc和123的编码顺序,有6*5*4*3*2/3*2*1/2/2
;线性一致性限定了2和b必须紧挨在一起,干掉了bc2 bc12 b12 b1c2 2ab 23ab 23b 23ab这8中可能,有6*5*4*3*2/3*2*1/2/2/8
。如果有协调者,则top和bottom 就按照协调者制定的唯一顺序运行。
bc2 bc12 b12 b1c2 2ab 23ab 23b 23ab
Quorum 机制
Quorum(属于最终一致性) NWR 中的三个要素NWR
- N 表示副本数,又叫做复制因子(Replication Factor)
- W又称写一致性级别(Write Consistency Level),表示成功完成 W 个副本更新,才完成写操作
- R,又称读一致性级别(Read Consistency Level),表示读取一个数据对象时需要读 R 个副本。你可以这么理解,读取指定数据时,要读 R 副本,然后返回 R 个副本中最新的那份数据
N、W、R 值的不同组合,会产生不同的一致性效果,具体来说,有这么两种效果:
- 当 W + R > N 的时候,对于客户端来讲,整个系统能保证强一致性,一定能返回更新后的那份数据。
- 当 W + R <= N 的时候,对于客户端来讲,整个系统只能保证最终一致性,可能会返回旧数据。
Kafka 数据可靠性深度解读虽然 Raft 算法能实现强一致性,也就是线性一致性(Linearizability),但需要客户端协议的配合。在实际场景中,我们一般需要根据场景特点,在一致性强度和实现复杂度之间进行权衡。比如 Consul 实现了三种一致性模型。
- default:客户端访问领导者节点执行读操作,领导者确认自己处于稳定状态时(在 leader leasing 时间内),返回本地数据给客户端,否则返回错误给客户端。在这种情况下,客户端是可能读到旧数据的,比如此时发生了网络分区错误,新领导者已经更新过数据,但因为网络故障,旧领导者未更新数据也未退位,仍处于稳定状态。
- consistent:客户端访问领导者节点执行读操作,领导者在和大多数节点确认自己仍是领导者之后返回本地数据给客户端,否则返回错误给客户端。在这种情况下,客户端读到的都是最新数据。
- stale:从任意节点读数据,不局限于领导者节点,客户端可能会读到旧数据。
当kafka producer 向 leader 发送数据时,可以通过 request.required.acks
参数来设置数据可靠性的级别:
- 1(默认):这意味着 producer 在 ISR 中的 leader 已成功收到的数据并得到确认后发送下一条 message。如果 leader 宕机了,则会丢失数据。
- 0:这意味着 producer 无需等待来自 broker 的确认而继续发送下一批消息。这种情况下数据传输效率最高,但是数据可靠性确是最低的。
- -1:producer 需要等待 ISR 中的所有 follower 都确认接收到数据后才算一次发送完成,可靠性最高。但是这样也不能保证数据不丢失,比如当 ISR 中只有 leader 时(前面 ISR 那一节讲到,ISR 中的成员由于某些情况会增加也会减少,最少就只剩一个 leader),这样就变成了 acks=1 的情况。
如果要提高数据的可靠性,在设置 request.required.acks=-1
的同时,也要 min.insync.replicas
这个参数 (可以在 broker 或者 topic 层面进行设置) 的配合,这样才能发挥最大的功效。
类似的思路: The Google File System (二):如何应对网络瓶颈?
其它
分布式系统原理介绍副本控制协议指按特定的协议流程控制副本数据的读写行为,使得副本满足一定的可用性和一致性要求的分布式协议。
- 中心化(centralized)副本控制协议,由一个中心节点协调副本数据的更新、维护副本之间的一致性。所有的副本相关的控制交由中心节点完成。并发控制将由中心节点完成,从而使得一个分布式并发控制问题,简化为一个单机并发控制问题。所谓并发控制,即多个节点同时需要修改副本数据时,需要解决“写写”、“读写”等并发冲突。单机系统上常用加锁等方式进行并发控制。
- 去中心化(decentralized)副本控制协议
kafka 因为有更明确地业务规则,有一个专门的coordinator,选举过程进一步简化,复制log的逻辑基本一致。《软件架构设计》 多副本一致性章节的开篇就 使用kafka 为例讲了一个 做一个强一致的系统有多难。
条分缕析分布式:到底什么是一致性?在证明CAP定理的原始论文Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web,C指的是linearizable consistency,也就是「线性一致性」。更精简的英文表达则是linearizability。而很多人在谈到CAP时,则会把这个C看成是强一致性(strong consistency)。这其实也没错,因为线性一致性的另一个名字,就是强一致性。只不过,相比「线性一致性」来说,「强一致性」并不是一个好名字。因为,从这个名字你看不出来它真实的含义(到底「强」在哪?)
分布式一致性技术是如何演进的?分布式一致性,简单的说就是在一个或多个进程提议了一个值后,使系统中所有进程对这个值达成一致。