分布式系统杂谈

1. 前言

  这一段时间在学习分布式相关的知识,正好有幸遇到了《数据密集型应用系统设计》这本好书,阅读了部分和分布式系统比较相关的知识,这本书的知识密度太大,很多地方需要反复的阅读才行。虽然自己之前关于分布式也看了很多的资料,但是感觉这本书介绍的是非常全面的,但是由于知识量过大,有些地方写的也不是很清晰,也有可能对于我来说新概念有点多,导致理解起来宏观把握上有些困难。
  这篇文章也算是自己对分布式学习的一个梳理,主要参考了该书中的前面9章的内容。当然,主要还是自己对这些内容的思考,所以很有可能也有许多不太对的地方,欢迎各位大佬指正。另,本篇的分布式系统主要指分布式数据存储系统,不包括分布式计算系统。
在这篇文章的行文之初,我其实是想写写raft的,本来只是想要写一些东西作为引子,结果发现越写越多,分布式周边的概念等确实太多了,于是又查阅了一些资料,单独形成了一篇文章,这里暂且称之为分布式杂谈吧。

首先,我觉得分布式数据系统有两个大的功能点

  1. 分布式数据一致性
  2. 分布式事务支持
    这两个功能点实际上是对数据存储或者处理系统的不同层面的要求,有些系统只满足了其中的一个特性(比如raft),有些会满足其中的两个特性(比如oceanbase)。

而想要实现这两个方面的特性都离不开共识的达成,这段话算是总的一个理解认识,后面的话我会沿着这些来展开。

2. 什么是分布式,为什么要有分布式

  在早期的网络世界里面大多数系统的设计是简单的,因为使用互联网的人很多,而且互联网只是一个非常弱的补充,比如干开始的时候都是一些个人站长,做做听音乐的,简单的咨询聚合类的,或者是网页小游戏,很多时候这些网站都是一两个人在做,所以架构设计上也会选择更简单容易维护的系统,用户对数据的一致性等并不是非常的敏感,而且即使发生了少量数据丢掉的问题也不会产生特别的影响,互联网的用户群体也不是很大,大家并没有像今天这样热衷于在网上产生各种各样的内容,所以数据量也比较小,并不像现在一个可能只是有上百人的公司就要面临对上百G数据的存储和查询需求,所以早期的网络架构在存储层很多时候都是单节点。
通过上面的论述也可以看出来,采用单节点的原因

  1. 要存储的数据内容相对较少
  2. 用户对网站服务的可用性和安全性等要求较低,可以接受服务短时间不可用或者是部分数据丢失
  3. 对于网站建设者来说这样的架构方案更加简单,占用的人力资源更少

  而随着互联网的普及,已经深入到人们生活的方方面面,比如现在的支付宝,微信等等,人们也把自己很重要的信息存储在各个网络服务上,于是对网络服务的可用性和安全性要求也越来越高。分布式系统的存在主要就是为了提高网站的可用性而逐步发展出来的。
我们仅仅拿数据存储层面来举一个简单的例子

  1. 在网站的早期,可能只有一台服务器用来跑MYSQL进程,上面可能有10个database。
  2. 随着服务的发展,用户越来越多,数据量也越来越大
    1. 这个时候可能需要多个MYSQL进程(跑在不同的服务器上),每个MYSQL进程里面有2个database。
    2. 同时,这个时候可能希望数据库能够更加稳定,比如一台MYSQL服务器挂了,不至于整个服务长时间的停滞不可访问,如果这个服务器的硬盘完全坏掉了,可能所有的用户数据也就丢失了,这对公司来说是一个巨大的损失。这个时候可能会对MYSQL进行master/salve架构的改造,在master出现意外的时候能够快速的切换到slave,继续对外提供服务,可能在原来的情况下需要一个小时才能恢复的服务现在可以控制的分钟级别(当然因为master和slave之间数据的复制方式也决定了可能会产生数据丢失)。
  3. 随着时间的累计,用户的数据可能更多了,产生的内容也更多了,这个时候可以继续升级单个MYSQL服务器的配置,使用高性能的磁盘,更大的内存,更多的cpu核数,但是这个单机的拓展是有限的,而且后面的拓展会让一个MYSQL服务器看起来像一个奢侈品,每次拓展都让你肝疼,这种拓展方式被称为垂直拓展,注意,这里是两个缺点哦,一个是贵,还有一个即使你不缺钱,受限于操作系统和程序软件,这些东西无法一直向上扩容(比如MYSQL的表太大的话机器性能再好,查询时长也会比较长)。但是好处也很明显,使用方(业务程序)不需要做任何改动。
  4. 还有一种拓展方式就是水平拓展,对应的,是使用多个低廉的机器来替代3中的一个高性能的机器,比如MYSQL中这个,可以使用对一个特别大的表进行分表处理,这些表可存储在一个database中也可以存储在多个database中,这样的话,理论上我的服务器可以无限增加,而且代价很小。当然这样的话也有弊端,比如每个数据应该存放到哪个database的哪个table,需要有专门的路由等等,这会导致很多工作要放在业务程序层面来做,后面我们会更具体的分析。

上面的这个数据库架构也是一个互联网公司从起步到逐步壮大可能会经常使用的一个架构(当然现在很多使用了云服务,可能功能相对强大了很多), 系统也是由单点系统逐步变成了分布式系统,当mysql从单点系统变成 master/slave模式,他就已经是一个分布式系统了,只是这个系统的一致性还不够好,应该说可以满足最终一致性,比如client停止了写入,那么经过了一段时间slave和master上的数据会变成一致的,但是这个一段时间则是一个玄妙的东西,就像你要带你对象出门的时候正在照镜子的她说的"马上好"一样,可能是一分钟,也可能是十个一分钟。

综上而言,分布式系统主要是为了改善单点的这些局限性

  1. 单个节点容量有限,存的太多处理起来也可能很慢(单个mysql服务实例的节点容量有限,单个table的数据容量有限)
  2. 单个节点宕机后恢复比较慢,一般会导致一段时间无法堆外提供服务,而且数据有可能有全部丢失的风险

同时分布式系统努力提供这样的抽象:
对于client来说,整个服务集群就像一个单节点node一样,假如是MYSQL,那么使用master/slave架构和使用单个节点保证对于client是透明的

这样的抽象就要求

  1. 系统内部肯定要有多个节点构成(这个是扩容和容错的根本)
  2. 数据系统要能够按照分区的方式对数据进行存储,不同的分区可以存在不同的节点(可以参考理解为MYSQL的某张表进行了分库分表的处理,每张表是一个分区)
  3. 任何一个分区不能只有一个节点持有,否则,万一这个节点宕掉了就导致这个分区的数据都丢了,整个数据系统无法对外提供服务了(可以理解为MYSQL的master/slave架构,slave负责对数据进行备份)

当然这里我们忽略了分区后对事务的支持,这个属于数据存储系统要实现的功能,有些存储系统是不带事务的,比如redis,mongo等,再后面我们会单独聊一聊分布式事务。

3. 一致性(Consistency)

  感觉现在的论文翻译和博客等把共识(consensus)和一致性(Consistency)直接互译了,也就是认为是一个概念了,就此我还查看了很多博客和论文,感觉两者还是有一些区别的。下面的论述都是个人的一些见解,不具备学术上的代表性哈。从paxos和raft的论文中都把自己称为一种consensus算法,而且通篇没有提到consistency。个人感觉我们常说的分布式系统的一致性其实是强调了分布式系统的一个抽象保证,就是保证分布式系统对于应用层和单节点的表现有多么的相近。
一致性分为以下几种

1. 一致性分类

1. 弱一致性

  弱一致性是指系统并不保证后续进程或线程的访问都会返回最新的更新的值。系统在数据write成功之后,不承诺立即可以读到最新写入的值,也不会具体承诺多久读到。但是会尽可能保证在某个时间级别(秒级)之后。可以让数据达到一致性状态。
  最终一致性是弱一致性的一种特例。某个进程更新了分区的数据(但是复制到全部的副本需要一些时间),如果没有其他进程更新这个副本的数据,系统最终一定能够保证后续进程能够读取到A进程写进的最新值。但是这个操作存在一个不一致性的时间窗口,也就是A进程写入数据,到其他进程读取 A 写进去的值所用的时间。在最终一致性的要求下,如果没有故障发生,不一致窗口的时间主要受通信延迟,系统负 载和复制副本的个数影响。达到最终一致状态的系统,被称为收敛系统(converged systen),或者是达到副本收敛(replica convergence)。最终一致性提供了一个弱保证:在系统达到收敛之前,可能会返回任意值。

我们可以看看MYSQL的master/slave架构,实际上就是一种最终一致性的架构,如果我们的write操作总是访问master,而read操作允许访问slave,那么很有可能slave和master之间存在时间延迟,当你在master做了一个写入,之后立刻对slave进行读取的话很有可能会出现读取不到刚才写入的值的情况,很多时候这个延迟能够控制在秒级,但是有时候网络延迟的时候达到分钟级别也很正常。

2. 线性一致性

CAP理论中的C(consistency)就是这个意思,这个是强一致性,可以参考这篇来理解。
简而言之,如果一个分布式系统满足线性一致性,那么就要求系统堆外提供的所有操作都是原子的,而且在时间线上先完成的操作必须对后面的操作可见。

3. 因果一致性

因果一致性是指对于有相互依赖的数据,其依赖的先后关系是必须保证的。可以参考下面的分布式系统中的顺序一节的论述。

感觉consensus是一个比一致性更加大一些的概念。通过达成共识可以设计出来一个对外表现出来一致性的系统

4. safety 和liveness

这个也是分布式系统的两个属性,这两个概念在分布式系统中一般有如下保证

safety: nothing bad happens
liveness: something good eventually happens
主要参考这里

1. Safety 安全性

在程序运行中不会进入错误的状态,比如对于分布式数据库来说,不管是网络问题还是整个集群的大部分节点都挂了,系统可以不响应,但是不能响应错误的信息,比如数据明明没有完成持久化但是却给client返回了成功持久化的信息。违反了安全性就意味了对系统的破坏已经发生了,系统自身很难消除破坏造成的影响

2. Liveness 活性

在程序运行中预期状态最终会达到。
没有死锁的系统可以认为是一种具有活性的系统,比如多个进程共同访问一块共享区域,同一时刻只有一个进程能够访问,那么在一个如果某个进程被阻塞,那么预期在将来是可以访问到该共享区域的,而不是出现绝对访问不到的情况。同样,当你访问一个系统的时候,如果系统是正常工作的,则理论上最终会收到响应。

区分安全性和活性属性的一个优点是可以帮助我们处理困难的系统模型。对于分布式算法,在系统模型的所有可能情况下,要求始终保持安全属性是基本要求。也就是说,即使所有节点崩溃,或者整个网络出现故障,算法仍然必须确保它不会返回错误的结果(即保证安全性得到满足)。

但是,对于活性属性,我们可以提出一些注意事项:例如,只有在大多数节点没有崩溃的情况下,只有当网络最终从中断中恢复时,我们才可以保证请求最终会收到响应。部分同步模型的定义要求任何网络中断的时间段只会持续一段有限的时间,然后得到了修复,系统最终返回到同步状态。

5. Distributed algorithm 分布式算法的使用场景

  分布式算法是一种设计为在由互连处理器构成的计算机硬件上运行的算法。分布式算法用于分布式计算的许多不同应用领域,例如电信,科学计算,分布式信息处理和实时过程控制。分布式算法解决的标准问题包括原子提交,领导者选举,共识,分布式搜索,生成树生成,互斥和资源分配。
  分布式算法是并行算法的一种子类型,通常并行执行,算法的各个部分在独立的处理器上同时运行,并且关于该算法的其他部分正在做什么的信息有限。面对处理器故障和不可靠的通信链路,成功开发和实施分布式算法的主要挑战之一是成功地协调算法独立部分的行为。选择合适的分布式算法来解决给定问题取决于问题的特征以及算法将运行的系统的特征,例如处理器或链接故障的类型和可能性,进程间通信的类型可以执行的操作,以及各个进程之间的定时同步级别。(来自这里)

我们这里主要关注的是分布式存储系统,这里主要的应用场景有

  1. leader选举
  2. 共识Consensus
  3. 分布式事务(原子提交)
  4. 互斥

这些场景其实都可以通过最基本的共识算法增加一些条件,演化得到。

1. leader选举

  领导者选举是指在多个node之间选出一个node作为处理任务的组织者的过程。 在选举开始之前,所有网络节点都不知道哪个节点将充当任务的“领导者”或协调者。但是,在运行领导者选举算法后,整个网络中的每个节点都会将特定的唯一节点识别为任务领导者。在很多具备leader/follower架构的系统中,leader选举算法都是必须的。在leader选举中很多时候并不是要求所有的节点达成共识,只要大部分节点达成共识,其他的节点接受这个共识就好了。
  迄今为止所讨论的所有共识协议,在内部都以某种形式使用一个领导者,但它们并不能保证领导者是独一无二的。相反,它们可以做出更弱的保证:协议定义了一个时代编号(epoch number)(在Paxos中称为投票编号(ballot number),视图戳复制中的视图编号(view number),以及Raft中的任期号码(term number),并确保在每个时代中,领导者都是唯一的。
  每次当现任领导被认为挂掉的时候,节点间就会开始一场投票,以选出一个新领导。这次选举被赋予一个递增的时代编号,因此时代编号是全序且单调递增的。如果两个不同的时代的领导者之间出现冲突(也许是因为前任领导者实际上并未死亡),那么带有更高时代编号的领导说了算。
  在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高时代编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在“真理在多数人手中”中提到的:一个节点不一定能相信自己的判断—— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。
  相反,它必须从**法定人数(quorum)**的节点中获取选票(参阅“读写的法定人数”)。对领导者想要做出的每一个决定,都必须将提议值发送给其他节点,并等待法定人数的节点响应并赞成提案。法定人数通常(但不总是)由多数节点组成【105】。只有在没有意识到任何带有更高时代编号的领导者的情况下,一个节点才会投票赞成提议。

具体的详情可以后面再解读raft的时候再进一步对比学习。

2. 共识Consensus

  共识算法(Consensus algorithms )一般是为了解决多个进程对某个一决定(decision)达成一致。这里隐含的意思是(all processes have an initial value),常用在分布式事务的提交决定,状态机副本的复制,原子广播等场景。
  共识问题要求多个进程(或代理)之间就单个数据值达成一致。某些进程(代理)可能失败或不可靠,因此共识协议必须具有容错性或弹性。进程必须以某种方式提出其候选值,彼此沟通并就单个共识值达成共识。
共识问题是控制多主体系统中的一个基本问题。达成共识的一种方法是让所有进程(代理)都同意多数值。在这种情况下,多数需要至少一个以上一半以上的可用投票(其中每个过程都被投票)。但是,一个或多个错误的进程可能会扭曲最终结果,从而可能无法达成共识或错误达成共识。
  解决共识性问题的协议旨在容忍有限数量的错误进程。这些协议必须满足许多要求才能有用。共识协议的输出值必须是某个进程的输入值。另一个要求是,一个进程只能决定一次输出值,并且该决定是不可撤销的。如果进程没有失败,则该进程被称为正确的进程。允许停止失败的共识协议必须满足以下属性。

在wiki和一些其他的文章上都提到了一个共识协议必须达成以下三个准则,参考这里这里

Termination: Eventually every correct process decides some value.
Integrity: If all processes propose the same value v, then every correct process decides v.
Agreement: Every correct process must agree on the same value

翻译过来说就是必须满足

  1. Termination: 每一个正常运行的进程都要最终做决定,也就是达到最终的一致认可的状态,这一条保证了算法的活性,假如一个共识的场景是leader选举,当绝大部分节点都选举某个nodeA为leader,但是有一个nodeB始终不承认nodeA作为他的master,那么就是没有达成一致性,也就无法终结,除非使用其他的方案(将这个节点踢出去),终止性保证了算法的活性liveness,不会陷入永不结束的僵局当中
  2. Integrity(完整性): 如果所有的进程都提出(propose)同一个value v, 那么所有的进程最终也会决定(decides) v,这个应该是安全性的保证
  3. Agreement(一直性):所有正确运行的进程必须对同一个value达成一致同意的状态

这里的Integrity属性也被称为Validity,在不同的应用约束也不太一致,在有些系统中对Integrity要求更加弱一些,有可能是decision value 只要是任何一个或者多个correct 进程 proposed就行。

所以正式的定义有可以是下面的样子

Agreement: All correct processes must agree on the same value.
Weak validity: For each correct process, its output must be the input of some correct process.
Strong validity: If all correct processes receive the same input value, then they must all output that value.
Termination: All processes must eventually decide on an output value

  网上的英文描述不尽相同,中文翻译更是如此,也有可能是我的知识背景不够的原因,总觉得解释的看的有点云里雾里,这里尝试根据自己的理解来解释一下相关含义,想了想,之所以难以理解有可能一个原因是因为wiki中在阐述这几个定义的时候又用了一些特殊的词,但是没有对这些词的含义进行明确界定,所以导致了一些误解
主要有两个词

  1. propose value, input value 这个可以认为是每个process最初开始提出的,想要大家达成一致认同的备选value
  2. decide , output vule 这个是指最终的一致性状态,一个decide value就是指大家最终一致认可的value,这个很容易被误解为raft 的leader选举的投票阶段的投票(或者paxos的accept request阶段)

3. 分布式事务(原子提交)

  原子提交是将一组不同的更改作为单个操作应用的操作。如果原子提交成功,则意味着所有更改都已应用。如果在完成原子提交之前发生故障,则“提交”将中止,并且不会应用任何更改。这个也需要共识进行支撑。常见的有XA事务标准,但是这个存在一定的单点风险。分布式事务和leader选举不一样,分布式事务需要所有的节点都要达成共识才行。

4. 互斥

  在计算机科学中,互斥是并发控制的一个属性,其建立是为了防止竞争状况。要求一个执行线程永远不要在另一个并发执行线程进入自己的关键部分的同时进入其关键部分,这是指执行线程访问共享资源的时间间隔,例如共享内存。在分布式领域则是多个应用服务对某个远程共享资源访问的互斥性,同一时间只能有一个进程持有该锁。分布式算法有时候需要提供互斥的能力,比如zookeeper提供的同一路径只能有一个client创建成功的互斥能力。

6.时钟

在分布式系统中为了体现事件发生的先后顺序,所以有了时钟,时钟也分为两类,物理时钟和逻辑时钟

1. 物理时钟(physical clock)

最开始最自然的想法就是使用物理时钟了,想要使用现实世界中的时间,但是也是会有问题的,因为我们的应用程序试图获取的物理时间是通过是服务器的时钟来获取的,这是一个实际的硬件设备:通常是石英晶体振荡器。这些设备不是完全准确的,所以每台机器都有自己的时间概念,可能比其他机器稍快或更慢。可以在一定程度上同步当前机器上的时钟:最常用的机制是网络时间协议(NTP),它允许根据一组服务器报告的时间来调整计算机时钟。这样就可以从更精确的时间源(如GPS接收机)获取时间。

在计算机系统中海油一个叫单调钟的东西,单调钟适用于测量持续时间(时间间隔),例如超时或服务的响应时间:Linux上的clock_gettime(CLOCK_MONOTONIC),和Java中的System.nanoTime()都是单调时钟。这个名字来源于他们保证总是前进的事实(而时钟可以及时跳回)。

你可以在某个时间点检查单调钟的值,做一些事情,且稍后再次检查它。这两个值之间的差异告诉你两次检查之间经过了多长时间。但单调钟的绝对值是毫无意义的:它可能是计算机启动以来的纳秒数,或类似的任意值。特别是比较来自两台不同计算机的单调钟的值是没有意义的,因为它们并不是一回事。

2. 逻辑时钟(logic clock)

所谓的逻辑时钟是基于递增计数器而不是振荡石英晶体,对于排序事件来说是更安全的选择(请参见“检测并发写入”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。

7. 分布式系统中的顺序

首先说一下,这里的顺序是指数据系统执行的操作顺序,顺序和一致性是不一样的概念,但是属于一个事务的两个方面描述,比如描述一个人的话,第一种描述是这个人职业技能很好 第二种描述是这个人之前的工作成果都很好,那么这两种描述可以认为是相关的,相互印证的。

因为分布式系统中的每个数据分区都有多个副本,如果不做限制的话,可能全部的操作构成的操作历史的顺序在不同的副本之间可能是不同的。分布式的顺序就是指分布式系统的执行操作的顺序在所有的副本中能否保证多大程度的一致性。这里可能大家觉得有点奇怪,这种应该是要么是一致的顺序,要么是不一致的,为什么顺序还有程度呢?这里牵扯到另一个概念,叫因果序。

1. 因果顺序

通过前面的讨论我们知道通过物理时钟(即绝对参考系)来区分先后顺序的前提是所有节点的时钟完全同步,但目前并不现实。因此,在没有绝对参考系的情况下,在一个分布式系统中,你无法判断事件A是否发生在事件B之前,除非A和B存在某种依赖关系,即分布式系统中的事件仅仅是部分有序的。这里的依赖关系也就是因果关系。注意这里的因果关系和并发是两个不同的概念。如果两个操作存在依赖关系那么这两个操作肯定不是并发的。两个并发的操作,其必然没有依赖关系。所以对于并发的操作我们要满足互斥性。同样,对于一个数据系统来说,很多时候我们并不需要完全的线性一致性,我们只需要因果性就可以了,这样的化,系统的一致性要求就降低了,可能系统的其他方面的表现就有可能提升,比如吞吐量等。当时目前好像还没有看到这方面的实现,也有可能是工程上模型抽象尚不完善。
像数据库中的事务隔离的快照一致性读写实际上就满足了因果性。

举一个例子,比如对同一个数据连续执行了两个个操作,

  1. res=read01(k1)+1
  1. update(k1,res)

那么read01不能读到update之后的值,也就是说read01必须先于update发生,所以这两个操作的先后顺序必须是一致的,但是对其他数据的操作,和这两个操作的数据是不同的,则可以选择放在这个操作的前面或者后面。副本之间不需要保持一致性。这个具体的实现我也没有关注过,能够保证数据在副本之间的因果顺序的系统被称为满足因果一致性。

如果你熟悉像Git这样的分布式版本控制系统,那么其版本历史与因果关系图极其相似。通常,一个**提交(Commit)**发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并(Merge)**会在这些并发创建的提交相融合时创建。
拿git操作来举例来说倒是有点像,哈哈哈。

对应的,Lamport提出的时间戳(被称为Lamport时间戳),他的实现方式找到了这两篇参考01,02
通过Lamport时间戳的运行模式,我们可以得到一个因果一致性的系统。注意是因果一致性性不是线性一致性。

兰伯特时间戳与物理时间时钟没有任何关系,但是它提供了一个全序:如果你有两个时间戳,则计数器值大者是更大的时间戳。如果计数器值相同,则节点ID越大的,时间戳越大。
使兰伯特时间戳因果一致的关键思想如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大计数器值,并在每个请求中包含这个最大计数器值。当一个节点收到最大计数器值大于自身计数器值的请求或响应时,它立即将自己的计数器设置为这个最大值。这个只是lampart时间戳生成全局序列的方式,当时并不代表这顺序就是线性一致性的。他可能和真实的事件的发生顺序并不一致。
但是,上面的描述并没有解决对同一个数据操作的并发冲入问题(虽然这两个操作没有因果性)。还需要额外的手段来保持互斥性。(在上面的博客里有相关的案例)

2. 全序广播顺序

我们主要关注的是全序广播(total order broadcast),这个也被称为原子广播(atomic broadcast)

“原子广播”是一个传统的术语,非常混乱,而且与“原子”一词的其他用法不一致:它与ACID事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 )或原子寄存器(线性一致存储)有间接的联系

顺序保证的范围
每个分区各有一个主库的分区数据库,通常只在每个分区内维持顺序,这意味着它们不能提供跨分区的一致性保证(例如,一致性快照,外键引用)。 跨所有分区的全序是可能的,但需要额外的协调。

非正式地讲,它要满足两个安全属性:

  1. 可靠交付(reliable delivery): 没有消息丢失:如果消息被传递到一个节点,它将被传递到所有节点。
  2. 全序交付(totally ordered delivery): 消息以相同的顺序传递给每个节点。

正确的全序广播算法必须始终保证可靠性和有序性,即使节点或网络出现故障。当然在网络中断的时候,消息是传不出去的,但是算法可以不断重试,以便在网络最终修复时,消息能及时通过并送达(当然它们必须仍然按照正确的顺序传递)。
像ZooKeeper和etcd这样的共识服务实际上实现了全序广播,同样的,这也说明全序广播和实现分布式存储系统的线性一致性有着很大的关系。

8.并发的检测

在系统中,如何检测到两个错并发是并发的也是一个问题,如果两个线程同时去update一个数据,但是没有进行并发的互斥的话,可能就会发生更新丢失(也就是其中的一个操作会被直接覆盖,就像他从来没有发生过一样),这在有些场景下是不可接受的。
并发的检测也分为两种情况

  1. 单个副本的并发检测: 如果所有的写操作都是对当个副本进行操作的话,那么就是单副本的并发检测控制
  1. 多个副本的并发检测: 如果系统有多个副本,写入操作可以针对任何一个副本进行的话,那么就需要进行多副本的并发检测

1. 单副本的并发检测

单个副本的并发检测包括两种情况,一种是只有一个副本,比如MYSQL,还有就是多个副本写入,但是真正的写入只是通过master写入,其他的副本只是进行复制工作。
参考操作系统中,如果想要满足原子操作,有一个选择是先加锁,然后再执行,但是这个相对来说属于悲观锁,不管有没有并发都会先加锁,还有一种是只有检测到并发了才会进行互斥,这个就是CAS操作(compare and swap),这里的数据系统一般肯定是使用乐观锁,要不然加锁等操作太频繁了,对性能影响比较大。所以也是要模拟CAS操作,回想一下cas操作,就是你要知道原来的值是什么,然后如果值没有变,就可以替换为新的值。因为我们更新的是一条记录,可能有很多个字段,所以不可能对原值进行对比(太慢),这个时候我们可以在每个记录中增加一个字段,叫version,用version来代表每次操作的数据是否发生了变化,这样的话就需要version在每次write操作的时候都发生变化才行,每次进行write操作的时候先看看version是否和之前的一致,不一致的话就抛出异常,让客户端重试。每次write操作version字段都会自增加一。这样的话就完成了CAS的更新操作。
当然,这个只是对单个操作的并发有序的保证,如果是一系列的过程操作则是无法保证的(同一个事务中的很多update操作)。
在Elasticsearch中,每个doc都有一个_version字段,在update的时候都是现读取相关文档,然后在内存中修改,修改完之后再写回去,写回去的时候就要判断和之前读出来doc的_version是否是一致的,不一致的话就会报冲突(后面Elasticsearch又引入了_seq_no,_primary_term两个来供client来控制并发)。
单副本的并发检测一般使用版本号(version)来进行支持。

2. 多副本的并发检测

假如是多副本写入并且没有领导者的情况,除了对每个键使用版本号之外,还需要在每个副本中使用版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些值,以及保留哪些值作为兄弟。
所有副本的版本号集合称为版本向量(version vector)。这个想法的一些变体正在使用,但最有趣的可能是在Riak 2.0 中使用的分散版本矢量(dotted version vector)。我们不会深入细节,但是它的工作方式与版本向量非常相似。

9. 共识的局限性

共识算法对于分布式系统来说是一个巨大的突破:它为其他充满不确定性的系统带来了基础的安全属性(一致同意,完整性和有效性),然而它们还能保持容错(只要多数节点正常工作且可达,就能取得进展)。它们提供了全序广播,因此它们也可以以一种容错的方式实现线性一致的原子操作(参见“使用全序广播实现线性一致性存储”)。尽管如此,它们并不是在所有地方都用上了,因为好处总是有代价的。

1. 系统吞吐量下降

节点在做出决定之前对提议进行投票的过程是一种同步复制。如“同步与异步复制”中所述,通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失 —— 但是为了获得更好的性能,许多人选择接受这种风险。

2. 需要冗余的节点来完成容错

共识系统总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞(参阅“线性一致性的代价”)。

3. 扩容缩容机制比较复杂

大多数共识算法假定参与投票的节点是固定的集合,这意味着你不能简单的在集群中添加或删除节点。共识算法的**动态成员扩展(dynamic membership extension)**允许集群中的节点集随时间推移而变化,但是它们比静态成员算法要难理解得多。

4. 系统服务的稳定性可能受网络影响较大

共识系统通常依靠超时来检测失效的节点。在网络延迟高度变化的环境中,特别是在地理上散布的系统中,经常发生一个节点由于暂时的网络问题,错误地认为领导者已经失效。虽然这种错误不会损害安全属性,但频繁的领导者选举会导致糟糕的性能表现,因系统最后可能花在权力倾扎上的时间要比花在建设性工作的多得多。
有时共识算法对网络问题特别敏感。例如Raft已被证明存在让人不悦的极端情况:如果整个网络工作正常,但只有一条特定的网络连接一直不可靠,Raft可能会进入领导频繁二人转的局面,或者当前领导者不断被迫辞职以致系统实质上毫无进展。其他一致性算法也存在类似的问题,而设计能健壮应对不可靠网络的算法仍然是一个开放的研究问题。

10. 分布式系统不可能证明之FLP

你可能已经听说过作者Fischer,Lynch和Paterson之后的FLP结果,它证明,如果存在节点可能崩溃的风险,则不存在总是能够达成共识的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而这里我们正在讨论达成共识的算法,到底是怎么回事?

答案是FLP结果在异步系统模型中得到了证明,这是一种限制性很强的模型,它假定确定性算法不能使用任何时钟或超时。如果允许算法使用超时或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),则共识变为一个可解的问题。即使仅仅允许算法使用随机数,也足以绕过这个不可能的结果。
因此,FLP是关于共识不可能性的重要理论结果,但现实中的分布式系统通常是可以达成共识的。

11. 补充一个脑图来方便梳理

在这里插入图片描述

啊,累死我了,放个图歇歇
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值