深圳幻海软件技术有限公司 欢迎您!

这或许是最通俗易懂的数据一致性问题解读

2023-02-26

本文从普遍认为的分布式系统中最最重要的数据一致性开始。内容适合经验>=0年技术相关经验的人群。一、对数据一致性问题的剖析1为什么需要分布式系统?任何事物能够被持续的运用和发展,必然有其价值,分布式系统也是一样。分布式系统的产生我认为主要的目的就是“快”和“海量”。这个“快”可以分为两个方面:系

本文从普遍认为的分布式系统中最最重要的数据一致性开始。内容适合经验>=0年技术相关经验的人群。

一、对数据一致性问题的剖析

1为什么需要分布式系统?

任何事物能够被持续的运用和发展,必然有其价值,分布式系统也是一样。分布式系统的产生我认为主要的目的就是“快”和“海量”。这个“快”可以分为两个方面:

  • 系统的处理速度快
  • 开发的速度快(历时短)

这2点本质都是相同的,把一个动作或者一件事情拆成两部分或者多个部分去同时进行,使得整体的耗时缩短。比如:原本一件事情要一个人做的话要两分钟。那么我雇佣两个人帮我各自做一部分,那么最理想情况下一分钟就可以完成了。

当然这两个方面中第二项从某种意义上来说是可以克服的,但是第一项是无法克服的。因为没有一个程序或计算机的性能是无穷大的,如果有,那分布式系统也不会像现在这么普遍了(很多时候用钱能解决的问题都不是问题)。

“海量”则是由于不存在无穷大的硬盘,所以我们需要把数据分别存储到不同的硬盘上,才能满足需求。这些硬盘可能在不同主机、不同机房、不同地域(未来或许还可能会在不同的星球)。

2分布式系统的副作用

所谓每个事物都是矛盾统一的结合体,都具有两面性。分布式系统再带来了前面提到的好处的同时,也带来了业界普遍认为最大的问题——数据一致性问题。

系统是给人用的,构成使用场景的概念叫业务。业务是核心,对一个系统来说,业务的发展归根到底是建立在数据之上的。我可以慢,可以宕机,可以搞得很复杂,这些都能忍。但唯独不能忍的就是数据问题——数据错误、数据不一致等等。

分布式就意味着分治与协作,一件事一个人只负责一部分。

生活中这样的例子也无处不在,就拿举办一个Party来说:一部分人去准备吃的,一部分人去准备喝的,一部分人去准备场地布置。这些事情大家都可以同时进行,但是任一环节掉链子了,或者说不符合Party主题的话,都是失败的。(不知道为什么,脑子里浮现的是一场发布会,大家喊着cheers,一口干了高脚杯里的二锅头。。。)。

再举个电商场景中的程序案例:

这里的4个操作以目标来看,其实先后顺序并不重要,重要的是要么都成功,要么都失败,其中任意一个程序不一致那么就会出问题。这个问题本质上和人与人之间的沟通问题是类似的,与沟通唯一的不同在于,对程序来说,不一定都要得到响应,都没响应也是一致。当一个事情分成100个部分去做的时候,很可怕,从概率的角度来看,达到一致的概率是2/5050。

这里举的程序例子并不是严谨,因为实际的分布式系统中因为除了“write”操作还有“read”操作,所以一致性问题比这个更复杂,后面会有更详细的说明。

3产生数据不一致的原因

那么是什么原因导致了数据不一致的产生呢?

有一种原因是程序设计问题(代码写错了)。这点很好理解,也很容易想到解决方案——多做测试,验证是否符合预期咯。常见的单元测试、接口测试、自动化测试、集成测试等都是为了更具性价比地将BUG降低到无限接近于0,也造就了“测试工程师”这个岗位更大的作用。

但是,假设真的没有BUG,却还是会产生数据不一致。因为软件是运行在硬件之上的,所以还有硬件的因素存在。对我们这里的大部分人来说,我们对硬件的掌控力相比对软件,更弱。

这其中,最严重的属网络问题。网络相比其它而言是一个更大、更复杂的组织,未知性会随着局域网、广域网这样范围越大越严重。想象一下,每一台主机仅仅是一张大网中的一个渺小的连接点,它所承载的链接越多越容易出现问题。

可能有的小伙伴会有疑问,其它像硬盘、电源断电什么的,也有出现问题的可能性,为什么网络问题最为严重呢?

其实硬盘、电源好比是你身体的一部分,如手和脚。而网络是人与人之间沟通的渠道,比如手机通话,虽然你没有主动挂断电话,但是整个通话过程是有很多可能性导致中断的,对方的主观意愿也好、信号不好也罢,甚至被第三者给拦截了。相信大家也能认可,打电话出现异常的概率相比自己的手脚不听使唤是高很多的吧。

现实中网络的特点,常遇到的问题如:延迟、丢包、乱序等问题。为了解决这些问题,从互联网第一次出现的1969年(当年美军在ARPA制定的协定下用网络连接了4所大学)到现在,几十年间出了很多的理论和解决方案,这些会在后续的文章中给大家一一做梳理。本部分先和大家具体剖析下什么是一致性。

4详解一致性

什么叫达成一致了?说起来很简单——在任意时间、任意位置看到的同一个事物是完全一致的。

比如一场足球赛,不管我们在现场还是在电视机前,看到足球从球员A传给球员B,这个信息都是一样的。但是严格意义上来说,这个并称不上真正的一致,因为电视机接收到这个信息需要经过卫星信号、网络等的传输,我们看到的时候相比现场的人肯定要晚。哪怕在现场的人,根据他所处的位置,理论上看到的信息也存在延迟差,只是因为光速非常快,使得在相差几百米之内,这个延迟小到完全感受不到而已。

能得出的结论是:在考虑时间维度的情况下,不存在真正意义上的一致。

况且我们在分布式系统中,也没有必要去达到真正的意义上的一致。因为越趋近于一致,系统相当于又归一成一个单体了,在某一个时刻,只能做一件事,完全丧失了分布式系统的两个目的之一“快”的优势。也因此衍生出多种一致性的变种,分别适用于不同的场景。为了便于理解,我们从严格程度的低到高来说。

大多数情况下,为了尽可能的“快”,系统中使用的大部分方案都是所谓的最终一致性,也就容忍一定条件下的不一致,优先保证局部一致,然后再通过一系列复杂的状态同步达到全局的一致。最终一致性很多可实现的分支,列出几种常见的,抛砖引玉一下:

  • 因果一致性:仅要求有因果关系的操作顺序得到保证。比如朋友圈的回复功能。问“饭吃了吗?”肯定得在回答“吃了”之前。
  • 读你所写一致性:文字看着别扭,但很好解释。比如你在朋友圈下面回复一句话,其它好友可以不用马上看到你的回复,但是你自己必须得马上看到,要不然回复到哪去了?
  • 会话一致性:与人的一次聊天可以理解为一次会话。聊天虽然也有一定的因果关系,但是大部分场景下更多的是逻辑上的先后关系。比如你阐述一个事情,分为3条信息:首先...,然后...,最后...。如果这里的一致性得不到保证那么可能会变成:最后...,首先...,然后...。

比局部一致更严格一些的就是全局的顺序一致性[1],保证所有进程看到的全局执行顺序一致,并且每个进程自身的执行顺序和实际发生顺序一致。

注:文中[1-6]标注皆可于文末找到对应参考资料

像上面提到的足球赛,比如实际发生的事情是①梅西把球传给了C罗,②C罗又把球回传给了梅西,那么每个人看到顺序都应该是这样。哪怕现场观众已经看到②了,电视机前的我们还没看到①,但是没关系,这个事情发生的顺序,对全世界来说都是一样的。

再严格一些,就是在全局的顺序一致性基础上再增加一个相对时间的一致性要求,业界称之为线性一致性[2]。还是用上面梅西和C罗相互传球的例子来做个比喻,相当于梅西传出球给C罗之后,整个球场“暂停”了,要等所有在观看这场球赛的人都接收到这个传球信息之后,C罗才能做下一个回传。这里需要一个上帝(全局时钟)来“暂停”。这是我们实际可以做到的极限了,满足这类要求的系统中,名气最大的就属Google的Spanner了。

对不同级别的一致性汇总概述如下:

二、通过共识达成数据一致性

第一部分我们已经对数据一致性问题做了一次剖析,那么怎么解决由于故障导致的不一致问题呢?通过共识来达成。所以,本部分会围绕“共识”这个点展开。

1“共识”是什么?为什么会产生?

一致性问题其实是一个「结果」,本质是由于数据冗余导致的,如果没有冗余,也就不会有一致性问题了。

分布式系统里的各个子系统之间之所以能够相互协作,就是因为其之间冗余了相同的数据作为“信物”。要不然我都不认识你的话,为什么要配合你干活呢?所以这个“信物”变了,你得通知我,要不然我又不认识你了。这个“信物”变更达成一致性的过程称作达成「共识」。所以:

一致性问题是结果,共识是为达到这个结果所要经过的过程,或者说一种手段。

在分布式系统中,冗余数据的场景不限于此,因为规模越大的系统,越不能容忍某一个子系统出问题后产生蝴蝶效应,所以往往会做高可用。小明1号倒下了还有千千万万个小明X号在坚守岗位,理想中的全天候24小时提供服务。

高可用的本质是通过相同数据存储多个副本,并都可对外提供服务。比如每个小明X号都有一本《按摩指法白皮书》,谁请假了都可以由其它小明X号提供相同的按摩服务。但是这个本《按摩指法白皮书》改了,就得通知到每个人,因为这是服务的全部和来源,所以在做了高可用的集群中数据冗余的问题更为突出。

实际上,如果分布式系统中各个节点都能保证瞬时响应、无故障运行,则达成共识很容易。就好像我们人一样,在一定范围内只要吼一嗓子,通过稳定的空气传播,相关人是否接收到这个消息,并且给出响应几乎可以是“瞬时”的。

但是正如前文提到,这样的系统只停留在想象中,响应请求往往存在延时,网络会发生中断,节点发生故障,甚至存在恶意节点故意要破坏系统。这就衍生出了经典的「拜占庭将军问题」[3]。

2拜占庭将军问题

我们一般把「拜占庭将军问题」分为2种情况来看待:

  • 拜占庭错误。表示通过伪造信息进行恶意响应产生的错误。
  • 非拜占庭错误。没有进行响应产生的错误。

这个问题的核心在于:

如何解决某个变更在分布式网络中得到一致的执行结果是被参与多方都承认的,同时这个信息是被确定的,不可推翻的。

好比如何让所有的小明X号收到的都是《按摩指法白皮书Ⅱ》,而不是其它的,并且把原来的那本销毁掉。

这个问题衍生出了很多“共识”算法,解决「拜占庭错误」的称作Byzantine Fault Tolerance(BFT)类算法,解决「非拜占庭错误」的称作Crash Fault Tolerance(CFT)类算法。从这个2个名字中也可以看出,本质的工作就是「容错」。

有的小伙伴在平时的工作中可能对「容错」的重要性感知没那么强烈——不就产生一个BUG或者异常数据么?但是在航天领域,一个小错误可能导致整个发射的失败,代价非常巨大。

对「拜占庭将军问题」想深入的了解的,可以自行查阅相关资料,这里就不展开了,文末附上刚才我们标注的论文。

我们常见的软件开发中一般不会考虑「拜占庭错误」,但它是区块链项目的必需品。不过在主流的分布式数据库中,皆能看到「非拜占庭错误」的身影,诸如TiDB的Paxos算法,CockroachDB的Raft算法。虽然我们大家在日常的coding中,对数据库底层原理的了解并不是必须项。但是只要当我们涉及到应用程序级别的高可用时,那么至少「非拜占庭错误」是必须要面临的一道坎。

BFT类算法

BFT类型算法又有2个分支。「基于确定性的」和「基于概率的」。

先聊聊「基于确定性的」:

此类算法表示一旦对某个结果达成共识就不可逆转,即共识是最终结果。它的代表作是PBFT(Practical Byzantine Fault Tolerance)算法[4],自从有了央行背书(区块链数字票据交易平台),名声更大了。算法的原理,如下图:

 


▲图片来源于网络,版权归原作者所有

拿军队来比喻,这里的直线C可以认为是“总司令”,直线0是“军长”,直线1、直线2、直线3都是“师长”,值得注意的是3号师长叛变了。整个过程这样解释:

  • 「request」:总司令给军长下了一个命令,“干!”。
  • 「pre-prepare」:军长把命令又广播给3个师长。
  • 「prepare」:每个师长收到并同意之后将发送“收到”给军长和其他两个师长。
  • 「commit」:每个师长收到2f个师长(军长不做prepare)的“收到”请求后发送“随时开干”给军长和其他两个师长。(f为可容忍的拜占庭节点数)
  • 「reply」:每个师长收到2f+1条“随时开干”消息之后,就能认为总司令的命令在相关的师长中都到达了“随时开干”的状态,那么他就直接开炮了!

真正想深入了解PBFT的话还有很多内容,这里就不继续展开了,有兴趣的小伙伴可以在文末参考处自行查阅论文。

再聊聊「基于概率的」:

此类算法的共识结果则是临时的,随着时间推移或某种强化,共识结果被推翻的概率越来越小,成为事实上的最终结果。它的代表作是PoW(Proof of Work)算法,曾经高达2W美元/个的比特币就是基于这个算法来实现的。算法的原理拿“修仙”来做个简单的比喻(实际比特中的算法比这更复杂):

  • 自己努力修炼,并让神仙中大于一半的人认可你的修为,同意你成仙。
  • 随之你就成为了神仙。并且参与到评判后续其他人是否可以成为“神仙”的事情中去。
  • 这个事情如果想通过贿赂来达到的话,随着这个团队的人数越多,贿赂的成本越大,就可以认为去做贿赂的人越少,那么导致被误判的概率就越低,最终就越可信。

被误判的概率公式是:0.5^个数,如果个数=6的话,误判的概率是1.5625%。如果个数=10的话,就已经是0.09765625%了,指数级下降。

值得注意的是,「基于确定性的」和「基于概率的」对于不合作节点的标准是不同的,前者至多能容忍1/3,后者是小于1/2。

4CFT类算法

正如上面所说CFT类算法解决的是分布式系统中存在故障,但不存在恶意节点的场景(即可能消息丢失或重复,但无错误消息)下的共识达成问题。「拜占庭将军问题」的提出者Leslie Lamport也在他另外的论文[5]中提出过「Paxos问题」,与这相似。在论文中通过一个故事类比了这个问题,如下:

  • 希腊岛屿Paxon 上的「执法者」在「议会大厅」中表决通过『法律』,并通过「服务员」传递纸条的方式交流信息,每个「执法者」会将通过的『法律』记录在自己的「账目」上。问题在于「执法者」和「服务员」都不可靠,他们随时会因为各种事情离开「议会大厅」,并随时可能有新的「执法者」进入「议会大厅」进行法律表决。
  • 使用何种方式能够使得这个表决过程正常进行,且通过的『法律』不发生矛盾。

—— 百度百科

这里的关键对象在我们的系统中,可以类比为:

  • 议会大厅=分布式系统
  • 执法者=某个程序
  • 服务员=RPC通道
  • 账目=数据库
  • 法律=一次变更操作

Leslie Lamport自己也提出了解决这个问题的算法——Paxos算法[6]。这个算法的关键由以下3个定义来体现:

  • 每次“变更”都有个唯一的序号,并且能够通过它识别新旧;
  • 「执法者」只能接受比已知的“变更”更新的变更;
  • 任意两次“变更”必须有相同的「执法者」参与。

这3点仅仅是保证一致性的最关键部分,全部内容还有很多。有兴趣的小伙伴可以在文末参考处自行查阅论文。

「Paxos」算法是一种无领导人(Leaderless)算法,实现比较复杂,所以产生了很多变种来简化它,其中名气最大的应该是「Raft」,2013年才问世。「Raft」算法是一种领导人(Leadership)的算法。由以下2个过程保证达成共识:

  • 只会存在一个活着的领导人,领导人负责跟随者的数据同步;
  • 如果领导人“失联”了,那么每个跟随者都可成为候选人,最终比较谁的term最新,谁就是新的领导人。这个term是每个节点内部维护的一个自增数。

虽然跟随者的投票秉承先到先得,但是还是会遇到多个term相同的候选人获得了相同票数(简称「分割投票问题」),那么进行新一轮投票,直到决出胜负为止。由于Raft用随机定时器来自增term,加上网络是不稳定的,所以再次遇到相同票数的概率就大大降低了。

完整的过程更复杂一些,有一个Raft算法的动画推荐给大家,有兴趣的可以了解一下:http://thesecretlivesofdata.com/raft/。

题外话,大家经常用的Zookeeper里的「ZAB」(ZooKeeper Atomic Broadcast)算法也是CFT类算法,是以Fast Paxos算法为基础实现的。

5结语

回过头来看,我们发现,想要更严谨的一致性,那么就需要增加相互通讯确认的次数,但是这会导致性能低下,正如PBFT和Paxos一样。但是分布式系统就是这样,到处都需要Balance,找到最适合的才是最重要的。

聊完了数据层面的「共识」问题,我们下回再聊聊「分布式事务」的问题,将会围绕着常见的CAP、BASE理论展开。