简介
单机可靠性靠不住,所以要多机,多机多数据副本则有数据一致性问题,即master挂了slave接手的时候,不能master有的数据slave没有。
陈现麟:最早研究一致性的场景并不是分布式系统,而是多路处理器。不过我们可以将多路处理器理解为单机计算机系统内部的分布式场景,它有多个执行单元,每一个执行单元都有自己的存储(缓存),一个执行单元修改了自己存储中的一个数据后,这个数据在其他执行单元里面的副本就面临数据一致的问题。对于数据的一致性,最理想的模型当然是表现得和一份数据完全一样,修改没有延迟,即所有的数据修改后立即被同步,但是这在现实世界中,数据的传播是需要时间的,所以理想的一致性模型是不存在的。不过从应用层的角度来看,我们并不需要理想的一致性模型,只需要一致性模型能满足业务场景的需求就足够了,同时由于一致性要求越高,实现的难度和性能消耗就越大,所以我们可以通过评估业务场景来降低数据一致性的要求,都是正确性和性能之间的衡权。
《大规模数据处理实践》
- 强一致性:系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值。所以在任意时刻,同一系统所有节点中的数据是一样的。在强一致性系统中,只要某个数据的值有更新,这个数据的副本都要进行同步,以保证这个更新被传播到所有备份数据库中。在这个同步进程结束之后,才允许服务器来读取这个数据。
- 弱一致性:系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。但经过“不一致时间窗口”这段时间后,后续对该数据的读取都是更新后的值。
- 最终一致性:是弱一致性的特殊形式。存储系统保证,在没有新的更新的条件下,最终所有的访问都是最后更新的值。在最终一致性系统中,我们无需等到数据更新被所有节点同步就可以读取。尽管不同的进程读同一数据可能会读到不同的结果,但是最终所有的更新会被按时间顺序同步到所有节点。
来龙去脉
回答有关分布式系统的几个核心问题亚马逊在其关于 Dynamo 数据存储系统的 SOSP 2007 论文中对此进行了非常生动的描述:“客户必须能够将商品添加到他们的购物车中,即使磁盘出现故障、网络路由抖动或数据中心被龙卷风摧毁。”因此,由于龙卷风或任何其他原因,不仅网络连接会失败,而且该网络中的节点也会失败。即使在出现故障的情况下,系统也必须继续运行并响应请求,这一点很重要,因为用 Werner Vogels 的话来说,“一切都在失败”。因此,部分故障的问题,以及需要在部分故障存在的情况下继续运行,在许多方面是分布式系统的定义特征。
但是在地理分布式系统中,我们还有另一个基本问题,那就是光速很慢!它在一纳秒内仅移动约 30 厘米,每个时钟周期只有大约 4 英寸。因此,如果我们谈论的机器可能彼此位于世界的另一端,那么即使在理想条件下,在消息发送和消息到达之间也可能会经历很多时钟周期。
所以,无论我们是在谈论故障——磁盘故障、宇宙射线、网络连接被反铲挖掘机截断、数据中心被龙卷风摧毁——或者只是更普通但不可避免的问题,即相距很远的机器之间的长时间延迟,缓解这些挑战的一种方法是在多个物理位置复制数据。这有助于确保数据的可用性——换句话说,有助于确保每个请求都得到有意义的响应。这样做是出于容错的原因——你拥有的副本越多,你丢失它的可能性就越小——以及数据局部性,以最大限度地减少延迟——我们希望数据的副本接近任何人的尝试访问它 - 以及吞吐量的原因:如果你有多个数据副本,那么原则上你可以为更多尝试同时访问它的客户提供服务。但是,尽管出于所有这些原因拥有多个副本是必不可少的,但它引入了必须保持副本彼此一致的新问题,特别是在我们拥有的网络中,由于网络分区,副本之间的延迟可能任意长。因此,正如我告诉参加我的分布式系统课程的本科生,复制既是大多数分布式系统问题的原因,也是解决方案。我们必须复制数据,但我们必须保持这些副本彼此一致。
现在,我们知道试图始终保持地理分布的副本完全一致是徒劳的。一个经典的不可能结果告诉我们,在存在网络分区的情况下实现完美一致性的唯一方法是牺牲数据的可用性。由于在处理地理分布式数据时网络分区是不可避免的,我们通常选择以允许客户观察副本之间某些类型的分歧的方式设计系统,以优先考虑可用性。如果我们要允许副本之间存在某些类型的分歧,那么我们需要一种指定策略的方法,该策略告诉我们允许副本以何种方式不同意,在什么情况下允许他们不同意,以及在什么情况下他们必须同意。PS:有隔离性的味道
复制模型(云里雾里)
Replication(上):常见的复制模型&分布式系统的挑战
- 最简单的复制模式——主从模式。问题:主节点压力大
- 多主节点复制。问题:冲突
- 无主节点复制。读写Quorum,要想保证读到的是写入的新值,每次只从一个副本读取显然是有问题的,那么需要每次写几个副本呢,又需要读取几个副本呢?这里的一个核心点就是让写入的副本和读取的副本有交集,那么我们就能够保证读到新值了。
每种模式都有一定的应用场景和优缺点,但是很明显,光有复制模式远远达不到数据的一致性,因为分布式系统中拥有太多的不确定性,需要后面各种事务、共识算法的帮忙才能去真正对抗那些“稀奇古怪”的问题。
分布式系统特有的故障
- 不可靠的网络,多了一种不确定的返回状态,即延迟,而且延迟的时间完全无法预估。
- 不可靠的时钟
假设老板想要处理一批文件,如果让一个人做,需要十天。但老板觉得有点慢,于是他灵机一动,想到可以找十个人来搞定这件事,然后自己把工作安排好,认为这十个人一天正好干完,于是向他的上级信誓旦旦地承诺一天搞定这件事。他把这十个人叫过来,把任务分配给了他们,他们彼此建了个微信群,约定每个小时在群里汇报自己手上的工作进度,并强调在晚上5点前需要通过邮件提交最后的结果。于是老版就去愉快的喝茶去了,但是现实却让他大跌眼镜。 首先,有个同学家里信号特别差,报告进度的时候只成功报告了3个小时的,然后老板在微信里问,也收不到任何回复,最后结果也没法提交。另一个同学家的表由于长期没换电池,停在了下午四点,结果那人看了两次表都是四点,所以一点都没着急,中间还看了个电影,慢慢悠悠做完交上去了,他还以为老板会表扬他,提前了一小时交,结果实际上已经是晚上八点了。还有一个同学因为前一天没睡好,效率极低,而且也没办法再去高强度的工作了。结果到了晚上5点,只有7个人完成了自己手头上的工作。
如果一个理想的分布式数据系统,如果不考虑任何性能和其他的开销,我们期望实现的系统应该是这样的:
- 整个系统的数据对外看起来只有一个副本,这样用户并不用担心更改某个状态时出现任何的不一致(线性一致性)。
- 整个系统好像只有一个客户端在操作,这样就不用担心和其他客户端并发操作时的各种冲突问题(串行化)。
Replication(下):事务,一致性与共识线性一致性和串行化是两个正交的分支,分别表示外部一致性中的最高级别以及内部一致性的最高级别。用Jepsen官网对这两种一致性的定义来说,内部一致性约束的是单操作对单对象可能不同副本的操作需要满足时间全序,而外部一致性则约束了多操作对于多对象的操作。这类比于Java的并发编程,内部一致性类似于volatile变量或Atomic的变量用来约束实现多线程对同一个变量的操作,而外部一致性则是类似于synchronize或者AQS中的各种锁来保证多线程对于一个代码块(多个操作,多个对象)的访问符合程序员的预期。
回答有关分布式系统的几个核心问题2016 年发表的一篇调查论文,其中对分布式系统文献中的 50 多个一致性概念进行了分类,并按照所谓的“语义强度”对它们进行了排序。换句话说,一致性模型的顺序越高,允许的执行次数越少,副本之间的分歧就越少。你走的越低,允许的执行越多,副本之间的分歧越大。而且,正如你所看到的,这是一个偏序——有许多对一致性模型,其中一个都不比另一个强。50多个 也不是全部,举个例子(称之为强收敛性):假设我和我的伴侣都在更新我们共享的女儿照片相册,名为photos。我添加了一张照片,首先将其添加到副本 R1,稍后再添加到副本 R2。与此同时,我的搭档添加了一张不同的照片,它首先出现在 R2 中,然后出现在 R1 中。尽管副本以相反的顺序接收更新,但它们的状态最终是一致的。因此,本次执行具有较强的收敛性。强收敛性并没有出现在这个调查论文里。
从读写角度
用更专业一点的词来说,线性一致性需要的是定义“全序”,而其他一致性则是某种“偏序”,也就是说允许一些并发操作间不比较顺序,按所有可能的排列组合执行。
线性一致性
线性一致性也被称为原子一致性(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。
相对于顺序一致性来说,因果一致性在一致性方面有两点放松:
- 对于写操作,对没有因果关系的非并发写入操作,不仅不要求按时间排序,还不再要求节点之间的写入顺序一致了;
- 对于读操作,由于对非并发写入顺序不再要求一致性,所以自然也无法要求多个客户端必须观察到一样的顺序。
我向相册添加了一张照片。我碰巧与副本 R1 通信,R1 将更新异步发送到 R2。后来,我的搭档过来看照片。他看到了我的更新,因为他碰巧与进行更新的副本相同。但是假设他在那之后试图发表评论,不幸的是,那篇文章发给了 R2,它还没有拿到照片。因为照片还没有,所以复制品不能附加评论。解决此问题的一种方法是使用确保消息按因果顺序传递到副本的机制,其中“已传递”一词意味着在收到后实际应用于副本的状态。一种广泛使用的方法是将元数据附加到每条消息中,以总结该消息的因果历史——例如,矢量时钟。我们可以使用此元数据来确定两条消息之间的因果关系(如果有的话),并延迟将消息传递到给定副本,直到每条因果先前的消息都已传递。在这种情况下,comment发送到副本 R2 的消息将排队,直到add消息到达,然后可以应用评论。
最终一致性
对于同一台机器的两个写操作 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)。这其实也没错,因为线性一致性的另一个名字,就是强一致性。只不过,相比「线性一致性」来说,「强一致性」并不是一个好名字。因为,从这个名字你看不出来它真实的含义(到底「强」在哪?)
分布式一致性技术是如何演进的?分布式一致性,简单的说就是在一个或多个进程提议了一个值后,使系统中所有进程对这个值达成一致。
并发扣款,如何保证一致性? 很有意思。