可以说一致性与共识是分布式系统中最为麻烦的问题,同时也是理论研究的热点,诞生了各种各样的算法,而这些东西又偏偏被八股文所喜爱,因此充满了各种各样的文章与分析。以非应试、实际工作的角度来说,虽然只有很少一部分人、很少一部分情况会直接使用或面对一致性算法,但理解一致性与共识也是很有必要的,其作为各种复杂系统的基石,有一个正确良好的认识会在面对实际问题时更有把握、精准。

如今碎片化知识爆炸的情况下,零散的各种文章、回答、介绍都不足以对这种偏基础理论的内容构建完备的认知,往往是看过就忘或者在收藏夹里吃灰,这种情况下书籍就相对更有优势,在此特别推荐一本好书:《设计数据密集型应用》,原作者Martin Kleppmann(也就是和Redis之父Antirez争论Redlock的那位),真的很不错,我作为一个不爱读书的人都认为这是可以读好几遍的书籍。这本书其实并不算深奥,还是非常平易近人的,但是这第九章:一致性与共识,我确实是第一遍没看明白,重新阅读、整理了几遍也不敢说彻底明白,所以就有了本篇文章。

本文也是浓缩后的再浓缩,阅读不是很理解的内容时容易陷入无头苍蝇的状态,看完了就好像没看过一样,因为从某些地方思路就断了,这也是读书时做笔记、整理思路能够带来巨大好处的原因。书籍比碎片文章好的点在于优秀的书籍在内容的组织上包含了作者连贯的思路与表达,把条理顺出来是读者理解消化的过程,能很好地避免无脑阅读造成的过目即忘,这也是为什么会有本篇的原因。本篇文章作为个人向、浓缩化的内容写出来更多是为了自我总结,但也不失为一件与读者共赢的事情,希望这能够在你阅读完这本书后整理思路时有所帮助,其他好的总结文章也是一样。你应该先阅读这本书,或者这一章节之后再来看看本文,我是看的中译版(在书栈网,附送一个好网站),有能力有资源还是阅读原版更好一些。

本篇采用白话型的大标题,标题连起来看就能看到整体的思路。

分布式数据的需求

该书的第二部分首先介绍了需要分布式数据的理由,这是后续讨论的实际诉求来源,主要有如下几点:

  1. 可扩展性:如果你的数据量、读取负载、写入负载超出单台机器的处理能力,需要将负载分散到多台计算机上
  2. 容错和高可用:应用需要在单台机器(或多台机器,网络或整个数据中心)出现故障的情况下仍然能继续工作,需要使用多台机器,以提供冗余。
  3. 低延迟和高性能:如果在世界各地都有用户,你也许会考虑在全球范围部署多个服务器,从而每个用户可以从地理上最近的数据中心获取服务,避免了等待网络数据包穿越半个世界。

分布式数据按组织形式又可分为两大类:复制和分区,而且它们经常同时使用。我们着重考虑复制的问题。

分布式系统的麻烦

该书花了一整章来讨论这个问题,可粗略种地那归纳为三类:

  1. 网络的不可靠性:通过网络发送数据包时,数据包可能会丢失或任意延迟,同样的,响应也可能会丢失或延迟
  2. 节点时钟不一致:计算机中的石英钟不够精确,它会漂移(drifts)(运行速度快于或慢于预期),依靠它是很危险的
  3. 进程可能暂停:进程可能会在其执行的任何时候暂停一段相当长的时间(可能是因为全局的垃圾收集器),被其他节点宣告死亡,然后再次复活,却没有意识到它被暂停了

而这些问题对于数据副本的复制会造成极大的麻烦,副本之间的数据是否相同、何时相同、如何相同延伸出各种各样的问题、讨论和理论,可以归为一致性的范畴。

对一致性的诉求

当应用程序从异步更新的从库读取数据时,如果从库落后,它可能会看到过时的信息。这会导致数据库中出现明显的不一致:同时对主库和从库执行相同的查询,可能得到不同的结果,因为并非所有的写入都反映在从库中。这种不一致只是一个暂时的状态——如果停止写入数据库并等待一段时间,从库最终会赶上并与主库保持一致。出于这个原因,这种效应被称为 最终一致性(eventually consistency)

大多数复制的数据库至少提供了最终一致性,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值。然而,这是一个非常弱的保证 —— 它并没有说什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有。

诚然存在对分布式数据的诉求,但是数据不一致会使人困惑甚至产生严重的问题。如果将一个值赋给一个变量,然后很快地再次读取,你不会认为可能读到旧的值或者读取失败;如果两个用户一个读取到了最新值,而另外一个用户此时恰巧从副本上读取到了旧值,这会让他们感到困惑,认为这是个错误的系统。这是人最直观的感受,因为这违反了因果关系

此外,一致性不仅仅是数据一致,还包括各个节点对某件事达成一致,这就是所谓的共识。一旦达成共识,就可以将其应用于各种目的,比如领导选举、原子提交。共识的一种反面是脑裂(split brain),有多个节点认为自己是领导者,引发各种灾难性的后果。

共识是更加面向底层的概念,涵盖了数据一致这一朴素诉求。在书籍对此进行讨论的时候,这俩的论述被分的比较开,认知到这俩同源的本质是很重要的一点。

最强的线性一致是什么

线性一致性也称为原子一致性、强一致性、立即一致性或外部一致性。什么使得系统线性一致?这个问题很重要,但是书里并没有给出一个具体的回答,而是给了精确定义的论文引用,我简单瞄了一眼看不太下去,非常学术性的定义,所以还得是书里意境化的回答:线性一致使系统看起来好像只有一个数据副本,而且所有的操作都是原子性的。 在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。根据这个定义,从存储本身的角度来看,并发操作是不存在的,必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。

线性一致与并发

上述可知:

  1. 线性一致的存储不存在并发操作,但是从全知上帝视角来看并发是存在的,我们可能同时做某些操作
  2. 网络是不可靠的、进程是可能暂停的,先发出的请求不一定会先被系统处理

那么如何定义或者判断是否线性一致呢?书籍给了几个非常不错的带图例子,首先是一个具体的真实模拟场景:

Alice和Bob正坐在同一个房间里,都盯着各自的手机,关注着2014年FIFA世界杯决赛的结果。在最后得分公布后,Alice刷新页面,看到宣布了获胜者,并兴奋地告诉Bob。Bob难以置信地刷新了自己的手机,但他的请求路由到了一个落后的数据库副本上,手机显示比赛仍在进行。

如果Alice和Bob在同一时间刷新并获得了两个不同的查询结果,也许就没有那么令人惊讶了。因为他们不知道服务器处理他们请求的精确时刻。然而Bob是在听到Alice惊呼最后得分之后,点击了刷新按钮(启动了他的查询),因此他希望查询结果至少与爱丽丝一样新鲜。但他的查询返回了陈旧结果,这一事实违背了线性一致性的要求。

这个例子描述了没有任何额处理逻辑下的单主复制读写场景,可以看到复制操作是全异步的,很好地展现了不一致发生的场景。这个例子的重点在于Bob是在Alice的请求响应结束之后再发起的操作,突出了非并发的条件限制,注意我们在看这个例子的时候是站在全知的上帝视角。

此后再给出第二个例子,抽象程度更高一些,力求点出问题所在:

三个客户端在线性一致数据库中同时读写相同的键x。在分布式系统文献中,x被称为寄存器(register),例如,它可以是键值存储中的一个,关系数据库中的一,或文档数据库中的一个文档。每个柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间——只知道它发生在发送请求和接收响应的之间的某个时刻。

该书称这个例子采用了用户请求的视角,但是实际上是全体用户的视角。重点在于请求或操作在客户端的角度来说在(时间)顺序上并不是 一个点,而是一段时间,这是前述两点问题带来的必然结果。有如下推导:

线性一致 => 像是只有一个副本 => 符合先后顺序、遵从因果关系 => 后发生的请求或操作必须要能感知到相对于它之前发生的操作的结果 => 俗话翻译为:写完新值后不应该读到旧值(这句话有隐含条件,读写原子) => 虽然服务处理操作时可以认为是原子的,但是网络的不确定性和进程的可停顿会拉长用户视角下的操作 => 全体用户视角下,先发起的请求有可能因为延迟读到了旧的数据,这是无法避免的,那要怎么判定数据系统是否是线性一致的? => 加条件:如果一个客户端的读取返回了新值,则所有按照顺序在其之后发起的读请求也必须返回新值。

该书的第三个例子增加了CAS操作,阐明了线性一致判定方式: 通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序。这是一个推断式的结论,因为网络是不确定的。此外书中解释了什么是跨信道的时序依赖,在第一个例子中如果Alice没有惊呼得分,Bob就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面,并最终看到最后的分数。由于系统中存在额外的信道(Alice的声音传到了Bob的耳朵中),线性一致性的违背才被注意到。这和我强调每个例子的视角很重要是同样的根源,在上帝视角下,上帝与每个用户的信道是无延迟的,所以立刻能发现不一致。所以之前我总是会觉得这些个算法不还是无法避免不一致么,因为这些操作并非物理原子,总会有那么些个时刻存在不一致,此刻的我就是站在上帝视角的想法,但是站在一个没有并行能力的个体视角,ta是不知道是否存在并发的,ta只会关注自己得到的响应是否合理(符合因果),上帝视角看到的不一致对于ta来说可能并不是个问题,只是在延迟无限提高的情况下,就不是特别容易接受这个理论。

线性一致的代价

虽然线性一致是一个很有用的保证,但实际上,线性一致的系统惊人的少。例如,现代多核CPU上的内存甚至都不是线性一致的:如果一个CPU核上运行的线程写入某个内存地址,而另一个CPU核上运行的线程不久之后读取相同的地址,并没有保证一定能一定读到第一个线程写入的值(除非使用了内存屏障(memory barrier)围栏(fence))。许多分布式数据库也是如此:它们是为了提高性能而选择了牺牲线性一致性,而不是为了容错。线性一致的速度很慢——这始终是事实,而不仅仅是网络故障期间。

能找到一个更高效的线性一致存储实现吗?看起来答案是否定的:Attiya和Welch证明,如果你想要线性一致性,读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中,线性读写的响应时间不可避免地会很高。更快地线性一致算法不存在,但更弱的一致性模型可以快得多,所以对延迟敏感的系统而言,这类权衡非常重要。

话虽如此,数量少归少,后面还是表明了线性一致的必要性以及讨论如何线性一致(狗头)。

因果一致性方案的分析

那线性一致这么难,这么有损耗,能不能换个一致性模型呢?首先表明在线性一致中看重的是什么:可以说是满足人类常识的合理性,也可以说是保证因果,一个问题的答案先于问题出现是令人困惑的,而顺序有助于保持因果关系(causality)。如果一个系统服从因果关系所规定的顺序,我们说它是因果一致(causally)的。例如,快照隔离提供了因果一致性:当你从数据库中读取到一些数据时,你一定还能够看到其因果前驱(假设在此期间这些数据还没有被删除)。

因果关系对事件施加了一种顺序:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即什么在什么之前发生。

特别注意理解一下全序与偏序 :

全序(total order)允许任意两个元素进行比较,所以如果有两个元素,你总是可以说出哪个更大,哪个更小。例如,自然数集是全序的:给定两个自然数,比如说5和13,那么你可以告诉我,13大于5。

然而数学集合并不完全是全序的:{a, b}{b, c} 更大吗?好吧,你没法真正比较它们,因为二者都不是对方的子集。我们说它们是无法比较(incomparable)的,因此数学集合是偏序(partially order)的:在某些情况下,可以说一个集合大于另一个(如果一个集合包含另一个集合的所有元素),但在其他情况下它们是无法比较的

可以通过跟踪所有的因果关系来实现因果一致,但是这是不切实际的,在许多应用中,客户端在写入内容之前会先读取大量数据,我们无法弄清写入因果依赖于先前全部的读取内容,还是仅包括其中一部分。显式跟踪所有已读数据意味着巨大的额外开销。有一个更好的办法是通过使用序列号(sequence nunber)时间戳(timestamp)来排序事件。时间戳不一定来自时钟(物理时钟存在许多问题),它可以来自一个逻辑时钟(logical clock),这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。

CAS自增的发号器是一个满足因果一致的实现,但是这其实已经属于线性一致,线性一致隐含着因果关系,它需要确定的主库,如果主库失效了就会无法服务。而常规思路下的序列号生成器大都是非因果一致的:

  1. 每个节点独立生成序列号:但是所有节点是一个整体,这个序号除了不会重复并没有什么卵用
  2. 使用物理时钟时间戳: 时钟漂移目前是无法避免的问题,如果时间精度足够高就是一种符合直觉思维的全序
  3. 预先分配序列号块:这和第一种发号器差不多,而且会依赖一个外部的统一配号工具,那这个外部配号器又会成为问题,治标不治本

那有没有因果一致的序列号呢?这就引出了兰伯特时间戳,兰伯特时间戳不是特别好理解,因为我总想着这玩意儿搞不定一些东西,这个想法是对的,但是抱着这个想法会阻碍理解这个概念。兰伯特时间戳的基础在于三个点:

  1. 同个进程或机器中的事件通过内部配号就能够区分先后
  2. 两个进程或机子之间收发消息,发送事件先于接收事件
  3. 因果关系是偏序,但是作为序号总是要拿来比较,比不了的怎么办:序号里带了节点ID,约定节点ID大的更大,以此提供了一种全序

具体内容还是看书理解,这里不细说,此外也可以参考这篇文章来辅助理解。

全序广播的诉求

那有了兰伯特时间戳提供的全序就都OK了么,并不是的,因为它可以说是“事后诸葛亮”,当你拿到一个兰伯特时间戳时,你没法确定是不是还有更小的时间戳会出现,这就解决不了实时系统中与唯一性挂钩的相关问题。

这种方法适用于事后确定胜利者:一旦你收集了系统中的所有用户名创建操作,就可以比较它们的时间戳。然而当某个节点需要实时处理用户创建用户名的请求时,这样的方法就无法满足了。节点需要马上(right now)决定这个请求是成功还是失败。在那个时刻,节点并不知道是否存其他节点正在并发执行创建同样用户名的操作,罔论其它节点可能分配给那个操作的时间戳。

那如何知道你的全序关系已经尘埃落定?这就涉及到了全序广播。什么是全序广播,该书也是没给一个白话式的定义,其实看完后面回过头来看,可以不准确地说是:主节点控制顺序(单机全序)然后广播到所有副本。看着就是单主复制,也许是我理解不够准确,但是一定有所联系。

全序广播的一个重要表现是,顺序在消息送达时被固化:如果后续的消息已经送达,节点就不允许追溯地将(先前)消息插入顺序中的较早位置。这个事实使得全序广播比时间戳命令更强。

全序广播与线性一致的相互推导

有了全序广播,可以在这基础上构建出线性一致的存储,注意是原理上可以。将全序广播实现成仅追加(append only)的日志,可以保证写入是线性一致的,但并不保证读取也是线性一致,有可能从异步更新的日志中读取到陈旧的数据,所以这种实际上实现的是顺序一致性,也称为时间线一致性。

如果要保证读取也是线性一致的,需要做额外操作,可能的方式有:

  1. 追加写入消息,写入返回时进行读取,消息写入的位置定义了读取发生的时间点
  2. 查询的时候带上位置信息,服务器必须等待该位置之前的消息都到达后才能读取返回(zookeeper sync)
  3. 确保从同步更新的副本中读取

同样,线性一致的存储也可以反推实现全序广播。最简单的情况下,比如有一个单机的线性一致存储器,每个要通过全序广播的消息先从这个存储器执行incrementAndGet操作,然后将返回值作为消息的序号,以此发送到所有节点。由于有序,可以进行重发,收件节点按照顺序接收这些数据。

这里可以延伸一个非常重要的概念:一致性读。回到前面Alice和Bob看世界杯的问题,如果写入力求原子和一致,那么就可能变成如下图:

这采用了单主同步复制写,按照理论它应该是线性一致的,但是不加额外逻辑的情况下就如同上述所说有可能从副本中读取到陈旧的数据。数据库还是那个数据库,读写的行为发生了改变,一致性就发生了改变,这也说明服务满足线性一致与具体操作有关。此时读与写属于并发,Alice的读取是否返回新值与系统是否一致无关,但是Alice如果收到了新值并且要求系统线性一致,那么之后Bob的读取也必须返回新值,图中的读取就不满足了。如果要实现一致性读,在这个例子中,根据上述的额外操作对应来说:

  1. 将读与写关联,因为写入必须在主节点,那么Alice与Bob的读取必须经过主节点
  2. 读取操作可以带上位置信息并且会返回位置信息,Alice读完把位置信息给Bob,Bob带着位置信息请求尚未同步的副本,副本收到Bob请求后进入阻塞,直到同步完成后才给Bob响应,Bob的请求变慢了,但是能够拿到最新的、与Alice一致的结果
  3. 即确保从主节点进行读取,主节点保证读写原子,这就属于单机事务的范畴

还得是线性一致和单主复制

上面可以看到,我们从因果一致、全序广播饶了一大圈又回到了线性一致,该书前文也表明了复制的几种方式:

  1. 单主复制:可能是线性一致的,依赖于如何确定谁是主
  2. 共识算法:是线性一致的,与单主复制类似,但共识协议还包含防止脑裂和陈旧副本的措施
  3. 多主复制:非线性一致,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点,因此它们可能会产生冲突的写入
  4. 无主复制:也许不是线性一致的,取决于一致性如何定义,没有强有力的保证

那么还得是单主复制,该书随即挑明:可以证明,线性一致的CAS(或自增并返回)寄存器与全序广播都都等价于共识问题。

没有容错的共识:分布式事务和2PC

终于顺利引出了共识,共识从表面上解释很明确:让几个节点达成一致(get serveral nodes to agree on something)。看起来很简单,但是这是一个相当复杂的问题,因为上述分布式系统麻烦的存在。为了更好地理解共识,首先来看不容错的共识:具体来说是两阶段提交(2PC, two-phase commit)算法。

引入一个额外的角色协调者,每次启动分布式事务时,

  1. 应用都要向协调者请求一个全局唯一的事务ID
  2. 带上ID对每个参与的DB节点启动单节点事务
  3. 对每个节点写入完毕后,应用开始发起提交,协调者带着事务ID向每个参与者发送prepare请求(此为第一阶段开始)
  4. 参与者收到prepare请求后,如果没有问题就需要确保在任意情况下都可以提交事务后(例如写入磁盘)返回协调者“是”,此时参与者放弃了终止事务的权利,但没有实际提交
  5. 协调者收到所有prepare(第一阶段结束),根据响应决策这个事务是提交还是回滚,把决定写入事务日志(提交点)
  6. 事务日志落盘后将提交或放弃请求发送给所有参与者,这是一个必须重试直到成功的行为,包括崩溃后从事务日志恢复,必须收到参与者的响应,同时参与者不能拒绝提交请求

响应prepare后参与者没有权利拒绝提交请求、协调者做出决定写入事务日志后就不能撤销事务,这两点保证了2PC的原子性。两阶段内如果事务协调者发生崩溃,那么所有参与者必须处于等待状态,直到协调者恢复并发送事务的决定信息,因此它被称为阻塞原子提交协议,这也是2PC被诟病的一点。2PC的性能问题主原因是:1.崩溃恢复所需带来的强制刷盘fsync 2.额外的网络往返。

2PC的逻辑允许异构数据库参与事务,只要参与者能满足2PC中需要保证的能力,属于标准的定义。X/Open XA(eXtended Architecture)是跨异构技术实现两阶段提交的标准。

XA不是一个网络协议——它只是一个用来与事务协调者连接的C API。其他语言也有这种API的绑定;例如在Java EE应用的世界中,XA事务是使用Java事务API(JTA, Java Transaction API)实现的,而许多使用Java数据库连接(JDBC, Java Database Connectivity)的数据库驱动,以及许多使用Java消息服务(JMS)API的消息代理都支持Java事务API(JTA)

2PC、XA不是终点,所以我们看一下它的缺点来引出下一部分:

  1. 2PC在执行过程中如果协调者失效,对于单个节点来说必须维持可提交的状态,通常表现为持有锁,容易导致系统进入大面积不可用的状态,即使节点重启也无法解决(否则就会违反原子性保证)
  2. 协调者存在单点故障,大部分实现中的协调者并不是高可用的
  3. 协调者有状态,这与如今许多应用服务器使用无状态开发相违背,协调者日志不能丢
  4. XA兼容各种数据系统,它必须是所有系统的最小公分母,无法检测不同系统间的死锁
  5. 如果系统的任何部分损坏,事务也会失败,具有扩大失效(amplifying failures)的趋势

容错共识

我们需要共识又要保证高可用,这就是所谓的容错共识,也是共识算法所要达到的目的。共识算法必须满足如下性质:(进程和节点在这里是同一个意思)

  1. 一致同意:没有两个节点的决定不同
  2. 完整性:没有节点决定两次
  3. 有效性:如果一个节点决定了值v,则v必然由某个节点提议而来
  4. 终止:由所有未崩溃的节点来最终决定值

前三个属性保证了共识的正确性、安全性,终止属性是一种活性属性,带来了容错能力,表明共识算法必须取得进展,即使某些节点出现故障。

共识算法的实现方式和局限性

之前也明确了全序广播等价于共识,那单主复制不就是全序广播么,所以剩下的问题在于如何选主。如果leader由运维人员手动选择和配置,那么实际上这是一种独裁类型的“共识”,缺点是需要人工干预选主。选主需要每个节点都认可,否则就会发生脑裂,这也是共识的范畴。这就导致了一个循环:共识需要有主,选主需要共识。

为了跳出这个先有鸡还是先有蛋的问题,迄今为止的共识算法都采用了时代编号和法定人数的做法。共识算法在内部以某种形式确定一个领导者,但它们并不保证领导者是独一无二、不变的。

它们可以做出更弱的保证:协议定义了一个时代编号(epoch number)(在Paxos中称为投票编号(ballot number),视图戳复制中的视图编号(view number),以及Raft中的任期号码(term number)),并确保在每个时代中,领导者都是唯一的。每次当现任领导被认为挂掉的时候,节点间就会开始一场投票,以选出一个新领导。这次选举被赋予一个递增的时代编号,因此时代编号是全序且单调递增的。如果两个不同的时代的领导者之间出现冲突(也许是因为前任领导者实际上并未死亡),那么带有更高时代编号的领导说了算。

投票选主过程看起来很像2PC,但是最大的区别在于,2PC中协调者不是由选举产生的,而且2PC则要求所有参与者都投赞成票,而容错共识算法只需要多数节点的投票。而且,共识算法还定义了一个恢复过程,节点可以在选举出新的领导者之后进入一个一致的状态,确保始终能满足安全属性。

看起来容错的共识算法真不错,它为充满不确定性的系统带来了基础的安全属性并且还能保持容错。但是一切皆有代价,没有完美的银弹。共识算法有如下局限性:

  1. 节点在做出决定之前对提议进行投票是一种同步复制,势必带来更大的开销和延迟
  2. 共识系统需要严格多数来维持运转,为此需要成倍的节点数量
  3. 大多数共识算法假定节点成员固定,而支持动态成员扩展的算法会更难理解
  4. 依靠超时来检测故障可能引发频繁选举,对网络状况敏感

共识的应用:成员与协调服务

引出Zookeeper和etcd,这些项目看起来也可以作为数据库,那为什么不直接把它们当数据库:它们被设计为容纳少量完全可以放在内存中的数据(虽然它们仍然会写入磁盘以保证持久性),所以你不会想着把所有应用数据放到这里。这些少量数据会通过容错的全序广播算法复制到所有节点上。

如今我们可以看到,最后还是在单主复制的范围内,单主已经说明了写入能力是在单机范围内的,说是用性能来换取高级能力也不为过。但是并不是所有操作都需要强一致和共识,好钢用在刀刃上,这些高级的场景可以抽象封装出外部服务来降低应用组件的开发难度,所以出现了Zookeeper和etcd这种服务型工具,而HBase、Hadoop Yarn、Kafka这些实用组件依赖了Zookeeper。依赖这些共识服务工具可以得到:

  1. 工作任务分配。这包括了选主和负载重平衡,典型如kafka对zookeeper的依赖
  2. 服务发现。典型如微服务注册与发现,但是这个功能不一定需要共识,只是它偏好少量数据(服务地址)的高可用。
  3. 成员服务。确定哪些节点处于活动状态,这对于构建高度可靠的系统非常重要。尽管因为网络的不确定性存在误判的可能性,但是只要集群对节点是否存活达成一致就不会出问题。

最后

看待问题的视角很重要,很多话语和结论出自不同的思考角度,站在错误的视角很难得到正确一致的理解,小心你的潜意识。举个例子比如2PC,站在上帝视角,第二个阶段每个节点收到最后的commit也需要经过网络传输,总是有某些时刻部分节点提交了而其他节点没有,你说它是“不一致”的么?换到全体用户的半全知视角,要产生“不一致”,写入(提交)和读取就是并发的,按照理论,并发的时候,返回新值或旧值都可以,关键是读到新值以后有没有可能再读取到旧值;再换到无并发能力的单一用户视角,只要返回的数据是合理、满足因果的,那么就不会认为出现了问题。

一致性共识是一个深刻的话题,尽管总结了很多,我也没把握在实时交流中对其能有良好的表述,共勉。

Java日志的小注意
GRPC与HTTP2