分类目录归档:设计

API 设计最佳实践的思考

来源:阿里技术

前言

API 设计面临的挑战千差万别,很难有处处适用的准则,所以在讨论原则和最佳实践时,无论这些原则和最佳实践是什么,一定有适应的场景和不适应的场景。因此我们在下文中不仅提出一些建议,也尽量去分析这些建议在什么场景下适用,这样我们也可以有针对性地采取例外的策略。

为什么去讨论这些问题? API 是软件系统的核心,而软件系统的复杂度 Complexity 是大规模软件系统能否成功最重要的因素。但复杂度 Complexity 并非某一个单独的问题能完全败坏的,而是在系统设计尤其是API设计层面很多很多小的设计考量一点点叠加起来的(John Ousterhout老爷子说的Complexity is incremental【8】)。成功的系统不是有一些特别闪光的地方,而是设计时点点滴滴的努力积累起来的

 

继续阅读

发表在 设计 | 留下评论

集群调度系统的演进2-Kubernetes

来自:邵明岐

在这边文章,我会详细介绍一下 Kubernetes 的架构,您在看这篇文章之前,需要提前了解一下 Kubernetes 是什么,以及一些基本概念。

 

关于调度系统架构的文章,这是系列的第二篇,之前的第一篇请移步这这里阅读:集群调度系统的演进。在文章里面,通过介绍 Hadoop MRv1、YARN和Mesos调度系统,了解了调度系统的架构和演进过程,那么现在最新最流行的 Kubernetes 的架构是什么样子的呢?这篇文章就给大家介绍一下 Kubernetes 整体架构,并且会深入探讨其中 2 个比较深入的问题。

 

01
Kubernetes 的架构解析
 

首先,Kubernetes 的官方架构图是这样的。

 

 

这个架构图看起来会比较复杂,很难看懂,我把这个官方的架构图重新简化了一下,就会非常容易理解了:

 

 

  • ETCD :是用来存储所有 Kubernetes 的集群状态的,它除了具备状态存储的功能,还有事件监听和订阅、Leader选举的功能,所谓事件监听和订阅,各个其他组件通信,都并不是互相调用 API 来完成的,而是把状态写入 ETCD(相当于写入一个消息),其他组件通过监听 ETCD 的状态的的变化(相当于订阅消息),然后做后续的处理,然后再一次把更新的数据写入 ETCD。所谓 Leader 选举,其它一些组件比如 Scheduler,为了做实现高可用,通过 ETCD 从多个(通常是3个)实例里面选举出来一个做Master,其他都是Standby。
  • API Server:刚才说了 ETCD 是整个系统的最核心,所有组件之间通信都需要通过 ETCD,实际上,他们并不是直接访问 ETCD,而是访问一个代理,这个代理是通过标准的RESTFul API,重新封装了对 ETCD 接口调用,除此之外,这个代理还实现了一些附加功能,比如身份的认证、缓存等。这个代理就是 API Server。
  • Controller Manager:是实现任务的调度的,关于任务调度你可以参考之前的文章,简单说,直接请求 Kubernetes 做调度的都是任务,比如比如 Deployment 、Deamon Set 或者 Job,每一个任务请求发送给Kubernetes之后,都是由Controller Manager来处理的,每一个任务类型对应一个Controller Manager,比如 Deployment对应一个叫做 Deployment Controller,DaemonSet 对应一个 DaemonSet Controller。
  • Scheduler:是用来做资源调度的(具体资源调度的含义请参考之前的文章),Controller Manager会把任务对资源要求,其实就是Pod,写入到ETCD里面,Scheduler监听到有新的资源需要调度(新的Pod),就会根据整个集群的状态,给Pod分配到具体的节点上。
  • Kubelet:是一个Agent,运行在每一个节点上,它会监听ETCD中的Pod信息,发现有分配给它所在节点的Pod需要运行,就在节点上运行相应的Pod,并且把状态更新回到ETCD。
  • Kubectl: 是一个命令行工具,它会调用 API Server发送请求写入状态到ETCD,或者查询ETCD的状态。

 

这样是不是简单了很多。如果还是觉得不清楚,我们就用部署一个服务的例子来解释一个整个过程,假设你要运行一个多个实例的Nginx,那么在Kubernetes内部,整个流程是这样的:

  1. 通过kubectl命令行,创建一个包含Nginx的Deployment对象,kubectl会调用 API Server 往ETCD里面写入一个 Deployment对象。
  2. Deployment Controller 监听到有新的 Deployment对象被写入,就获取到对象信息,根据对象信息来做任务调度,创建对应的 Replica Set 对象。
  3. Replica Set Controller监听到有新的对象被创建,也读取到对象信息来做任务调度,创建对应的Pod来。
  4. Scheduler 监听到有新的 Pod 被创建,读取到Pod对象信息,根据集群状态将Pod调度到某一个节点上,然后更新Pod(内部操作是将Pod和节点绑定)。
  5. Kubelet 监听到当前的节点被指定了新的Pod,就根据对象信息运行Pod。

 

上面就是Kubernetes内部的是如何实现的整个 Deployment 被创建的过程。这个过程只是为了向大家解释每一个组件的职责,以及他们之间是如何相互协作的,忽略掉了很多繁琐的细节。

 

目前为止,我们有已经研究过了几个非常具有代表性的调度系统:Hadoop MRv1、YARN、Mesos和Kubernetes。我当时学习完这些调度系统的架构之后,在我脑子里面实际上有2个大大的疑问:

 

  1. Kubernetes是二次调度的架构么,和Mesos相比它的扩展性如何?
  2. 为什么所有调度系统都是无法横向扩展的?

后面我们就针对这两个问题深入讨论一下。

 

02
Kubernetes 是否是二层调度?

 

在 Google 的一篇关于他们内部的 Omega 的调度系统的论文,把调度系统分成三类:单体、二层调度和共享状态三种,按照它的分类方法,通常Google的 Borg被分到单体这一类,Mesos被当做二层调度,而Google自己的Omega被当做第三类“共享状态”。论文的作者实际上之前也是Mesos的设计者之一,后来去了Google设计新的 Omega 系统,并发表了论文,论文的主要目的是提出一种全新的“Shard State”的模式来同时解决调度系统的性能和扩展性的问题,但是实际我觉得 Shared State 模型太过理想化,根据这个模型开发的Omega系统,似乎在Google内部并没有被大规模使用,也没有任何一个大规模使用的调度系统采是采用 Shared State 模型。

 

 

因为Kubernetes的大部分设计是延续 Borg的,而且Kubernetes的核心组件(Controller Manager和Scheduler)缺省也都是绑定部署在一起,状态也都是存储在ETCD里面的的,所以通常大家会把Kubernetes也当做“单体”调度系统,实际上我并不赞同。

 

我认为 Kubernetes 的调度模型也完全是二层调度的,和 Mesos 一样,任务调度和资源的调度是完全分离的,Controller Manager承担任务调度的职责,而Scheduler则承担资源调度的职责。

 

 

实际上Kubernetes和Mesos调度的最大区别在于资源调度请求的方式:

  • 主动 Push 方式。是 Mesos 采用的方式,就是 Mesos 的资源调度组件(Mesos Master)主动推送资源 Offer 给 Framework,Framework 不能主动请求资源,只能根据 Offer 的信息来决定接受或者拒绝。
  • 被动 Pull 方式。是 Kubernetes 的方式,资源调度组件 Scheduler 被动的响应 Controller Manager的资源请求。

 

这两种方式所带来的不同,我会主要从下面 5 个方面来分析。另外注意,我所比较两者的优劣,都是从理论上做的分析,工程实现上会有差异,一些指标我也并没有实际测试过。

 

1)资源利用率:Kubernetes 胜出

理论上,Kubernetes 应该能实现更加高效的集群资源利用率,原因资源调度的职责完全是由Scheduler一个组件来完成的,它有充足的信息能够从全局来调配资源,然后 Mesos 缺却做不到,因为资源调度的职责被切分到Framework和Mesos Master两个组件上,Framework 在挑选 Offer 的时候,完全没有其他 Framework 的工作负载的信息,所以也不可能做出最优的决策。我们来举一个例子,比如我们希望把对耗费 CPU的工作负载和耗费内存的工作负载竟可能调度到同一台主机上,在Mesos里面不太容易做到,因为他们是属于不同的 Framework。

 

2)扩展性:Mesos胜出

从理论上讲,Mesos 的扩展性要更好一点。原因是Mesos的资源调度方式更容易让已经存在的任务调度迁移上来。我来举一个例子说明一下,假设已经有了一个任务调度系统,比如 Spark ,现在要迁移到集群调度平台上,理论上它迁移到 Mesos 比 Kubernetes 上更加容易。

如果迁移到 Mesos ,没有改变它原来的工作流程和逻辑,原来的逻辑是:来了一个作业请求,调度系统把任务拆分成小的任务,然后从资源池里面挑选一个节点来运行任务,并且记录挑选的节点 IP 和端口号,用来跟踪任务的状态。迁移到 Mesos 之后,还是一样的逻辑,唯一需要变化的是那个资源池,原来是自己管理的资源池,现在变成 Mesos 提供的Offer 列表。

如果迁移到 Kubernetes,则需要修改原来的基本逻辑来适配 Kubernetes,资源的调度完全需要调用外部的组件来完成,并且这个变成异步的。

 

3)灵活的任务调度策略:Mesos 胜出

Mesos 对各种任务的调度策略也要支持的更好。举个例子,如果某一个作业,需要 All or Nothing 的策略,Mesos 是能够实现的,但是 Kubernetes 完全无法支持。所以为的 All or Nothing 的意思是,价格整个作业如果需要运行 10 个任务,这 10个任务需要能够全部有资源开始执行,否则的话就一个都不执行。

 

4)性能:Mesos 胜出

Mesos 的性能应该更好,因为资源调度组件,也就是 Mesos Master 把一部分资源调度的工作甩给 Framework了,承担的调度工作更加简单,从数据来看也是这样,在多年之前 Twitter 自己的 Mesos 集群就能够管理超过 8万个节点,而 Kubernetes 1.3 只能支持 5千个节点。

 

5)调度延迟:Kubernetes 胜出

Kubernetes调度延迟会更好。因为Mesos的轮流给Framework提供Offer机制,导致会浪费很多时间在给不需要资源的 Framework 提供Offer。

 

03
为什么不支持横向扩展?

 

看到可能注意到了,几乎所有的集群调度系统都无法横向扩展(Scale Out),比如早期的 Hadoop MRv1 的管理节点是单节点,管理的集群上线是 5000 台机器,YARN 资源管理节点同时也只能有一个节点工作,其他都是备份节点,能够管理的机器的上限1万个节点,Mesos通过优化,一个集群能够管理 8 万个节点,Kubernetes 目前的 1.13 版本,集群管理节点的上限是 5000 个节点。

 

所有的集群调度系统的架构都是无法横向扩展的,如果需要管理更多的服务器,唯一的办法就是创建多个集群。集群调度系统的架构看起来都是这个样子的:

 

中间的 Scheduler(资源调度器)是最核心的组件,虽然通常是由多个(通常是3个)实例组成,但是都是单活的,也就是说只有一个节点工作,其他节点都处于 Standby 的状态。为什么会这样呢?看起来不符合互联网应用的架构设计原则,现在大部分互联网的应用通过一些分布式的技术,能够很容易的实现横向扩展,比如电商的应用,在促销的时候,通过往集群里面添加服务器,就能够提升服务的吞吐量。如果是按照互联网应用的架构,看起来应该是这样的:

 

Scheduler 应该是可以多活的,有任意多的实例一起对外提供服务,无论是资源的消费者,还是资源的提供者在访问 Scheduler 的时候,都需要经过一个负载均衡的组件或者设备,负责把请求分配给某一个 Scheduler 实例。为什么这种架构在集群调度系统里面变得不可行么?为了理解这件事情,我们先通过一个互联网应用的架构的例子,来探讨一下具备横向扩展需要哪些前提条件。

04
横向扩展架构的前提条件

 

假设我们要实现这样一个电商系统吧:

  1. 这是一个二手书的交易平台,有非常多的卖家在平台上提供二手书,我们暂且把每一本二手书叫做库存
  2. 卖家的每一个二手书库存,根据书的条码,都可以找到图书目录中一本书,我们把这本书叫做商品
  3. 卖家在录入二手书库存的时候,除了录入是属于哪一个商品,同时还需要录入其他信息,比如新旧程度、价钱、发货地址等等。
  4. 买家浏览图书目录,选中一本书,然后下单,订单系统根据买家的要求(价格偏好、送货地址等),用算法从这本书背后的所有二手书库存中,匹配一本符合要求的书完成匹配,我们把这个过程叫订单匹配好了。

 

这样一个系统,从模型上看这个电商系统和集群调度系统没啥区别,这个里面有资源提供者(卖家),提供某种资源(二手书),组成一个资源池(所有二手书),也有资源消费者(买家),提交自己对资源的需求,然后资源调度器(订单系统)根据算法自动匹配一个资源(一本二手书),但是很显然,这个电商系统是可以设计成横向扩展架构的,这是为什么呢,这个电商系统和集群调度系统的区别到底在什么地方 我想在回答这个问题之前,我们先来回答另外一个问题:这个电商系统横向扩展的节点数是否有上限,上限是多少,这个上限是有什么因素决定的?

 

系统理论上的并发数量决定了横向扩展的节点数

 

怎么来理解这个事情呢,假设系统架构设计的时候,不考虑任何物理限制(比如机器的资源大小,带宽等),能够并发处理 1000个请求,那么很显然,横向扩展的节点数量上限就是1000,应为就算部署了 1001个节点,在任何时候都有一个节点是处于空闲状态,部署更多的节点已经完全无法提高系统的性能。我们下面需要想清楚的问题其实就变成了:系统理论上能够并发处理请求的数量是多少,是有什么因素决定的。

 

系统的并发数量是由“独立资源池”的数量决定的

 

“独立资源池”是我自己造出来的一个词,因为实在想不到更加合适的。还是以上面的电商系统为例,这个订单系统的理论上能够处理的并发请求(订购商品请求)数量是由什么来决定的呢?先看下面的图吧:

 

在订单系统在匹配需求的时候,实际上应该是这样运行的,在订单请求来了之后,根据订单请求中的购买的商品来排队,购买同一个商品的请求被放在一个队列里面,然后订单的调度系统开始从队列里面依次处理请求,每次做订单匹配的时候,都需根据当前商品的所有库存,从里面挑选一个最佳匹配的库存。虽然在实现这个系统的时候,这个队列不见得是一个消息队列,可能会是一个关系型数据库的锁,比如一个购买《乔布斯传》的一个订单,系统在处理的时候需要先要从所有库存里面查询出《乔布斯传》的库存,将库存记录锁住,并且做订单匹配并且更新库存(将生成订单的库存商品设置为”不可用”状态)之后,才会将数据库锁释放,这个时候实际上所有后续购买《乔布斯传》的订单请求都在队列中等待,也有些系统在实现的时候采用“乐观锁”,就是在每一次订单处理的时候,并不会在第一开始就锁住库存信息,而是在最后一步更新库存的时候才会锁住,如果发生两个订单匹配到了同一个库存物品,那么其中一个订单处理就需要完全放弃然后重试。这两种实现方式不太一样,但是本质都是相同的。

 

所以从上面的讨论可以看出来,之所以所有购买《乔布斯传》的订单需要排队处理,原因是因为每一次做订单匹配的时候,需要所有乔布斯传的这个商品的所有库存信息,并且最后会修改(占用)一部分库存信息的状态。在这个订单匹配的场景里面,我们就把乔布斯传的所有库存信息叫做一个“独立资源池”,订单匹配这个“调度系统”的最大并发数量就完全取决于独立资源池的数量,也就是商品的数量。我们假设一下,如果这个二手书的商城只卖《乔布斯传》一本书,那么最后所有的请求都需要排队,这个系统也几乎是无法横向扩展的。

 

05
集群调度系统的“独立资源池”数量是 1

 

我们再来看一下集群调度系统,每一台服务器节点都是一个资源,每当资源消费者请求资源的时候,调度系统用来做调度算法的“独立资源池”是多大呢?答案应该是整个集群的资源组成的资源池,没有办法在切分了,因为:

 

  1. 调度系统的职责就是要在全局内找到最优的资源匹配。
  2. 另外,就算不需要找到最优的资源匹配,资源调度器对每一次资源请求,也没办法判断应该从哪一部分资源池中挑选资源。

 

正是因为这个原因,“独立资源池”数量是 1,所以集群调度系统无法做到横向扩展。

发表在 设计 | 留下评论

集群调度系统的演进1-MRv1 Yarn Mesos

来自:倒霉砖家

Kubernetes 已经成为容器编排领域的事实标准,将来所有应用都会在 Kubernetes 上开发和运行,这个系列文章的目的是深入浅出的介绍 Kubernetes 底层实现的原理。

 

Kubernetes 是一个集群调度系统,今天这篇文章主要是介绍 Kubernetes 之前一些集群调度系统的架构,通过梳理他们的设计思路和架构特点,我们能够学习到集群调度系统的架构的演进过程,以及在架构设计时需要考虑的主要问题,对理解 Kubernetes 的架构会非常有帮助。 

 

基本概念

 

我们需要先了解集群调度系统里面的一些基本的概念,为了方便大家理解,我通过一个例子来解释这些概念,假设我们要实现一个分布式的定时任务系统(分布式的 Cron),这个系统管理了一组 Linux 主机,用户通过系统提供的的 API / UI 定义定时任务(就类似在 Linux 里面定义 Crontab) ,系统会根据任务的定义,来定时来执行相应的任务,在这个例子里面有如下基本概念:

  • 集群(Cluster):这个系统管理的 Linux 主机组成一个资源池,用来运行任务,这个资源池就是集群。
  • 作业(Job):就是定义集群如何去执行任务,在例子里面 Crontab 就是一个简单的作业,里面明确的告诉了集群需要在什么时间(时间间隔) ,做什么事情(执行的脚本)。一些作业的定义会复杂很多,比如还会定义一个作业分几个任务做完,以及任务之间的依赖关系,还包括每一个任务对资源的需求。
  • 任务(Task):作业需要被调度成具体的执行任务,如果我们定义了一个作业是每天晚上凌晨 1 点执行一个脚本,那么在每天凌晨 1点被执行的这个脚本进程就是任务。

 

在设计集群调度系统的时候,这个调度系统的核心任务也就是 2 个:

  • 任务调度。作业提交给集群调度系统之后,需要对提交的作业拆分成具体的执行任务,并且跟踪和监控任务的执行结果。在分布式 Cron 的例子中,调度系统需要按照作业的要求定时启动进程,如果进程执行失败,需要重试等,一些复杂的场景,比如 Hadoop 的 Map Reduce ,调度系统需要把 Map Reduce 任务拆分成相应的多个 Map 和 Reduce 任务,并且最终拿到任务执行结果的数据。
  • 资源调度:本质上是对任务和资源做匹配,根据集群中主机的资源使用情况,分配合适的资源来运行任务。和操作系统的进程调度算法比较类似,资源调度的主要目标是,在固定的资源供给的情况下,尽可能提高资源使用率,减少任务等待的时间(任务等待资源去执行的时间),减少任务运行的延迟或者响应时间(如果是批量任务的话,就是任务从开始执行到结束的时间,如果在线响应式任务的话,比如 Web 应用,就是每一次响应请求的时间),尽可能公平(资源公平的被分配到所有任务)的同时,还需要考虑任务的优先级。这些目标里面有一些是有冲突的,需要平衡,比如资源利用率和响应时间,公平和优先级。

 

Hadoop MRv1  

 

Map Reduce 是一种并行计算模型,Hadoop 是可以运行这种并行计算的集群管理平台,其中 MRv1 是 Hadoop 平台的 Map Reduce 任务调度引擎的第一代版本,简单来说,用户定义了 一个 Map Reduce 计算,提交给 Hadoop 之后,由 MRv1 负责在集群上调度和执行这个作业,并且返回计算结果。 MRv1 的架构看起来是这样的:

 

架构还是比较简单的,标准的 Master/Slave 的架构,有 2 个核心组件:

  • Job Tracker 是集群主要的管理组件,同时承担了资源调度和任务调度的责任。
  • Task Tracker 运行在集群的每一台机器上,负责在主机上运行具体的任务,并且汇报状态。

随着 Hadoop 的流行和各种需求的增加,MRv1 有如下问题需要改进:

  1. 性能有一定瓶颈:支持管理的最大节点数是 5千个节点,支持运行的任务最大数量 4万,还有一定的提高空间。
  2. 不够灵活,无法扩展支持其他任务类型。Hadoop 生态里面除了 Map Reduce 类型任务,还有其他很多任务类型需要调度,比如 Spark,Hive,HBase,Storm,Oozie 等,所以需要一个更加通用的调度系统,能够支持和扩展更多的任务类型。
  3. 资源利用率比较低。MRv1 给每个节点静态配置了固定数目的 Slot ,每个 Slot 也只能够运行的特定的任务的类型(Map or Reduce),这就导致资源利用率的问题,比如大量 Map 任务在排队等待空闲资源,但实际上机器有大量 Reduce 的 Slot 被闲置。
  4. 多租户和多版本的问题。比如不同部门在同一个集群里面运行任务,但是彼此是逻辑上隔离的,或者在同一个集群里面运行不同版本的 Hadoop。

 

YARN

 

YARN( Yet Another Resource Negotiator)是 Hadoop 的第二代调度系统,主要目的就是为了解决 MRv1 中的各种问题。YARN 的架构看起来是这样的

YARN 简单的理解就是,相对于 MRv1 的主要改进就是,把原来的 JobTrack 的职责,拆分给两个不同的组件来完成:Resource Manager 和 Application Master

  • Resource Manager:承担资源调度的职责,管理所有资源,将资源分配给不同类型的任务,并且通过“可插拔”的架构来很容易的扩展资源调度算法。
  • Application Master:承担任务调度的职责,每一个作业(在 YARN 里面叫做 Application)都会启动一个对应的 Application Master,它来负责把作业拆分成具体的任务、向 Resource Manager 申请资源、启动任务、跟踪任务的状态并且汇报结果。

 

我们看看这种架构改变是如何解决 MRv1 的各种问题的:

  • 将原来的 Job Tracker 的任务调度职责拆分出来,大幅度提高了性能。原来的 Job Tracker 的任务调度的职责拆分出来由 Application Master 承担,并且 Application Master 是分布式的,每一个实例只处理一个作业的请求,将原来能够支撑的集群节点最大数量,从原来的5千节点提升到1万节点。
  • 任务调度的组件,Application Master,和资源调度解耦,而且是根据作业的请求而动态创建的,一个 Application Master 实例只负责一个作业的调度,也就更加容易支持不同类型的作业。
  • 引入了容器隔离技术,每一个任务都是在一个隔离的容器里面运行,根据任务对资源的需求来动态分配资源,大幅提高了资源利用率。不过有一个缺点是,YARN 的资源管理和分配,只有内存一个维度。

 

Mesos 的架构

 

YARN 的设计目标依然是服务于 Hadoop 生态的调度,Mesos 的目标更近一步,被设计成一个通用的调度系统,能够管理整个数据中心的作业,看的出来 Mesos 的架构设计很多借鉴了 YARN,将作业调度和资源调度分别由不同的模块承担,不过 Mesos 和 YARN 很大不同的地方是对资源的调度方式,设计了一个叫非常独特的 Resource Offer 的机制,进一步释放了资源调度的压力,增加了作业调度的扩展性。

Mesos 的主要组件是:

  • Mesos Master ,单纯是承担资源分配和管理的组件,的对应到 YARN 里面就是那个 Resource Manager,不过工作方式会稍微有些不太一样,后面会讲到。
  • Framework,承担作业调度,不同的作业类型都会有一个对应的 Framework,比如负责 Spark 作业的 Spark Framework。

 

Mesos 的 Resource Offer

 

看起来 Mesos 和 YARN 架构非常类似,不过实际上在资源管理的方面, Mesos 的 Master 有非常独特(甚至有些奇怪)的 Resource Offer 机制

  • YARN 中 Resource Manager 提供资源的方式是被动的,当资源的消费者(Application Master) 需要资源的时候,会调用 Resource Manager 的接口来获取到资源,Resource Manager 只是被动的响应 Application Master 的需求。
  • Mesos 的 Master 提供资源的方式是主动的。Mesos 中的 Master 会定期的主动推送当前的所有可用的资源(就是所谓的 Resource Offer,后面统一都叫 Offer)给 Framework,Framework 如果有任务需要被执行,不能主动申请资源,只有当接收到 Offer 的时候,从 Offer 里面挑选满足要求的资源来接受(在 Mesos 里面这个动作叫做 Accept),剩余的 Offer 就都拒绝掉(这个动作叫做 Reject),如果这一轮 Offer 里面没有足够能够满足要求的资源,只能等待下一轮 Master 提供 Offer。

 

相信大家看到这个主动的 Offer 机制的时候,会和我有同样的感觉,就是效率比较低,会产生如下问题:

  • 任何一个 Framework 的决策效率会影响整体的效率。为了一致性,Master 一次只能给一个 Framework 提供 Offer,等待这个 Framework 挑选完 Offer 之后,再把剩余的提供给下一个 Framework,这样的话,任何一个 Framework 做决策的效率就会影响整体的效率;
  • “浪费”很多时间在不需要资源的 Framework 上。 Mesos 并不知道哪个 Framework 需要资源,所以会出现有资源需求的 Framework 在排队等待 Offer,但是没有资源需求的 Framework 却频繁收到 Offer 的情况。

 

针对上面的问题,Mesos 提供了一些机制来做一定程度的缓解,比如给 Framework 设置一个做决策的超时时间,或者允许 Framework 可以通过设置成 Suppress 状态来表示不需要接受 Offer等,因为不是本次讨论的重点,所以细节就不展开了。

 

实际上,Mesos 采用这种主动 Offer 的机制,也是有一些明显的优点的:

  • 性能明显提高。根据模拟测试一个集群最大可以支撑 10 万个节点,Twitter 的生产环境最大集群支撑 8 万个节点,主要原因是 Mesos Master主动 Offer 的机制,进一步简化了 Mesos Master 的工作职责,Mesos 中将资源调度的过程(资源 —> 任务的匹配)分成了 2 个阶段:资源 —> Offer —> 任务 。Mesos Master 只负责完成第一个阶段,第二个阶段的匹配交给 Framework 来完成。
  • 更加灵活,能够满足更加负责的任务调度的策略。举个例子,All Or Nothings 的资源使用策略。

 

Mesos 的调度算法 DRF(Dominant Resource Fairness)

 

关于 DRF 算法,其实对我们理解 Mesos 架构并没有什么关系,但是在 Mesos 中却非常核心和重要,所以多啰嗦几句。

 

上面说了,Mesos 是轮流给 Framework 提供 Offer 的,那么每次该挑选哪个 Framework 提供 Offer 呢?这就是 DRF 算法要解决核心的问题, 基本原则就是要兼顾公平和效率,在经量满足所有 Framework 对资源需求的同时,也要应该尽可能公平,避免某一个 Framework 占用太多资源而把其他 Framework 给“饿死”。

 

DRF 是 min-max fairness 算法的一个变形,简单来说就是每次都挑选支配资源占用率(Dominant Resource Usage)最低的那个 Framework 提供 Offer。如何计算 Framework 的“支配资源占用率”呢?就是从 Framework 占用的所有资源类型里面,挑选资源占用率最小的那个资源类型最为支配资源(Dominant Resource),它的资源占用率就是这个 Framework 的支配资源占用率( Dominant Resource Usage),举个例子,一个 Framework X 的 CPU 占用了整体资源的 20%,内存是 30%,磁盘是 10%,那么这个 Framework 的支配资源占用率就是 10%,官方术语把磁盘叫做支配资源, 这个 10% 叫做支配资源占用率。

 

DRF 的最终目的是把资源平均的分配给所有 Framework,如果一个 Framework X 在这一轮 Offer 中接受(Accept Offer)了过多的资源,那么就要等更长的时间才能获得下一轮 Offer 的机会。不过仔细的读者会发现,这个算法里面有一个假设,就是 Framework 接受了资源之后,会很快释放掉,否则就会有 2 个后果:

  1. 其他 Framework 被“饿死“。某个 Framework A 一次性的接受了集群中大部分资源,并且任务一直运行不退出,这样大部分资源就被 Framework A 一直霸占了,其他 Framework 就没法获得资源了。
  2. 自己被饿死。因为这个 Framework 的支配资源占用率一直很高,所以长期无法获得 Offer 的机会,也就没法运行更多的任务。

 

所以实际上,Mesos 只适合调度短任务,而且实际上,在企业的数据中心的资源 80% 都是被短任务消耗掉的(数据实时查询,日志流式计算,人工智能/大数据计算等),另外根据 Mesos 的 Paper 里面的数据,Facebook 的 Hadoop 平台的任务平均时长是 84 秒。

 

总结一下

 

从大的架构上,所有调度系统的架构都是 Master / Slave 的架构,Slave 端安装在每一台需要管理的机器上,用来收集主机信息,在主机上执行任务。Master 主要负责做资源调度和任务调度,资源调度对性能要求比较高,任务调度对可扩展性要求较高,总体趋势是讲这两类职责解耦,分别由不同的组件来完成。

 

    已同步到看一看

     

     

    发表在 设计 | 留下评论

    分布式系统的Raft算法

    分布式系统的Raft算法

      来自:Jdon

    过去, Paxos一直是分布式协议的标准,但是Paxos难于理解,更难以实现,Google的分布式锁系统Chubby作为Paxos实现曾经遭遇到很多坑。

      来自Stanford的新的分布式协议研究称为Raft,它是一个为真实世界应用建立的协议,主要注重协议的落地性和可理解性。

      在了解Raft之前,我们先了解Consensus一致性这个概念,它是指多个服务器在状态达成一致,但是在一个分布式系统中,因为各种意外可能,有的服务器可能会崩溃或变得不可靠,它就不能和其他服务器达成一致状态。这样就需要一种Consensus协议,一致性协议是为了确保容错性,也就是即使系统中有一两个服务器当机,也不会影响其处理过程。

      为了以容错方式达成一致,我们不可能要求所有服务器100%都达成一致状态,只要超过半数的大多数服务器达成一致就可以了,假设有N台服务器,N/2 +1 就超过半数,代表大多数了。

      Paxos和Raft都是为了实现Consensus一致性这个目标,这个过程如同选举一样,参选者需要说服大多数选民(服务器)投票给他,一旦选定后就跟随其操作。Paxos和Raft的区别在于选举的具体过程不同。

      在Raft中,任何时候一个服务器可以扮演下面角色之一:

    1. Leader: 处理所有客户端交互,日志复制等,一般一次只有一个Leader.
    2. Follower: 类似选民,完全被动
    3. Candidate候选人: 类似Proposer律师,可以被选为一个新的领导人。

    Raft阶段分为两个,首先是选举过程,然后在选举出来的领导人带领进行正常操作,比如日志复制等。下面用图示展示这个过程:

    1. 任何一个服务器都可以成为一个候选者Candidate,它向其他服务器Follower发出要求选举自己的请求:

    2. 其他服务器同意了,发出OK。

    注意如果在这个过程中,有一个Follower当机,没有收到请求选举的要求,因此候选者可以自己选自己,只要达到N/2 + 1 的大多数票,候选人还是可以成为Leader的。

    3. 这样这个候选者就成为了Leader领导人,它可以向选民也就是Follower们发出指令,比如进行日志复制。

    4. 以后通过心跳进行日志复制的通知

    5. 如果一旦这个Leader当机崩溃了,那么Follower中有一个成为候选者,发出邀票选举。

    6. Follower同意后,其成为Leader,继续承担日志复制等指导工作:

    值得注意的是,整个选举过程是有一个时间限制的,如下图:

      Splite Vote是因为如果同时有两个候选人向大家邀票,这时通过类似加时赛来解决,两个候选者在一段timeout比如300ms互相不服气的等待以后,因为双方得到的票数是一样的,一半对一半,那么在300ms以后,再由这两个候选者发出邀票,这时同时的概率大大降低,那么首先发出邀票的的候选者得到了大多数同意,成为领导者Leader,而另外一个候选者后来发出邀票时,那些Follower选民已经投票给第一个候选者,不能再投票给它,它就成为落选者了,最后这个落选者也成为普通Follower一员了。

    日志复制

      下面以日志复制为例子说明Raft算法,假设Leader领导人已经选出,这时客户端发出增加一个日志的要求,比如日志是”sally”:

    2. Leader要求Followe遵从他的指令,都将这个新的日志内容追加到他们各自日志中:

    3.大多数follower服务器将日志写入磁盘文件后,确认追加成功,发出Commited Ok:

    4. 在下一个心跳heartbeat中,Leader会通知所有Follwer更新commited 项目。

    对于每个新的日志记录,重复上述过程。

    如果在这一过程中,发生了网络分区或者网络通信故障,使得Leader不能访问大多数Follwers了,那么Leader只能正常更新它能访问的那些Follower服务器,而大多数的服务器Follower因为没有了Leader,他们重新选举一个候选者作为Leader,然后这个Leader作为代表于外界打交道,如果外界要求其添加新的日志,这个新的Leader就按上述步骤通知大多数Followers,如果这时网络故障修复了,那么原先的Leader就变成Follower,在失联阶段这个老Leader的任何更新都不能算commit,都回滚,接受新的Leader的新的更新。

    总结:目前几乎所有语言都已经有支持Raft算法的库包,具体可参考:raftconsensus.github.io

    英文动画演示Raft

    CAP定理

    分布式Paxos算法

    ZooKeeper在服务发现中应用

    分布式事务

     

     

    发表在 设计 | 留下评论

    如何浅显易懂地解说 Paxos 的算法?

    来自:知乎

    看了几篇前面的答案,感觉都是为了逻辑的验密性,进行了大篇幅的推理,这样确实非常严谨,但是理解起来就要废一番功夫了。我就不用一步一步的推理来描述了,这样虽然丧失一些严密性,但是会尽量提高可读性,争取让每个人都能理解这个算法的要点。

    Paxos算法背景介绍: 
    Paxos算法是分布式技术大师Lamport提出的,主要目的是通过这个算法,让参与分布式处理的每个参与者逐步达成一致意见。用好理解的方式来说,就是在一个选举过程中,让不同的选民最终做出一致的决定。 
    Lamport为了讲述这个算法,假想了一个叫做Paxos的希腊城邦进行选举的情景,这个算法也是因此而得名。在他的假想中,这个城邦要采用民主提议和投票的方式选出一个最终的决议,但由于城邦的居民没有人愿意把全部时间和精力放在这种事情上,所以他们只能不定时的来参加提议,不定时来了解提议、投票进展,不定时的表达自己的投票意见。Paxos算法的目标就是让他们按照少数服从多数的方式,最终达成一致意见。

    Paxos算法的具体情况:

    1、在整个提议和投票过程中,主要的角色就是“提议者”(向“接受者”提出提议)和“接受者”(收到“提议者”的提议后,向“提议者”表达自己的意见)。

    2、整个算法的大致过程为:
    第一阶段:因为存在多个“提议者”,如果都提意见,那么“接受者”接受谁的不接受谁的?太混乱了。所以,要先明确哪个“提议者”是意见领袖有权提出提议,未来,“接受者”们就主要处理这个“提议者”的提议了(这样,也可以在提出提议时就尽量让意见统一,谋求尽早形成多数派)。
    第二阶段:由上阶段选出的意见领袖提出提议,“接受者”反馈意见。如果多数“接受者”接受了一个提议,那么提议就通过了。

    3、必须要了解的其他相关背景:
    1)怎么明确意见领袖呢?通过编号。每个“提议者”在第一阶段先报个号,谁的号大,谁就是意见领袖。如果不好理解,可以想象为贿选。每个提议者先拿着钞票贿赂一圈“接受者”,谁给的钱多,第二阶段“接受者”就听谁的。(注:这里和下文提到的“意见领袖”,并不是一个新的角色,而是代表在那一轮贿赂成功的“提议者”。所以,请把意见领袖理解为贿赂中胜出的“提议者”即可)
    2)有个跟选举常识不一样的地方,就是每个“提议者”不会执着于让自己的提议通过,而是每个“提议者”会执着于让提议尽快达成一致意见。所以,为了这个目标,如果“提议者”在贿选的时候,发现“接受者”已经接受过前面意见领袖的提议了,即便“提议者”贿选成功,也会默默的把自己的提议改为前面意见领袖的提议。所以一旦贿赂成功,胜出的“提议者”再提出提议,提议内容也是前面意见领袖的提议(这样,在谋求尽早形成多数派的路上,又前进了一步)
    3)钱的多少很重要,如果钱少了,无论在第一还是第二阶段“接受者”都不会尿你,直接拒绝。
    4)上面2)中讲到,如果“提议者”在贿选时,发现前面已经有意见领袖的提议,那就将自己的提议默默改成前面意见领袖的提议。这里有一种情况,如果你是“提议者”,在贿赂的时候,“接受者1”跟你说“他见过的意见领袖的提议是方案1”,而“接受者2”跟你说“他见过的意见领袖提议是方案2”,你该怎么办?这时的原则也很简单,还是:钱的多少很重要!你判断一下是“接受者1”见过的意见领袖有钱,还是“接受者2”见过的意见领袖有钱?如何判断呢?因为“接受者”在被“提议者”贿赂的时候,自己会记下贿赂的金额。所以当你贿赂“接受者”时,一旦你给的贿赂多而胜出,“接受者”会告诉你两件事情:a.前任意见领袖的提议内容(如果有的话),b.前任意见领袖当时贿赂了多少钱。这样,再面对刚才的情景时,你只需要判断一下“接受者1”和“接受者2”告诉你的信息中,哪个意见领袖当时给的钱多,那你就默默的把自己的提议,改成那个意见领袖的提议。
    5)最后这一部分最有意思,但描述起来有点绕,如果不能一下子就理解可以先看后面的例子。在整个选举过程中,每个人谁先来谁后到,“接受者”什么时间能够接到“提议者”的信息,是完全不可控的。所以很可能一个意见领袖已经产生了,但是由于这个意见领袖的第二阶段刚刚开始,绝大部分“接受者”还没有收到这个意见领袖的提议。结果,这时突然冲进来了一个新的土豪“提议者”,那么这个土豪“提议者”也是有机会让自己的提议胜出的!这时就形成了一种博弈:a.上一个意见领袖要赶在土豪“提议者”贿赂到“接受者”前,赶到“接受者”面前让他接受自己的提议,否则会因为自己的之前贿赂的钱比土豪少而被拒绝。b.土豪“提议者”要赶在上一个意见领袖将提议传达给“接受者”前,贿赂到“接受者”,否则土豪“提议者”即便贿赂成功,也要默默的将自己的提议改为前任意见领袖的提议。这整个博弈的过程,最终就看这两个“提议者”谁的进展快了。但最终一定会有一个意见领袖,先得到多数“接受者”的认可,那他的提议就胜出了。

    4、总结
    好啦,故事到这里基本讲述完了,咱们来总结一下,其实Paxos算法就下面这么几个原则:
    1)Paxos算法包括两个阶段:第一个阶段主要是贿选,还没有提出提议;第二个阶段主要根据第一阶段的结果,明确接受谁的提议,并明确提议的内容是什么(这个提议可能是贿选胜出“提议者”自己的提议,也可能是前任意见领袖的提议,具体是哪个提议,见下面第3点原则)。
    2)编号(贿赂金额)很重要,无论在哪个阶段,编号(贿赂金额)小的,都会被鄙视(被拒绝)。
    3)在第一阶段中,一旦“接受者”已经接受了之前意见领袖的提议,那后面再来找这个“接受者”的“提议者”,即便在贿赂中胜出,也要被洗脑,默默将自己的提议改为前任意见领袖的提议,然后他会在第二阶段提出该提议(也就是之前意见领袖的提议,以力争让大家的意见趋同)。如果“接受者”之前没有接受过任何提议,那贿选胜出的“提议者”就可以提出自己的提议了。

    5、举例

    最后举个例子,加深一下印象:

    有两个“提议者”和三个“接受者”。

    1)首先“提议者1”贿赂了3个“接受者”

    2)3个“接受者”记录下贿赂金额,因为目前只有一个“提议者”出价,因此$1就是最高的了,所以“接受者”们返回贿赂成功。此外,因为没有任何先前的意见领袖提出的提议,因此“接受者”们告诉“提议者1”没有之前接受过的提议(自然也就没有上一个意见领袖的贿赂金额了)。

    3)“提议者1”向“接受者1”提出了自己的提议:1号提议,并告知自己之前已贿赂$1。

    4)“接受者1”检查了一下,目前记录的贿赂金额就是$1,于是接受了这一提议,并把1号提议记录在案。

    5)在“提议者1”向“接受者2”“接受者3”发起提议前,土豪“提议者2”出现,他开始用$2贿赂“接受者1”与“接受者2”。

    6)“接受者1”与“接受者2”立刻被收买,将贿赂金额改为$2。但是,不同的是:“接受者1”告诉“提议者2”,之前我已经接受过1号提议了,同时1号提议的“提议者”贿赂过$1;而,“接受者2”告诉“提议者2”,之前没有接受过其他意见领袖的提议,也没有上一个意见领袖的贿赂金额。

    7)这时,“提议者1”回过神来了,他向“接受者2”和“接受者3”发起1号提议,并带着信息“我前期已经贿赂过$1”。

    8)“接受者2”“接受者3”开始答复:“接受者2”检查了一下自己记录的贿赂金额,然后表示,已经有人出价到$2了,而你之前只出到$1,不接受你的提议,再见。但“接受者3”检查了一下自己记录的贿赂金额,目前记录的贿赂金额就是$1,于是接受了这一提议,并把1号提议记录在案。

    9)到这里,“提议者1”已经得到两个接受者的赞同,已经得到了多数“接受者”的赞同。于是“提议者1”确定1号提议最终通过。

    10)下面,回到“提议者2”。刚才说到,“提议者2”贿赂了“接受者1”和“接受者2”,被“接受者1”告知:“之前已经接受过1号提议了,同时1号提议的‘提议者’贿赂过$1”,并被“接受者2”告知:“之前没有接到过其他意见领袖的提议,也没有其他意见领袖的贿赂金额”。这时“提议者2”,拿到信息后,判断一下,目前贿赂过最高金额(即$1)的提议就是1号提议了,所以“提议者2”默默的把自己的提议改为与1号提议一致,然后开始向“接受者1”“接受者2”发起提议(提议内容仍然是1号提议),并带着信息:之前自己已贿赂过$2。

    11)这时“接受者1”“接受者2”收到“提议者2”的提议后,照例先比对一下贿赂金额,比对发现“提议者2”之前已贿赂$2,并且自己记录的贿赂金额也是$2,所以接受他的提议,也就是都接受1号提议。

    12)于是,“提议者2”也拿到了多数派的意见,最终通过的也是1号提议。

    这里再多说一句:

    回到上面的第5)步,如果“提议者2”第一次先去贿赂“接受者2”“接受者3”会发生什么?那很可能1号提议就不会成为最终选出的提议。因为当“提议者2”先贿赂到了“接受者2”“接受者3”,那等“提议者1”带着议题再去找这两位的时候,就会因为之前贿赂的钱少($1<$2)而被拒绝。所以,这也就是刚才讲到可能存在博弈的地方:a.“提议者1”要赶在“提议者2”贿赂到“接受者2”“接受者3”之前,让“接受者2”“接受者3”接受自己的意见,否则“提议者1”会因为钱少而被拒绝;b.“提议者2”要赶在“提议者1”之前贿赂到“接受者”,否则“提议者2”即便贿赂成功,也要默默的将自己的提议改为“提议者1”的提议。但你往后推演会发现,无论如何,总会有一个“提议者”的提议获得多数票而胜出。

    以上,只是把大致的Paxos算法的思路介绍了一下,因为情景实在太复杂,比如:“提议者”、“接受者”如果是4个、5个……,比如:“提议者”与“接受者”之间的交互谁先谁后,等等各类情况。但是,其实都是能够严谨的推导出最后能够选出一个多数派的,不过篇幅就会太长了。大家有兴趣可以按照上面的思路,自己再模拟模拟“提议者”“接受者”数量或多或少,交互或先或后的各种情况,结果肯定是最终唯一一个提议会获得多数票而胜出。

    回想当时自己看Paxos算法时,走过很多的弯路,花了很长时间,这篇文章也是结合自己看Paxos算法时的一些心得所写,希望对初学者能有些启发。

    ———————————————-

    之前的回答本来就觉得一些细节处并不严谨,现在回看=/=。我觉得严谨是一个讨论技术的必要条件,觉得现在也有能力写的严谨,于是想把回答改的尽量严谨,最后发现不如重写,顺便补充了我想补充的内容,结果就是更长=.=。Paxos是个精巧,又强大的协议,仅从过程的复杂度来说,确实如作者本人一再说的那样是个“简单的协议”,但是可以从非常多的角度来理解它为何正确,而原本的流程也并不适合直接工程化,这也是大概为什么工程上它存在如此多的变体。希望这个回答的让人更快的感受paxos的魅力,建立一个初步印象的同时不给人以误导。最后依然推荐larmport自己写的和paxos相关的三篇论文:<< The Part-Time Parliament>>、<<Paxos made simple>>、<<Fast Paxos>>前面关于Paxos的论述。
    2016/12/28

    上周和一个有真正paxos工程经验的人讨论一下paxos,paxos现在大多是应用于replication的一致性,用来实现一个 多节点一致的日志,和他 的讨论让我觉得要想真正的精确掌握paxos和它对应的强一致性领域,也许只有真正的在工程中实现过才行。这个回答只能当做是想要了解原理的入门吧,甚至可能有些微妙的地方还会产生误导。它介绍了paxos面向的问题,以及为何它的流程要这么设计,但还是希望对有兴趣阅读这个问题的有所帮助。
    2016 10/30

    现在看开头这段话是在是觉得有点羞耻,遂改之。我了解paxos是从找工作开始,比较详细的了解则是在毕设,自己动手了写了个类似Zookeeper的系统,paxos本身并不复杂,在<<paxos made simple>> Lamport用两段话就描述清楚了它的流程,他老人家也说paxos其实是个简单的算法。但是是我在工程领域见过最为精妙的算法。我想论述Paxos为什么难以理解会比描述Paxos的流程长的多的多。我最初学习Paxos是从《从Paxos到Zookeeper:分布式一致性原理与实践》,现在看来并不是个很好选择,作者讲解的方式是直接翻译论文,论述ZAB和paxos关系的部分我也觉得不是很严谨。如果真心觉得Paxos的原文不知所云,也不知道能拿来干嘛,可以从阅读Raft的论文开始,如果真的有兴趣,强推Raft作者Diego Ongaro那篇300页的博士论文《CONSENSUS: BRIDGING THEORY AND PRACTICE》,不只是讲解了Raft协议,而且系统的论述Paxos类似的一致性协议,不仅从原理,也从工程角度出发,涉及方方面面,在写毕设中了就是写不动就翻翻的良作。我个人觉得阅读任何号称浅显易懂的解说Paxos算法的描述(比如下文=/=),最多只是让你更好的入门,要更深的理解Paxos以及所有等同于它的一致性协议,ZAB,Raft,ViewStamp,直接阅读相关论文,理解证明,理解它们哪里不同,为何本质上相同,与人探讨,在工程上自己实现,或者阅读开源实现的源代码才是最好的方式。分布式一致性是个有趣的领域,而Paxos和类似的协议对这个问题的重要性不喻,在过去的十年,Paxos几乎等价于分布式一致性。
    2016 6/20

    之前的答案最大的不严谨之处在于两个事件“先后”这种时序关系的处理上。paxos是个分布式一致性协议,它的事件需要多个节点共同参与,一个事件完成是指多个节点上均完成了自身负责的单机子事件(就让我门把这样的事件称为”分布式事件”),这样的分布式事件可以看作是多个单机子事件的复合,但是即不能从两个分布式事件的先后推导出某个节点上它们的单机子事件的先后,也不能根据某个节点上两个单机子事件的先后断言它们对应的分布式事件的先后。举一个简单的例子,两个节点P1,P2;分布式事件a1设置每节点的本地变量v=1,分布式式事件a2设置每个节点本地变量v=2,如果我们称a1先于a2完成,那么对于节点P1而言,v=1发生在v=2之前还是之后都有可能;反之如果P1上v=1发生在v=2之前,a1和a2哪个县完成也都有可能。

    原来的回答有些地方论述 分布式事件a1在a2之后(先)时,默认了单节点上,a1会比a2先达成状态,或者反之。

    实际上为了论证paxos的正确性,并不需要借助于分布式事件的时序(起码无用太在意物理时序),对于paxos流程中的分布式事件,例如提案被通过,值被决定,让我们忘记它们之间物理时间上的先后关系吧。

    下面就开始假装推导出paxos,作为一种理解协议的流程和协议如何保证正确性的方式。这样的推导的过程远比我想象的冗长;相比之下,论文中Lamport自己推导出Paxos的过程简洁、巧妙、漂亮,但是更抽象。在末尾用自己的语言描述了这种方式,作为补充。补充的版本基本思路来自<<Paxos made simple>>,和原文略有不同;总共不到1500字,却既说明了Paxos是如何得到的,又严谨的论证了Paxos的正确性。

    首先我们简单介绍paxos所保证的一致性的具体含义;达成一致的条件(何时达成一致);基于的一个基本的数学原理;以及它需要满足的假设。

    什么是一致性?实际上这个词在不同地方语义并不那么一致,Paxos面向的是一个理论的一致问题,这个问题的通俗描述是:

    有一个变量v,分布在N个进程中,每个进程都尝试修改自身v的值,它们的企图可能各不相同,例如进程A尝试另v=a,进程B尝试另v=b,但最终所有的进程会对v就某个值达成一致,即上述例子中如果v=a是v达成一致时的值,那么B上,最终v也会为a。需要注意的是某个时刻达成一致并不等价于该时刻所有进程的本地的v值都相同,有一个原因是进程可能挂掉,你不能要求挂掉的进程任何事;更像是最终所有存活的进程本地v的值都会相同。

    这个一致性需要满足三个要求:

    1.v达成一致时的值是由某个进程提出的。这是为了防止像这样的作弊方式:无论如何,最终都令每个进程的v为同一个预先设置好的值,例如都令v=2,那么这样的一致也太容易了,也没有任何实际意义。
    2.一旦v就某个值达成了一致,那么v不能对另一个值再次达成一致。这个要求称为安全性。
    3.一致总是能够达成,即v总会被决定为某个值。这是因为不想无休止的等待,这个要求也称为活性。

    Paxos中变量v达成一致的条件: N个进程中大多数(超过一半) 进程都认为v是同一个值,例如c,那么我们称v被决定为c。这样即使少数进程挂掉了,也不会使得一致无法达成。

    Paxos保证的一致性如下:不存在这样的情形,某个时刻v被决定为c,而另一个时刻v又决定为另一个值d。由这个定义我们也看到,当v的值被决定后,Paxos保证了它就像是个单机的不可变变量,不再更改。也因此,对于一个客户端可以多次改写值的可读写变量在不同的节点上的一致性问题,Paxos并不能直接解决,它需要和状态机复制结合。

    Paxos基于的数学原理: 我们称大多数进程组成的集合为法定集合,两个法定集合必然存在非空交集,即至少有一个公共进程,称为法定集合性质。 例如A,B,C,D,F进程组成的全集,法定集合Q1包括进程A,B,C,Q2包括进程B,C,D,那么Q1和Q2的交集必然不在空,C就是Q1,Q2的公共进程。如果要说Paxos最根本的原理是什么,那么就是这个简单性质。同时,可以注意到,这个性质和达成一致的定义相呼应。

    Paxos中进程之间是平等的,即不存在一个特殊的进程,这是由于如果协议依赖于某个特殊的进程,那么这个进程挂掉势必会影响协议;而对于分布式环境,无法保证单个进程必然必活,能够容忍一定数量的进程挂掉,是分布式协议的必然要求。这是推导过程所要遵循的一个原则,就称为平等性原则好了。

    消息是进程间通信的唯一手段,对于分布式环境来说,这是显然的。

    Paxos要求满足的前置假设只有一个:消息内容不会被篡改;更正式的说是无拜占庭将军问题。

    假装的推导总是先从一些具体的场景开始,既然Paxos的假设仅仅只是消息不会被篡改,保证了这点任意场景下都能保证一致性,那么对于举例的场景它必然是能够保证一致性的;因此不妨先使得协议流程能在当前场景下能保证一致性,然后再举出另一个场景,当前的协议流程无法再该场景下满足一致性,接着再丰富协议流程,满足该场景,如此往复,最终得到完整的paxos协议,最后再不失一般性的论证协议对任意场景都能保证一致性。

    进程的平等性假设会带来如下的问题,考虑如下的场景1:三个进程的场景P1,P2P3(n个进程的场景类似),P1尝试令v的值被决定为a,P2尝试令v被决定为b。假设它们都先改写了自身的v值,然后发送消息尝试改修P3的v值。显然如果P3收到两个消息后都满足了它们的企图,那么v就会两次被决定为不同的值,这破坏了之前的定义。因此P3必须得拒绝掉其中一个进程的请求,如何拒绝也是我们最先考虑的问题。一个最简单的拒绝策略是先来后到,P3只会接受收到的第一个消息,拒绝之后的消息,即只会改写v一次。按照这个策略,如果P1发送的消息首先到达P3,那么P3接受该消息令v=a,拒绝掉后到的来自P2的消息。但是这个策略会引入一个另外的问题;在场景1的基础上考虑这样的场景1’,P3也尝试决定v的值,P3尝试令v被决定为c,那么P1,P2,P3都尝试修改v的值,首先P1令v=a,P2令v=b,P3令v=c(相当于自己给自己发消息),按照之前的策略,每个进程只会改写v的值一次,那么将永远不会出现两个进程的v值相等的情况,即v永远无法被决定。更正式的说,这样的协议不满足活性,活性要求协议总能达成一致。由此我们也得到第一个结论:进程必须能够多次改写v的值。同时我们也要意识到:当进程收到第一个消息时,进程是没有任何理由拒绝这个消息的请求的。

    拒绝策略总是需要有一个依据,之前我们的依据是消息到达的先后,只接受第一个到达的消息,但这导致不满足活性。现在我们需要另一个拒绝策略,也就是需要另一个依据,这个依据至少能够区分两个消息。为此我们引入一个ID来描述这个消息,这样就可以根据ID的大小来作为拒绝或接受的依据;选择ID更大的消息接受和选择ID更小的消息接受是两种完全对称的策略,不妨选择前者。这个策略不会导致明显的活性问题,ID更大的消息总是能被接受,一个节点可以多次更改v的值。例如在场景1’中,只要P1的消息ID比P3发送给自己的消息ID更大,P3就会接受P1的消息,令v=a,从而令v的值被决定为a。再来考虑最初的场景1,不妨假设P1的消息ID大于P2的消息ID,根据P3收到消息的先后可以分为两种情况:

    1. P3先收到P1的消息,记做场景1-2。由于P1的消息是P3收到的第一个消息,P3接受该请求,令v=a;同时为了能对之后收到的消息作出是否接受的判断,P3需要记录该消息的ID作为判断的依据。之后P3又收到P2的消息,该消息的ID小于P3记录的ID(即P1的消息ID),因此P3拒绝该消息,这样我们的目的就达到。

    2. P3先收到P2的消息,记作场景1-3。同样P3接受该消息,令v=b,记录该消息的ID。之后P3收到P1的消息,由于P1的消息ID大于P3记录的ID,因此P3无法拒绝该消息,之前的问题依旧存在。

    尽管对于场景1-3,目前的策略依旧无法保证一致性,但是起码我们缩小协议不适用的范围。先总结下我们目前的策略,并定义一些称谓以方便后面的论述。我们称呼进程P发送的尝试修改另一个进程中变量v的值的消息称之为提案,记作Proposal;提案的ID记作proposal_id;提案中会附带一个值,如果一个进程接受一个提案,则修改自身的v值为该提案中的值。如果一个提案被大多数进程所接受,那么称提案被通过,此时显然v被决定为提案中的值。进程P记录的接受的提案ID记做a_proposal_id。

    之前我们尚未清晰定义a_proposal_id,实际上我们也就并未清晰的定义我们的拒绝策略,当P收到一个提案Proposal-i时,可能已经收到过多个提案,Proposal-i.proposal_id该和其中哪个提案的proposal_id比较,我们并未定义。我们定义为其中的最大者,这样实际上进程P只需维护一个a_proposal_id即可,当收到一个Proposal时,更新a_proposal_id = Max(Proposal.proposal_id,a_proposal_id)。同时在之前的描述中我们应当注意到实际上一个进程存在两个功能

    1. 进程主动尝试令v的值被决定为某个值,向进程集合广播提案。

    2. 进程被动收到来自其它进程的提案,判断是否要接受它。

    因此可以把一个进程分为两个角色,称负责功能1的角色是提议者,记作Proposer,负责功能2的角色是接受者,记作Acceptor。由于两者完全没有耦合,所以并不一定需要在同个进程,但是为了方面描述,我们假定一个进程同时担任两种角色,而实际的工程实现也往往如此。

    接着我们尝试解决场景1-3,这看起来很难。P3作为接受者,收到P2的提案之前未收到任何消息,只能接受该提案,而由于P1的提案proposal_id大于P2的提案,我们的拒绝策略也无法让P3拒绝P2。我们先不急着推导具体可行的策略,先考虑下解决1-3场景可能的角度,有如下三种角度可以入手:

    1. P3能够拒绝掉P2的提案。

    2. P3能够拒绝掉P1的提案。

    3. 限制P1提出的提案中的值,如果P1的提案中的值与P2的提案一致,那么接受P1也不会破坏一致性。

    接着我们分析三个角度的可行性:

    角度1需要P3有做出拒绝的依据,由于消息是进程间通信唯一手段,这要求P3在收到P2的提案之前必须先收到过其它消息。对于场景1-3,只有P1,P2是主动发送消息的进程,P2当然不可能额外还发送一个消息试图令P3拒绝自己随后的提案。那么唯一的可能是P1在正式发送提案前,还发送了一个消息给P3,这个消息先于P2的提案到达,给了P3拒绝P2提案的理由。如果沿用目前的拒绝策略,那么P1只需先发送随后提案的proposal_id给P3,P3更新a_proposal_id 为 该消息附带的proposal_id,这样a_proposal_id将大于P2的提案的proposal_id,而导致P2的提案被拒绝,似乎这是一个可行的角度。

    对于角度2,我们目前的策略无法做到这一点,因此除了proposal_id外,我们还得给提案附加上额外的信息作为另外的拒绝策略的依据。提案由进程提出,也许我们可以附加进程的信息,但是就算P3得知P1的提案由P1提出,P3又凭什么歧视P1,这违反进程的平等性假设。似乎这个角度不是一个好角度。

    最后我们分析一下角度3,角度3提供了与1,2截然不同的思路,它不再是考虑如何拒绝,而把注意力放在如何对提案的值做出恰当的限制上。对于场景1-3而言,从这个角度,由于P3无法拒绝P1和P2的提案中的任何一个,因此P1的提案的值就必须与P2的提案一致;这也意味着了P1在正式提出提案前,需要有途径能获悉P2的提案的值。如我们上面一直强调的,消息是唯一的通信手段,P1必须收到来自其它节点消息才有可能得知P2提出的提案的值。P1可以被动的等待收到消息,也可以主动的去询问其它节点等待回复。后者显然是更好的策略,没有收到想要的消息就一直等待未免也太消极了,这种等待也可能一直持续下去从而导致活性问题。

    经过上面的分析,我们暂时排除了从角度2入手(实际上后面也不会再考虑,因为从1,3入手已经足以解决问题)。下面将沿着角度1,3进行更深入的分析,我们先尝试从角度1出发,毕竟考虑如何拒绝已经有了经验。先来总结下我们在分析角度1时引入的额外的流程:

    进程P在发送提案前,先广播一轮消息,消息附带着接下来要发送的提案的proposal_id。由于该消息和接下来发送的提案相关,且在提案被提出之前发送,不妨称这个消息为预提案,记作PreProposal,预提案中附带着接下来的提案的proposal_id。当作为接受者的进程Pj收到预提案后,更新Pj. a_proposal_id。还记得我们之前的拒绝策略中a_proposal_id的更新策略嘛:a_proposal_id = max(proposal_id,a_proposal_id),a_proposal_id是递增的。因此如果预提案的proposal_id小于Pj.a_proposal_id,Pj完全可以忽略这个预提案,因为这代表了该预提案对应的提案的proposal_id小于Pj.a_proposal_id,必然会被拒绝。我们沿用之前拒绝策略中a_proposal_id的更新策略。这样当收到预提案或者提案后,a_proposal_id的值均更新为 max(a_proposal_id,proposal_id)。

    接着我们来看看引入了预提案后,能否真正解决场景1-3。根据P1和P2的预提案到达P3的先后也存在两种场景:

    1.场景1-3-1:P1的预提案先到达,P3更新a_proposal_id 为该提案的proposal_id,这导致随后到达的P2的提案的proposal_id小于a_proposal_id,被拒绝。满足一致性

    2.场景1-3-2:P2的提案先到达,P3接受P2的提案,此时和原始的场景1-3存在同样的问题。归根结底,预提案阶段能否使得P3拒绝该拒绝的,也依赖消息到达的顺序,和提案阶段的拒绝策略存在相同的问题,但至少又缩小了不能保证安全性的场景范围。

    幸好我们还有角度3可以着手考虑,所以仍有希望完全解决场景1-3。在深入角度3之前,先总结下协议目前为止的流程,现在协议的流程已经分为了两个阶段:预提案阶段和提案阶段,两种消息:预提案 ,两种角色:接受者  提议者,流程如下:

    阶段一 提议者Proposer:向接受者Acceptor广播预提案,附带接下来提案Proposal的proposal_id 接受者Acceptor:收到预提案后更新a_proposal_id = max(proposal_id,a_proposal_id)

    阶段二 提议者Proposer:向接受者Acceptor广播提案,和之前的预提案共享同一个proposal_id 接受者Acceptor:如果收到的提案的proposal_id>= a.proposal_id,那么接受这个提案,更新a_proposal_id = max(proposal_id,a_proposal_id)

    为了更形象,之前的讨论是基于三个进程的场景,实际上对于N进程的场景也是一样的。N个进程时,与之前场景1对应的场景是:

    N个进程,存在两个进程Pi,Pj,Pi尝试另v被决定为a,Pj尝试另v被决定为b,Pi提出的预提案记作PreProposal-i,提案记作Proposal-i;Pj的预提案PreProsal-j,提案Proposal-j。

    之拒绝策略的讨论都是基于一个关键的进程P3,只要P3最终能拒绝Proposal-i和Proposal-j中的一个,两个提案就不会都被通过,那么一致性就不会被破坏。Pi的提案被通过代表了存在一个法定集合Q-i,Q-i中的进程都接受了Proposal-i,Pj同理,存在一个Q-j,Q-j中的进程都接受了Proposal-j。由于法定集合的性质,两个多数集Q-i和Q-j中必然存在一个公共进程Pk。Pk即相当于场景1中的P3,只要Pk能够拒绝Proposal-i和Proposal-j中的一个,协议依旧是安全的。为了不失一般性,下面我们都以N个进程的场景作为讨论的基础,称为场景2,由于场景1和场景2可以一一对应,所以接下来顺着限制提案的值的角度,我们直接针对场景2-3-2,之前的场景和场景1一样,我们的拒绝策略已经足以应付。v的值被决定代表有一个提案,它被法定数目的集合接受,我们称这为提案被通过。

    首先我们看下场景2-3-2,Pi对应场景1-3-2中的P1,Pj对应P2,Pk对应P3。Pj的提案Proposal-j最终会被法定集合Q-j接受,即v的值被决定为b,且Proposal-i.proposal-id > Proposal-j.proposal_id。我们需要限制Pi的提案的值,不能让Pi自由的给Proposal-i中的v赋值。在2-3-2中,由于拒绝策略失效,所以只能令Proposal-i.v = Proposal-j.v=b。要做到这一点,正如前面的分析所言,Pi需要先主动询问进程集合,来得知Proposal-j.v =b这一事实。显然Pi是没有先验信息来得知Proposal-j由哪个进程提出,也不知道Q-i和Q-j的公共节点Pk是谁,因此Pi只能广播它的查询。由于我们需要允许少数进程失败,Pi可能只能得到大多数进程的回复,而这之中可能不包括Pj。我们称这些回复Pi的查询的进程的集合为Q-i-2,为了描述更简单,无妨假设Q-i-2=Q-i。尽管Pi的查询可能得不到Pj的回复,好在作为将会被通过的提案,Proposal-j将会被Q-j内所有进程接受,因此如果进程作为接受者在接受提案时,顺便记录该提案,那么Q-j内所有进程都将得知Proposal-j.v=b。由于Pk属于Q-i和Q-j的交集,所以Pk即收到了Pi的查询,又接受了提案Proposal-j。之前我们已经引入了预提案阶段,显然我们可以为预提案附带上查询的意图,即Pk作为接受者收到Pi的预提案后,会回复它记录的接受过的提案。有一个问题是Pk此时是否已经记录了Proposal-j呢?很巧的是在场景2-3-2中,Pj的提案Proposal-j是先于Pi的预提案PreProposal-i先到达,所以Pk已经记录了Proposal-j.v = b,Pj收到的来自Pk的回复中包含了提案Proposal-j,而2-3-2之外的场景,拒绝策略已经足以应付。这里依旧还有两个问题,先讨论第一个:

    实际上除了Pi和Pj外可能还会有多个进程发起预提案和提案,所以收到 PreProposal-i时Pk可能已经接受过多个提案,并非只有Proposal-j,那么Pk应该回复PreProposal-i其中哪个提案,或者都回复?Pk并不知道Proposal-j会被通过,它只知道自己接受了该提案。都回复是个效率很低但是稳妥,可以保证Pk不会遗漏Proposal-j,Pk已经回复了它所能知道的全部,我们也无法要求更多。需要注意到的是进程是平等的,所以Q-i中所有进程都和Pk一样回复了它接受过的所有提案。当Pi收到所有来自Q-i的回复时,随之而来的是第二个问题:

    Pi收到了多个Proposal作为一个Acceptor组成的法定集合Q-i对PreProposal-i的回复,记这些Proposal组成的集合记坐K-i,那么它应当选择K-i中哪个一个提案的值作为它接下来的提案Proposal-i的v值?记最终选择的这个提案为Proposal-m。

    在场景2-3-2中,我们第一直觉是希望选择的Proposal-m 即是 Proposal-j,但是实际上,我们只要保证Proposal-m .v = Proposal-j.v即可。从另一个角度 ,K-i中很可能存在这样的提案Proposal-f,Proposal-f.v!=Proposal-j.v,我们要做的是避免选择到这类提案。我们可以根据一些依据瞎选碰碰运气,但是这并明智。我们不妨假设存在一个策略CL,CL满足需求,使得选择出的提案Proposal-m满足Proposal-m.v= Proposal-j.v。然后让我们来分析一下此时Proposal-f有什么特征。

    Proposal-f能够被提出,代表存在一个多数集合Q-f,Q-f中每个进程都接受了PreProposal-f,同时假设是进程P-f提出了PreProposal-f和Proposal-f。Q-f和Q-j必然存在一个公共节点,记做Ps,Ps即接受了PreProposal-f又接受了Proposal-j。Ps收到PreProposal-f和Proposal-j的顺序只有两种可能:

    1.Ps先收到PreProposal-f

    2.Ps先收到Proposal-j

    PreProposal-f.proposa-id和Proposal-j. proposal_id的大小也只有两种可能,不妨先假设PreProposal-f.proposal_id > Proposal-j.proposal_id

    对于情形1,Ps先收到PreProposal-f,接受它,更新Ps.a_proposal_id = PreProposal-f.proposal_id > Proposal-j.proposal_id,同时之前的a_proposal_id的更新策略又使得Ps.a_proposal_id是递增的,于是导致收到Proposal-j时,Proposal-j.proposal_id小于Ps.a_proposal_id,被拒绝,而这于Ps的定义矛盾。

    对于情形2,Ps将把提案Proposal-j回复给PreProposal-f。由于我们假设了策略CL的存在,于是P-f在收到所有Q-f对PreProposal-f的回复后,将令Proposal-f.v=Proposal-j.v,CL就是干这个的。因此由于Proposal-f.v!=Proposal-j.v矛盾。

    于是当假设PreProposal-f.proposal_id > Proposal-j.proposal_id 时,情形1,2我们都得出了矛盾,同时两者的proposal_id又不相等(最初的假设),所以必然PreProposal-f.proposal_id < Proposal-j.proposal_id,即Propsoal-f.proposal_id < Proposal-j.proposal_id

    于是我们得到的结论是:如果策略CL存在,提案Proposal-j最终会被通过,任意一个proposal_id更大的预提案PreProposal-i,对于它得到的Q-i的回复K-i中的Proposal-f,只要Proposal-f.v!= Proposal-j.v,那么必然 Proposal-f.proposal_id < Proposal-j.proposal_id。

    既然K-i中所有v值不等于Proposal-j.v的提案,proposal_id都比Proposal-j更小,那代表所有proposal_id比Proposal-j更大的提案,v值都等于Proposal-j.v因此选择K-i中proprosal_id最大的提案,就能保证Proposal-i.v = Proposal-j.v。于是我们得到了策略CL的具体形式。

    我们得到了具体可行的策略CL是建立在策略CL存在这一前提之上,因此反过来,对于这个具体的选值策略CL,结合之前我们得到了协议流程,它是否能保证如下的性质CP1,依旧需要进一步的论证 :
    如果一个提案Proposal-j最终会被通过,那么对于任意的一个提案Proposal-i,如果Proposal-i.proposal_id > Proposal-j.proposal_id,那么Proposal-i.v = Proposal-j.v。

    我们先总结下目前得到的协议流程:

    阶段一 预提案阶段 提议者Proposer:向接受者Acceptor广播预提案,附带接下来提案Proposal的proposal_id 接受者Acceptor:收到预提案后更新a_proposal_id = max(proposal_id,a_proposal_id),如果预提案的proposal_id大于a_proposal_id,那么回复该预提案的提议者改接受者接受过的所有提案。

    阶段二 提案阶段 提议者Proposer:等待直到收到大多数接受者对预提案的回复,从所有回复的提案组合成的集合K中挑选proposal_id最大的提案,以该提案的值作为本次提案的值。如果K是空集,那么可以给提案任意赋值。 向接受者Acceptor广播提案,和之前的预提案共享同一个proposal_id 接受者Acceptor:如果收到的提案的proposal_id>= a.proposal_id,那么接受这个提案,更新a_proposal_id = max(proposal_id,a_proposal_id)

    这些流程是为了解决举例的场景而不断丰富的,接着就让我们论证下协议流程是否总是可以确保CP1。

    首先假设Proposal-i.v != Proposal-j.v,如果得出矛盾即可证明CP1。在尝试推出矛盾前,我们先做一些定义,以便后续的推导。

    记大多数接受者组成的法定集合为Q,K是提议者在提案阶段收到的所有Q回复的提案组成的集合,如果K不为空,记K中proposal_id最大的提案是MaxProposal(K),本次提案的值即是MaxProposal(K).v;如果K是空集,那么MaxProposal(K).v = null。特别的,对于提案Proposal-i,回复它预提案接受者的集合为Q-i,回复的提案组成的集合为K-i,Proposal-i.v = MaxProposal(K-i),Proposal-i.v=null代表可以随意赋值。为了描述方便,我们令Proposal-i的proposal_id为i,即Proposal-i代表了proposal_id=i的提案,Proposal-j意味着Proposal-j.proposal_id =j。

    论证过程如下:

    (1) Proposal-i.v!=Proposal-j.v,即MaxProposal(K-i) .v!= Proposal-j.v,即MaxProposal(K-i)!=Proposal-j

    (2) Proposal-j最终会被通过,代表最终会存在一个多数集合Q-j,Q-j中每个接受者都接受了Proposal-j。

    (3) 两个多数集必然存在公共成员,故Q-j和Q-i必然存在一个公共的进程Pk,Pk即收到了PreProposal-i又收到了Proposal-j,且都接受了它们;Pk收到消息的先后关系只存在如下两种可能:

    1.Pk先收到了PreProposal-i

    2.Pk先收到了Proposal-j

    (4) 情形1中Pk先收到了PreProposal-i,那么Pk收到Proposal-j时,Pk.a_proposal >= PreProposal-i.proposal_id >Proposal-j.proposal_id,Pk会拒绝Proposal-j,与(3)矛盾,因此情况1不可能,Pk必然是先收到Proposal-j

    (5) 情形2中Pk收到PreProposal-i时,已经接受了Proposal-j,因此Pk回复PreProposal-i的提案中包含了Proposal-j,因此K-i中必然包含了Proposal-j。

    (6) 由(1)已知MaxProposal(K-i) != Proposal-j,即存在另一个提案Proposal-m = MaxProposal(K-i),而Proposal-j属于K-i,因此Proposal-m.proposal_id > Proposal-j.proposal_id,且Proposal-m.v != Proposal-j.v

    (7)由预提案阶段,接受者回复预提案的条件可知:Proposal-i.proposal_id大于集合K-i中任意一个提案的Proposal-id,故Proposal-i.proposal_id>Proposal-m.proposal_id。

    (8) 目前我们已经论证如下一点:

    在Proposal-j最终会被通过的前提下,如果存在一个提案Proposal-i.v!=Proposal-j.v,且Proposal-i.proposal_id >Proposal-j.proposal_id,我们一个数学符号来带表示这个情况,记CF(j,i);那么 必然存在一个提案Proposal-m, Proposal-m!=Proposal-j.v,且Proposal-m.proposal_id > Proposal-j.proposal_id,同样的我们可以记做CF(j,m)。并且Proposal-m.proposal_id < Proposal-i.proposal_id,m < i

    即如果CF(i,j)成立,那么必然CF(m,j)成立,且i>m,即 CF(i,j) —> CF(m,j)。这个过程可以继续往下递归,但由于区间[j,i]范围是有限的,因此一定会递归到一个CF(j,e),此时不可能存在一个提案,它的proposal_id在区间(j,e)内,无法往下递归,这与(8)矛盾。这也就意味着CF(e,j)不成立,而如果CF(i,j)成立,那么CF(e,j)成立,因此CF(i,j)不成立,故假设不成立,即Proposal-i.v 必然等于Proposal-j.v,即证CP1。

    通过归约的方式得出矛盾的方式依旧有些抽象,我们可以通过更好的定义假设来更容易得到的矛盾:

    我们加强对Proposal-i的约束;先假设存在一个提案的非空集合KD,KD中的任意一个提案Proposal-k,Proposal-k.v!=Proposal-j.v,且Proposal-k.proposal_id > Proposal-j.v;再假设Proposal-i是KD中proposal_id最小的提案;由于KD是非空集合,故Proposal-i必然存在。

    我们依旧可以从Proposal-i出发,(1)~(7)与上面相同,同理得到:存在一个提案Proposal-m, Proposal-m!=Proposal-v,且Proposal-m.proposal_id > Proposal-j.proposal_id,且Proposal-m.proposal_id < Proposal-i.proposal_id。

    显然Proposal-m满足集合KD对提案的要求,故Proposal-m属于KD,又Proposal-m.proposal_id<Proposal-i.proposal_id,这和Proposal-i是KD中proposal_id最小的提案的定义矛盾。因此不存在这样的非空集合KD,即不存在一个提案Proposal-k,Proposal-k.v!=Proposal-j.v且Proposal-k.proposal_id>Proposal-j.proposal_id,即如果一个提案Proposal-j最终会被通过,对于任意的一个提案Proposal-i,如果Proposal-i.proposal_id > Proposal-j.proposal_id,那么必定Proposal-i.v = Proposal-j.v,即CP1。

    CP1约束了proposal_id大于Proposal-j的提案的值,保证了如果一个提案Proposal-j最终会被通过,不会存在另一个proposal-id更大且值不同的提案被通过,因为这些提案的值都和Proposal-j相同。那么对于proposal_id更小的提案呢? 我们假设存在一个提案Proposal-o,Proposal-o.proposal_id < Proposal-j.proposal_id,且Proposal-o.v!=Proposal-j.v,Proposal-o最终会被通过,将CP1应用于Proposal-o,则可知Proposal-j不存在,这矛盾,故Proposal-o不存在。故由CP1我们可知:如果一个提案Proposal-j最终会被通过,那么不存在另一个提案,它最终会被通过,且它的值与Proposal-j不同。由此协议必然是安全的。

    虽然我们得到了一个安全的一致性协议,基本上它就是Paxos,但是真正的Paxos要比我们假装推导出的协议更简单一点。

    回过头来看下我们的阶段1中接受者Acceptor的行为,它要回复所有的它接受过的提案,从实践的角度,不论是在本地保存所有它接受过的提案还是通过网络将它们传输给提议者,开销都太大且不可控。再看下阶段二中,提议者的选值策略,它只是选择了收到的多数集接受者回复的提案中proposal_id最大的那一个,因此接受者实际上只需要回复它接受过的proposal_id最大的提案即可,因为其它提案根本不可能会被选值策略选中。因此最终的协议如下,它就是Paxos:

    阶段一 预提案阶段 提议者Proposer:向接受者Acceptor广播预提案,附带接下来提案Proposal的proposal_id 接受者Acceptor:收到预提案后更新a_proposal_id = max(proposal_id,a_proposal_id),如果预提案的proposal_id>a_proposal_id,Acceptor回复记录的接受过的proposal_id最大的提案。

    阶段二 提案阶段 提议者Proposer:等待直到收到大多数接受者对预提案的回复,从所有回复的提案组成的法定数目的提案集合K中挑选proposal_id最大的提案,以该提案的值作为本次提案的值。如果K是空集,那么可以给提案任意赋值。然后把该提案广播给接受者们,提案和预提案共享同一个proposal_id。 接受者Acceptor:如果收到的提案的proposal_id>= a.proposal_id,那么接受这个提案,更新a_proposal_id = max(proposal_id,a_proposal_id),更新记录的提案。

    补充部分

    上面的过程从具体的场景开始推导Paxos,虽然直观但是繁琐,如果从抽象的概念和分析入手,那么过程将会相当简洁和漂亮,这也是Lamport的原始论文中的方式。这种方式理解起来更困难的地方在于:

    1.没有任何具体的认知下,直接抽象的讨论容易让人摸不着头脑。

    2.大神总是在一些地方觉得显然而不加以展开论述,而普通读者如我的内心OS:显然你mei!

    但是原文引出Paxos算法的过程实在是简洁、漂亮;而经过上面的轮述,相信有了直观的印象后,再来看抽象的方式也不那么困难,所以补充下。

    回顾下定理CP1

    如果一个提案Proposal-j最终会被通过,那么对于任意的一个提案Proposal-i,如果Proposal-i.proposal_id > Proposal-j.proposal_id,那么必定Proposal-i.v = Proposal-j.v。

    上面我们已经论证了只要协议能够保证CP1就能够保证一致性。但是CP1依旧过于宽泛,从CP1引出具体的协议流程依然是一头雾水,那么我们是否可以得到一个更加具体的定理CP2,保证CP2即可保证CP1,同时从CP2出发更容易引出协议的具体流程。为了描述方便,我们令Proposal-i的proposal_id为i,即Proposal-i代表了proposal_id=i的提案。

    要导出CP2不妨先考虑下如何证明CP1,利用归纳法,只要如能证明如下性质成立,即可证明CP1:
    如果proposal_id在区间[j,i)内任意的提案,提案的值均为Proposal-j.v,那么必定Proposal-i.v=v;这个定理记做CP1_2。

    现在我们用高中时简单而效果神奇的归纳法,利用CP1_2证明下CP1:

    假设propsal_id小于i的提案中最大的提案是Proposal-(i-1)。

    1.如果对于[j,i-1)内的任意提案,值均为Proposal-j.v,那么由CP1_2可知Proposal-i.v = Proposal-j.v。

    2.由1可知如果对于[j,i-1)内的任意提案,值均为Proposal-j.v,[j,i)内的任意提案,值均为Proposal-j.v

    3.假设Proposal-(j+1)是proposal-id大于j的最小提案,由CP1_2可知Proposal-(j+1).v = Proposal-j.v

    4.由3,2归纳可知[j, \infty )内任意提案Proposal-i,Proposal-i.v = Proposal-j.v,即CP1

    来看下CP1_2,相比CP1,它结论不变,但是多了一个前置条件:proposal_id在区间[j,i)内任意的提案值均为Proposal-j.v;这是一个重大的进步。CP1_2相比CP1看起来容易保证 很多,但是它们却是等价的。考虑CP1_2的三个前置条件:

    1.i > j
    2.提案Proposal-j最终会被通过。因此由提案被通过的定义可知必然存在一个法定集合Q-j,Q-j中任意一个接受者最终都接受了Proposal-j
    3.proposal_id在区间[j,i)内的提案的值均为Proposal-j.v

    对于任意的一个法定集合Q,考虑Q最终(包括过去和未来的所有时空)会接受的所有proposal_id小于i的提案组成的集合K。根据法定集合性质,Q和Q-j必然存在一个公共的节点,即Q中必然存在一个节点,该节点最终会接受Proposal-j,因此集合K包含Proposal-j。

    由K包含Proposal-j可知K中最大的提案proposal_id >= j;由CP1_2的前置条件3和K的定义可知如果K中存在proposal-id大于j的提案,那么该提案的值等于Proposal-j.v,因此K中proposal_id最大的提案的值等于Proposal-j.v。

    综上所述由CP1_2的前置条件可知:对于任意的一个法定集合Q,Q最终会接受的proposal_id小于i的提案组成的集合K,K中proposal_id最大的提案的值必定为Proposal-j.v。如果我们能证明该条件下,Proposal-i.v = Proposal-j.v,即可证明CP1_2。将CP1_2的前置条件替换为该条件,我们可以得到一个如下的性质CP2,保证CP2即可保证CP1_2:
    对于任意的一个法定集合Q,Q最终会接受的所有proposal_id小于i的提案组成的集合K,如果K中proposal_id最大的提案的值为Proposal-j.v;那么Proposal-i.v = Proposal-j.v。

    而引出满足CP2的流程就比较容易了,由于集合K中proposal_id最大的提案的值等于Proposal-j.v,看起来只要令Proposal-i的值为K中proposal-id最大提案的值就可以保证CP2。由于Q是任意一个法定集合,因此获取K似乎在实现上也不难,提出Proposal-i的提议者只要向Q中所有接受者询问即可。

    然后: CP2 —> CP1_2—> CP1 —>一致性

    但是实际上获取K没有那么简单,K包含的是Q所有最终接受的proposal-id小于i的的提案,不仅包含已经接受过的提案,还包括未来会接受的提案。获取已经接受过的提案是容易的,Q中的接受者只需记录它所有接受过的提案,当收到提出Proposal-i的提议者询问时,回复当中proposal_id小于i的提案即可;但是如何知晓未来?我们可以换个思路,既然无法知晓未来,那么我们约束未来,收到询问后,令Q中的接受者都承诺不再接受任何proposal_id小于i的提案,即接受者未来将不接受任何proposal_id小于i的提案;既然未来已不存在,那么Proposal-i的提议者根据Q的回复获能得到完整的K。

    于是协议的流程如下:

    对于提议者,在正式提案前,先向任意的法定集合Q发送一个消息,这个消息即是预提案,消息中要附带提案的proposal-id,作为接受者承诺回复的依据。

    接受者收到预提案后,承诺:不再接受比预提案中附带的proposal-id更小的提案;并回复:已经接受的proposal-id比于提案的proposal-id更小的提案,如之前所论述的,回复的所有满足条件的提案可以优化为只回复一个比预提案proposal_id更小的提案中proposal_id最大的那个提案。

    提议者收到所有Q中接受者回复的提案后,挑选其中proposal_id最大的提案的值作为本次提案的值。

    这样我们就得到了Paxos中最为关键的几步,阅读了之前冗长的假装推导,相信读者很容易就能补全它得到完整的Paxos。

    相比于之前近万字的假装推导,这个推导过程才1500字左右,但是即说清了Paxos是如何得出的,又论证Paxos为何正确,简洁却更有力。所以最后还是建议真有兴趣的话去看下原文,在我看来它无疑是计算机领域那数不尽的论文中最值得阅读的那一类。末尾我所描述的版本思路来自<<Paxos made simple>>,基本一致但也并不完全相同;而<< The Part-Time Parliament>>则别有一番风味。

    最后需要注意的是Paxos并不完全满足开头解决一致性问题需要满足的三个条件中的3。理论上,Paxos存在永远无法达成一致的可能,哪怕是在所有进程都存活的情况下。想象一下这样的场景,一个提案Proposal-j被提出时,恰好一个proposal-id更大的预提案Proposal-i被提出,导致Proposal-j无法被通过,而Proposal-i同样的 又因为一个proposal_id更大的其它预提案被提出,导致无法被通过。这种情况理论上存在无限递归的可能,这个问题也称为活锁;FLP早就证明了就算是容忍一个进程的失败,异步环境下任何一致性算法都存在永不终止的可能。但是实际的工程中,很多手段可以来减小两个提案的冲突概率,使得v被决定的均摊开销是一个提案,多个提案还无法决定v值的情形是极小概率事件,且概率随着提案个数增加越来越小。另外的一点,通常认为Paxos可以容忍少数进程挂掉 ,但这只是为了保证它的活性,对于安全性,实际上Paxos永远满足1,2,哪怕进程都挂掉了,此时只是显然一致无法达成而已。

    发表在 设计 | 留下评论

    软件架构的10个常见模式

    来自:喔家ArchiSelf

    企业规模的软件系统该如何设计呢?在开始写代码之前,我们需要选择一个合适的架构,这个架构将决定软件实施过程中的功能属性和质量属性。因此,了解软件设计中的不同架构模式对我们的软件设计会有较大的帮助。

    什么是架构模式?根据维基百科:架构模式是针对特定软件架构场景常见问题的通用、可重用解决方案。架构模式类似于软件设计模式,但范围更广。本文将简要解释10种常见架构模式及其用法、优缺点。

    1. 分层模式(Layered pattern)

    2. 客户端-服务器模式(Client-server pattern)

    3. 主从模式(Master-slave pattern)

    4. 管道-过滤器模式(Pipe-filter pattern)

    5. 代理模式(Broker pattern)

    6. 点对点模式(Peer-to-peer pattern)

    7. 事件-总线模式(Event-bus pattern)

    8. 模型-视图-控制器模式(Model-view-controller pattern)

    9. 黑板模式(Blackboard pattern)

    10. 解释器模式(Interpreter pattern)

    1. 分层模式

    此模式用于可分解为子任务的结构化程序,每个子任务都位于特定的抽象层级,每一层都为上一层提供服务。一般信息系统最常见的4个层次如下。

    • 表示层(也称为UI层)

    • 应用层(也称为服务层)

    • 业务逻辑层(也称为领域层)

    • 数据访问层(也称为持久层)

    应用场景:

    • 一般的桌面应用程序

    • 电子商务web应用程序

    • 一般的移动App

    分层模式

    2. 客户端-服务器模式

    这种模式由两部分组成:服务器和多个客户端。服务器将向多个客户端提供服务。客户端从服务器请求服务,服务器向这些客户端提供相关服务。此外,服务器继续侦听客户端请求。

    应用场景:

    • 电子邮件、文档共享和银行等在线应用程序。

    • 基于IPC的应用程序

    客户端-服务器模式

    3.主从模式

    这种模式由两部分组成:主节点和从节点。主节点将工作分配给相同的从节点,并根据从节点返回的结果计算最终结果。

    应用场景:

    • 在数据库复制中,主数据库被视为权威源数据库,从数据库与之同步。

    • 通过总线连接到计算机系统(主驱动器和从驱动器)的外围设备。

    • 进程内的多线程应用。

    主-从模式

    4.管道-过滤器模式

    这种模式可用于构造生成和处理数据流的系统。每个处理步骤都包含一个过滤器组件。要处理的数据通过管道传递。这些管道可用于缓冲或同步目的。

    应用场景:

    • 编译器。连续过滤器执行词法分析、词法解析、语义分析和代码生成。

    • 生物信息学的工作流

    • 工具链式的应用程序

    管道-过滤器模式

    5. 代理模式

    这种模式通过解耦组件来构造分布式系统。这些组件可以通过远程服务调用彼此交互。代理组件负责协调组件之间的通信。服务器向代理发布功能(服务和特征)。客户端向代理请求服务,然后代理将客户端重定向到合适的服务。需要注意broker,agent,proxy以及delegate的区别。

    应用场景:

    • 消息代理软件,例如:Apache ActiveMQ、Apache Kafka、RabbitMQ和JBoss消息传递。

    • 网络传输中的代理软件。

    代理模式

    6. P2P模式

    在这种模式中,每个组件都称为对等节点。对等节点既可以作为客户机(从其他对等节点请求服务),也可以作为服务器(向其他对等节点提供服务)。对等节点可以充当单个客户机或服务器,也可以同时充当客户机和服务器,并且可以随着时间变化动态地更改角色。

    使用场景:

    • 文件共享网络,例如Gnutella和G2等。

    • 多媒体协议,如P2PTV和PDTP。

    P2P模式

    7. 事件-总线模式

    这种模式也被称为订阅发布模式,主要处理事件,有4个主要组件:事件源、事件监听者、通道和事件总线。事件源将消息发布到事件总线上的特定通道,监听者订阅特定的通道。消息发布到监听者之前订阅的通道,监听者将收到消息的通知。

    使用场景:

    • 安卓开发

    • 通知服务

    • 注册中心

    事件-总线模式

    8. 模型-视图-控制器模式

    这种模式,也称为MVC模式,将一个交互应用程序分为三个部分:

    • 模型-包含核心功能和数据

    • 视图——向用户显示信息(可以定义多个视图)

    • 控制器——处理来自用户的输入

    这样做是为了将信息的内部表示、信息呈现给用户的方式、接受用户输入的方式分离开来。这种模式解耦组件并允许有效的代码重用。

    应用场景:

    • 一般的web应用程序架构

    • Django和Rails等Web框架

    • 一般的GUI 应用程序

    模型-视图-控制器模式

    9. 黑板模式

    这种模式对于没有确定解决方案策略的问题非常有用。黑板图案由三个主要部分组成:

    • 黑板:一个结构化的全局内存,包含来自解决方案空间的对象

    • 知识源:具有自己表示形式的专门化模块

    • 控制组件:选择、配置和执行模块

    所有的组件都可以到达黑板。组件可以生成添加到黑板上的新数据对象。组件在黑板上查找特定类型的数据,并通过与现有的知识源进行模式匹配找到这些数据。

    应用场景:

    • 语音识别

    • 车辆识别及追踪

    • 蛋白质结构识别

    • 声纳信号的解释

    黑板模式

    10. 解释器模式

    这种模式用于设计一个解释专用语言编写的程序组件。它主要指定如何评估每一行程序,即用特定语言编写的句子或表达式。其基本思想是语言的每个符号都有一个类。

    应用场景:

    • 数据库查询语言,如SQL。

    • 用于描述通信协议的语言。

    解释器模式

    下面的表格总结了每种架构模式的优缺点。

    发表在 设计 | 留下评论

    一致性协议:2PC和3PC(二阶段提交和三阶段提交两种协议)

    分布式一致性回顾

    在分布式系统中,为了保证数据的高可用,通常,我们会将数据保留多个副本(replica),这些副本会放置在不同的物理的机器上。为了对用户提供正确的增\删\改\差等语义,我们需要保证这些放置在不同物理机器上的副本是一致的。

    为了解决这种分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(Two Phase Commitment Protocol)、三阶提交协议(Three Phase Commitment Protocol)和Paxos算法

    分布式事务

    分布式事务是指会涉及到操作多个数据库的事务。其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚)

    在分布式系统中,各个节点之间在物理上相互独立,通过网络进行沟通和协调。由于存在事务机制,可以保证每个独立节点上的数据操作可以满足ACID。但是,相互独立的节点之间无法准确的知道其他节点中的事务执行情况。所以从理论上讲,两台机器理论上无法达到一致的状态。如果想让分布式部署的多台机器中的数据保持一致性,那么就要保证在所有节点的数据写操作,要不全部都执行,要么全部的都不执行。但是,一台机器在执行本地事务的时候无法知道其他机器中的本地事务的执行结果。所以他也就不知道本次事务到底应该commit还是 roolback。所以,常规的解决办法就是引入一个“协调者”的组件来统一调度所有分布式节点的执行。

    XA规范

    X/Open 组织(即现在的 Open Group )定义了分布式事务处理模型。 X/Open DTP 模型( 1994 )包括应用程序( AP )、事务管理器( TM )、资源管理器( RM )、通信资源管理器( CRM )四部分。一般,常见的事务管理器( TM )是交易中间件,常见的资源管理器( RM )是数据库,常见的通信资源管理器( CRM )是消息中间件。 通常把一个数据库内部的事务处理,如对多个表的操作,作为本地事务看待。数据库的事务处理对象是本地事务,而分布式事务处理的对象是全局事务。 所谓全局事务,是指分布式事务处理环境中,多个数据库可能需要共同完成一个工作,这个工作即是一个全局事务,例如,一个事务中可能更新几个不同的数据库。对数据库的操作发生在系统的各处但必须全部被提交或回滚。此时一个数据库对自己内部所做操作的提交不仅依赖本身操作是否成功,还要依赖与全局事务相关的其它数据库的操作是否成功,如果任一数据库的任一操作失败,则参与此事务的所有数据库所做的所有操作都必须回滚。 一般情况下,某一数据库无法知道其它数据库在做什么,因此,在一个 DTP 环境中,交易中间件是必需的,由它通知和协调相关数据库的提交或回滚。而一个数据库只将其自己所做的操作(可恢复)影射到全局事务中。

    XA 就是 X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。

    二阶提交协议三阶提交协议就是根据这一思想衍生出来的。可以说二阶段提交其实就是实现XA分布式事务的关键(确切地说:两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做)

    2PC与3PC

    在分布式系统中,每一个机器节点虽然都能够明确地知道自己在进行事务操作过程中的结果是成功或失败,但却无法直接获取到其他分布式节点的操作结果。因此,当一个事务操作需要跨越多个分布式节点的时候,为了保持事务处理的ACID特性,就需要引入一个称为“协调者(Coordinator)”的组件来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点则被称为”参与者(Participant)”。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务真正提交。基于这种思想,衍生出了二阶段提交和三阶段提交两种协议,下面先讲讲二阶段提交和三阶段提交。

    2PC

    2PC,是Two-Phase Commit的缩写,即二阶段提交,是计算机网络尤其是在数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务处理过程中能够保持原子性和一致性而设计的一种算法。通常,二阶段提交协议也被认为是一种一致性协议,用来保证分布式系统数据的一致性。目前绝大部分的关系型数据库都是采用二阶段提交协议来完成分布式事务处理的,利用该协议能够非常方便地完成所有分布式事务参与者的协调,统一决定事务的提交或回滚,从而能够有效地保证分布式数据一致性,因此二阶段提交协议被广泛地应用在许多分布式系统中。

    1、阶段一:提交事物请求

    (1)事务询问

    协调者向所有参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各个参与者的响应

    (2)执行事务

    各参与者节点执行事务操作,并将Undo和Redo信息记入事务日志中

    (3)各个参与者向协调者反馈事务询问的响应

    如果参与者成功执行了事务操作,那么就反馈给协调者Yes响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者No响应,表示事务不可以执行

    由于上面讲述的内容在形式上近似是协调者组织各参与者对一次事务操作的投票表态过程,因此二阶段提交协议的阶段一也被称为”投票阶段”,即各参与者投票表明是否要继续执行接下去的事务提交操作

    2、阶段二:执行事务提交

    在阶段二中协调者会根据各参与者的反馈来决定是否最终可以进行事务提交操作,正常情况下包含两种可能:

    (1)执行事务提交

    假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务提交:

      a)发送提交请求

        协调者向所有参与者节点发出Commit请求

      b)事务提交

        参与者收到Commit请求后会正式执行事务提交操作,并在完成提交之后释放整个事务执行期间占用的事务资源

      c)反馈事务提交操作

        参与者在完成事务提交之后,向协调者发送Ack消息

      d)完成事务

        协调者收到所有参与反馈的Ack消息后完成事务

    (2)中断事务

    假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事物。

      a)发送回滚请求

        协调者向所有参与者节点发出Rollback请求

      b)事物回滚

        参与者接收到Rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事物执行期间占用的资源

      c)反馈事务回滚结果

        参与者在完成事物回滚之后,向协调者发送Ack消息

      d)中断事物

        协调者接收到所有参与者反馈的Ack消息后,完成事物中断

    以上就是二阶段提交过程中,前后两个阶段分别进行的处理逻辑。简单讲,二阶段提交尝试讲一个事物的处理过程分为了投票和执行两个阶段,其核心是对每个事物都采取先尝试后提交的方式,因此也可以将二阶段提交看作是一个强一致性的算法。”事物提交”和”事物中断”两种场景分别如图所示:

    2PC的优缺点

    1、二阶段提交协议的优点:

    原理简单、实现方便

    2、二阶段提交协议的缺点,重点讲一下:

    (1)同步阻塞

    二阶段提交协议存在的最明显也是最大的一个问题就是同步阻塞,这会极大地限制分布式系统的性能。在二阶段提交的执行过程中,所有参与该事物操作的逻辑都处于阻塞状态,也就是说每个参与者在等待其他参与者响应的过程中,将无法进行其他任何操作

    (2)单点问题

    从上面的讲解以及上图中可以看出,协调者的角色在整个二阶段提交协议中起到了非常重要的作用。一旦协调者出现问题,那么整个二阶段提交流程将无法运转,更为严重的是,如果协调者是在阶段二中出现问题的话,那么其他参与者将一直处于锁定事物资源的状态中,而无法继续完成事物操作

    (3)数据不一致

    在二阶段提交协议的阶段二,即执行事务提交的时候,当协调者向所有的参与者发送Commit请求之后,发生了局部网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了Commit请求。于是,这部分收到了Commit请求的参与者就会进行事务的提交,而其他没有收到Commit请求的参与者则无法进行事物提交,于是整个分布式系统便出现了数据不一致的现象

    (4)太过保守

    如果在协调者指示参与者进行事务提交询问的过程中,参与者出现故障而导致协调者始终无法获取到所有参与者的响应的话,这时协调者只能依靠其自身的超时机制来判断是否需要中断事物,这样的策略显得比较保守。换句话说,二阶段提交协议没有设计较为完善的容错机制,任意一个节点的失败都会导致整个事物的失败

    3PC

    2PC在其实际运行过程中可能存在诸如同步阻塞、协调者的单点问题、脑裂和太过保守的容错机制等缺点,因此研究者在二阶段提交协议的基础上进行了改进,提出了三阶段提交协议。

    3PC,是Three-Phase Commit的缩写,即三阶段提交协议,是2PC的改进版本,其将二阶段提交协议的”提交事物请求”过程一分为二,并形成了由CanCommit、PreCommit和do Commit三个阶段组成的事物处理协议,从维基百科上拿一张图下来看一下三阶段提交协议流程示意图,原图地址为https://en.wikipedia.org/wiki/File:Three-phase_commit_diagram.png:

    1、阶段一:CanCommit

    (1)事物询问

    协调者向所有的参与者发送一个包含事物内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应

    (2)各参与者向协调者反馈事务询问的响应

    参与者在接收到来自协调者的canCommit请求后,正常情况下,如果其自身认为可以顺利执行事务,那么会反馈Yes响应,并进入预备状态,否则反馈No响应

    2、阶段二:PreCommit

    在阶段二中,协调者会根据各参与者的反馈情况来决定是否可以进行事务的PreCommit操作,正常情况下,包含两种可能:

    (1)执行事务预提交

    假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务预提交。

      a)发送预提交请求

        协调者向参与者节点发出preCommit的请求,进入Prepared阶段

      b)事务预提交

        参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中

      c)各参与者向协调者反馈事务执行的响应  

        如果参与者成功执行了事务操作,哪儿就会反馈给协调者Ack响应,同时等待最终的指令:提交(commit)或中止(abort)

    (2)中断事物

    假如任何一个参与者向协调者反馈了No响应,或者在等待超时后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事物。

      a)发送中断请求

        协调者向所有参与者节点发出abort请求

      b)中断事物

        无论是收到来自协调者的abort请求,或者是在等待协调者请求过程中出现超时,参与者都会中断事物

    3、阶段三:doCommit

    该阶段将进行真正的事物提交,会存在以下两种可能的情况:

    (1)执行提交

      a)发送提交请求

        进入这一阶段,假设协调者处于正常工作状态,并且它接收到了来自所有参与者的Ack响应,那么它将从”预提交”状态转换到”提交”状态,并向所有的参与者发送doCommit请求

      b)事物提交

        参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事物执行期间占用的事物资源

      c)反馈事物提交结果

        参与者在完成事物提交之后,向协调者发送Ack消息

      d)完成事物

        协调者接收到所有参与者反馈的Ack消息后,完成事物

    (2)中断事物

    进入这一阶段,假设协调者处于正常工作状态,并且有任意一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者商无法接收到所有参与者的反馈响应,那么就会中断事物

      a)发送中断请求

        协调者向所有的参与者节点发送abort请求

      b)事物回滚

        参与者接收到abort请求后,会利用其在阶段二中记录的Undo信息来执行事务回滚操作,并在完成事物回滚之后释放在整个事物执行期间所占用的资源

      c)反馈事务反馈结果

        参与者在完成事物回滚之后,向协调者发送Ack消息

      d)中断事物

        协调者接收到所有参与者反馈的Ack消息后,中断事物

    需要注意的是,一旦进入阶段三,可能会出现以下两种故障:

    • 协调者出现问题
    • 协调者和参与者之间的网络故障

    无论出现哪种情况,最终都会导致参与者无法及时接收到来自协调者的doCommit或者是abort请求,针对这样的异常情况,参与则都会在等待超时之后,继续进行事务提交

    3PC的优缺点

    1、三阶段提交的优点

    相较于二阶段提交协议,三阶段提交协议最大的优点就是降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致

    2、三阶段提交的缺点

    三阶段提交协议在去除阻塞的同时也引入了新的问题,那就是在参与者接收到preCommit消息后,如果出现网络分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种情况下,参与者依然会进行事物的提交,这必然出现数据的不一致

    而且由于3PC 的设计过于复杂,在解决2PC 问题的同时也引入了新的问题,所以在实际上应用不是很广泛。

    了解了2PC和3PC之后,我们可以发现,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。Google Chubby的作者Mike Burrows说过, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意即世上只有一种一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整版。后面的文章会介绍这个公认为难于理解但是行之有效的Paxos算法。

     

     

    发表在 设计 | 留下评论

    从分布式一致性谈到CAP理论、BASE理论

    来自:五月的仓颉

    问题的提出

    在计算机科学领域,分布式一致性是一个相当重要且被广泛探索与论证问题,首先来看三种业务场景。

    1、火车站售票

    假如说我们的终端用户是一位经常坐火车的旅行家,通常他是去车站的售票处购买车票,然后拿着车票去检票口,再坐上火车,开始一段美好的旅行—-一切似乎都是那么和谐。想象一下,如果他选择的目的地是杭州,而某一趟开往杭州的火车只剩下最后一张车票,可能在同一时刻,不同售票窗口的另一位乘客也购买了同一张车票。假如说售票系统没有进行一致性的保障,两人都购票成功了。而在检票口检票的时候,其中一位乘客会被告知他的车票无效—-当然,现代的中国铁路售票系统已经很少出现这样的问题了。但在这个例子中我们可以看出,终端用户对于系统的需求非常简单:

    “请售票给我,如果没有余票了,请在售票的时候就告诉我票是无效的”

    这就对购票系统提出了严格的一致性要求—-系统的数据(本例中指的就是那趟开往杭州的火车的余票数)无论在哪个售票窗口,每时每刻都必须是准确无误的!

     

    2、银行转账

    假如我们的终端用户是一位刚毕业的大学生,通常在拿到第一个月工资的时候,都会选择向家里汇款。当他来到银行柜台,完成转账操作后,银行的柜台服务员会友善地提醒他:”您的转账将在N个工作日后到账!”。此时这名毕业生有一定的沮丧,会对那名柜台服务员叮嘱:”好吧,多久没关系,钱不要少就好了!”—-这也成为了几乎所有用户对于现代银行系统最基本的需求

     

    3、网上购物

    假如说我们的终端用户是一位网购达人,当他看见一件库存量为5的心仪商品,会迅速地确认购买,写下收货地址,然后下单—-然而,在下单的那个瞬间,系统可能会告知该用户:”库存量不足!”。此时绝大部分消费者都会抱怨自己动作太慢,使得心爱的商品被其他人抢走了。

    但其实有过网购系统开发经验的工程师一定明白,在商品详情页上显示的那个库存量,通常不是该商品的真实库存量,只有在真正下单购买的时候,系统才会检查该商品的真实库存量。但是,谁在意呢?

     

    问题的解读

    对于上面三个例子,相信大家一定看出来了,我们的终端用户在使用不同的计算机产品时对于数据一致性的需求是不一样的:

    1、有些系统,既要快速地响应用户,同时还要保证系统的数据对于任意客户端都是真实可靠的,就像火车站售票系统

    2、有些系统,需要为用户保证绝对可靠的数据安全,虽然在数据一致性上存在延时,但最终务必保证严格的一致性,就像银行的转账系统

    3、有些系统,虽然向用户展示了一些可以说是”错误”的数据,但是在整个系统使用过程中,一定会在某一个流程上对系统数据进行准确无误的检查,从而避免用户发生不必要的损失,就像网购系统

     

    分布一致性的提出

    在分布式系统中要解决的一个重要问题就是数据的复制。在我们的日常开发经验中,相信很多开发人员都遇到过这样的问题:假设客户端C1将系统中的一个值K由V1更新为V2,但客户端C2无法立即读取到K的最新值,需要在一段时间之后才能读取到。这很正常,因为数据库复制之间存在延时。

    分布式系统对于数据的复制需求一般都来自于以下两个原因:

    1、为了增加系统的可用性,以防止单点故障引起的系统不可用

    2、提高系统的整体性能,通过负载均衡技术,能够让分布在不同地方的数据副本都能够为用户提供服务

    数据复制在可用性和性能方面给分布式系统带来的巨大好处是不言而喻的,然而数据复制所带来的一致性挑战,也是每一个系统研发人员不得不面对的。

    所谓分布一致性问题,是指在分布式环境中引入数据复制机制之后,不同数据节点之间可能出现的,并无法依靠计算机应用程序自身解决的数据不一致的情况。简单讲,数据一致性就是指在对一个副本数据进行更新的时候,必须确保也能够更新其他的副本,否则不同副本之间的数据将不一致。

     

    那么如何解决这个问题?一种思路是”既然是由于延时动作引起的问题,那我可以将写入的动作阻塞,直到数据复制完成后,才完成写入动作“。没错,这似乎能解决问题,而且有一些系统的架构也确实直接使用了这个思路。但这个思路在解决一致性问题的同时,又带来了新的问题:写入的性能。如果你的应用场景有非常多的写请求,那么使用这个思路之后,后续的写请求都将会阻塞在前一个请求的写操作上,导致系统整体性能急剧下降。

    总得来说,我们无法找到一种能够满足分布式系统所有系统属性的分布式一致性解决方案。因此,如何既保证数据的一致性,同时又不影响系统运行的性能,是每一个分布式系统都需要重点考虑和权衡的。于是,一致性级别由此诞生:

     

    1、强一致性

    这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大

     

    2、弱一致性

    这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不久承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态

     

    3、最终一致性

    最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型

     

    分布式环境的各种问题

    分布式系统体系结构从其出现之初就伴随着诸多的难题和挑战:

    1、通信异常

    从集中式向分布式演变的过程中,必然引入网络因素,由于网络本身的不可靠性,因此也引入了额外的问题。分布式系统需要在各个节点之间进行网络通信,因此每次网络通信都会伴随着网络不可用的风险,网络光纤、路由器或是DNS等硬件设备或是系统不可用都会导致最终分布式系统无法顺利完成一次网络通信。另外,即使分布式系统各个节点之间的网络通信能够正常进行,其延时也会大于单机操作。通常我们认为现代计算机体系结构中,单机内存访问的延时在纳秒数量级(通常是10ns),而正常的一次网络通信的延迟在0.1~1ms左右(相当于内存访问延时的105倍),如此巨大的延时差别,也会影响到消息的收发过程,因此消息丢失和消息延迟变得非常普遍

    2、网络分区

    当网络由于发生异常情况,导致分布式系统中部分节点之间的网络延时不断增大,最终导致组成分布式系统的所有节点中,只有部分节点之间能够正常通信,而另一些节点则不能—-我们将这个现象称为网络分区。当网络分区出现时,分布式系统会出现局部小集群,在极端情况下,这些局部小集群会独立完成原本需要整个分布式系统才能完成的功能,包括对数据的事务处理,这就对分布式一致性提出了非常大的挑战

    3、三态

    上面两点,我们已经了解到在分布式环境下,网络可能会出现各式各样的问题,因此分布式系统的每一次请求与响应,存在特有的三态概念,即成功、失败、超时。在传统的单机系统中,应用程序在调用一个函数之后,能够得到一个非常明确的响应:成功或失败。而在分布式系统中,由于网络是不可靠的,虽然在绝大部分情况下,网络通信也能够接受到成功或失败的响应,当时当网络出现异常的情况下,就可能会出现超时现象,通常有以下两种情况:

    (1)由于网络原因,该请求并没有被成功地发送到接收方,而是在发送过程中就发生了消息丢失现象

    (2)该请求成功地被接收方接收后,进行了处理,但是在将响应反馈给发送方的过程中,发生了消息丢失现象

    当出现这样的超时现象时,网络通信的发起方是无法确定当前请求是否被成功处理的

    4、节点故障

    节点故障则是分布式环境下另一个比较常见的问题,指的是组成分布式系统的服务器节点出现的宕机或”僵死”现象,通常根据经验来说,每个节点都有可能出现故障,并且每天都在发生

    分布式事务

    随着分布式计算的发展,事务在分布式计算领域也得到了广泛的应用。在单机数据库中,我们很容易能够实现一套满足ACID特性的事务处理系统,但在分布式数据库中,数据分散在各台不同的机器上,如何对这些数据进行分布式的事务处理具有非常大的挑战。

    分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点上,通常一个分布式事务中会涉及对多个数据源或业务系统的操作。

    可以设想一个最典型的分布式事务场景:一个跨银行的转账操作涉及调用两个异地的银行服务,其中一个是本地银行提供的取款服务,另一个则是目标银行提供的存款服务,这两个服务本身是无状态并且相互独立的,共同构成了一个完整的分布式事务。如果从本地银行取款成功,但是因为某种原因存款服务失败了,那么就必须回滚到取款之前的状态,否则用户可能会发现自己的钱不翼而飞了。

    从这个例子可以看到,一个分布式事务可以看做是多个分布式的操作序列组成的,例如上面例子的取款服务和存款服务,通常可以把这一系列分布式的操作序列称为子事务。因此,分布式事务也可以被定义为一种嵌套型的事务,同时也就具有了ACID事务特性。但由于在分布式事务中,各个子事务的执行是分布式的,因此要实现一种能够保证ACID特性的分布式事务处理系统就显得格外复杂。

    CAP理论

    一个经典的分布式系统理论。CAP理论告诉我们:一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中两项

    1、一致性

    在分布式环境下,一致性是指数据在多个副本之间能否保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一直的状态。

    对于一个将数据副本分布在不同分布式节点上的系统来说,如果对第一个节点的数据进行了更新操作并且更新成功后,却没有使得第二个节点上的数据得到相应的更新,于是在对第二个节点的数据进行读取操作时,获取的依然是老数据(或称为脏数据),这就是典型的分布式数据不一致的情况。在分布式系统中,如果能够做到针对一个数据项的更新操作执行成功后,所有的用户都可以读取到其最新的值,那么这样的系统就被认为具有强一致性

    2、可用性

    可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里的重点是”有限时间内”和”返回结果”。

    “有限时间内”是指,对于用户的一个操作请求,系统必须能够在指定的时间内返回对应的处理结果,如果超过了这个时间范围,那么系统就被认为是不可用的。另外,”有限的时间内”是指系统设计之初就设计好的运行指标,通常不同系统之间有很大的不同,无论如何,对于用户请求,系统必须存在一个合理的响应时间,否则用户便会对系统感到失望。

    “返回结果”是可用性的另一个非常重要的指标,它要求系统在完成对用户请求的处理后,返回一个正常的响应结果。正常的响应结果通常能够明确地反映出队请求的处理结果,即成功或失败,而不是一个让用户感到困惑的返回结果。

    3、分区容错性

    分区容错性约束了一个分布式系统具有如下特性:分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障

    网络分区是指在分布式系统中,不同的节点分布在不同的子网络(机房或异地网络)中,由于一些特殊的原因导致这些子网络出现网络不连通的状况,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干个孤立的区域。需要注意的是,组成一个分布式系统的每个节点的加入与退出都可以看作是一个特殊的网络分区。

    既然一个分布式系统无法同时满足一致性、可用性、分区容错性三个特点,所以我们就需要抛弃一样:

    用一张表格说明一下:

    选 ? ?择 说 ? ?明
    CA 放弃分区容错性,加强一致性和可用性,其实就是传统的单机数据库的选择
    AP 放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,例如很多NoSQL系统就是如此
    CP 放弃可用性,追求一致性和分区容错性,基本不会选择,网络问题会直接让整个系统不可用

    需要明确的一点是,对于一个分布式系统而言,分区容错性是一个最基本的要求。因为既然是一个分布式系统,那么分布式系统中的组件必然需要被部署到不同的节点,否则也就无所谓分布式系统了,因此必然出现子网络。而对于分布式系统而言,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。因此系统架构师往往需要把精力花在如何根据业务特点在C(一致性)和A(可用性)之间寻求平衡。

    BASE理论

    BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。接下来看一下BASE中的三要素:

    1、基本可用

    基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性—-注意,这绝不等价于系统不可用。比如:

    (1)响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒

    (2)系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面

    2、软状态

    软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时

    3、最终一致性

    最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

    总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事务ACID特性是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。

     

     

    发表在 设计 | 留下评论

    CAP定理

    来自:零壹技术栈

    前言

    CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性)这三个基本需求,最多只能同时满足其中的2个。

    正文

    1. CAP原则简介

    选项 描述
    Consistency(一致性) 指数据在多个副本之间能够保持一致的特性(严格的一致性)
    Availability(可用性) 指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应(不保证获取的数据为最新数据)
    Partition tolerance(分区容错性) 分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障

    什么是分区?

    在分布式系统中,不同的节点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不通的状态,但他们的内部子网络是正常的。从而导致了整个系统的环境被切分成了若干个孤立的区域,这就是分区。

    2. CAP原则论证

    如图所示,是我们证明CAP的基本场景,网络中有两个节点N1和N2,可以简单的理解N1和N2分别是两台计算机,他们之间网络可以连通,N1中有一个应用程序A,和一个数据库V,N2也有一个应用程序B和一个数据库V。现在,A和B是分布式系统的两个部分,V是分布式系统的数据存储的两个子数据库。

    • 在满足一致性的时候,N1和N2中的数据是一样的,V0=V0。
    • 在满足可用性的时候,用户不管是请求N1或者N2,都会得到立即响应。
    • 在满足分区容错性的情况下,N1和N2有任何一方宕机,或者网络不通的时候,都不会影响N1和N2彼此之间的正常运作。

    如图所示,这是分布式系统正常运转的流程,用户向N1机器请求数据更新,程序A更新数据库V0为V1。分布式系统将数据进行同步操作M,将V1同步的N2中V0,使得N2中的数据V0也更新为V1,N2中的数据再响应N2的请求。

    根据CAP原则定义,系统的一致性、可用性和分区容错性细分如下:

    • 一致性:N1和N2的数据库V之间的数据是否完全一样。
    • 可用性:N1和N2的对外部的请求能否做出正常的响应。
    • 分区容错性:N1和N2之间的网络是否互通。

    这是正常运作的场景,也是理想的场景。作为一个分布式系统,它和单机系统的最大区别,就在于网络。现在假设一种极端情况,N1和N2之间的网络断开了,我们要支持这种网络异常。相当于要满足分区容错性,能不能同时满足一致性和可用性呢?还是说要对他们进行取舍?

    假设在N1和N2之间网络断开的时候,有用户向N1发送数据更新请求,那N1中的数据V0将被更新为V1。由于网络是断开的,所以分布式系统同步操作M,所以N2中的数据依旧是V0。这个时候,有用户向N2发送数据读取请求,由于数据还没有进行同步,应用程序没办法立即给用户返回最新的数据V1,怎么办呢?

    这里有两种选择:

    • 第一:牺牲数据一致性,保证可用性。响应旧的数据V0给用户。
    • 第二:牺牲可用性,保证数据一致性。阻塞等待,直到网络连接恢复,数据更新操作M完成之后,再给用户响应最新的数据V1。

    这个过程,证明了要满足分区容错性的分布式系统,只能在一致性和可用性两者中,选择其中一个。

    3. CAP原则权衡

    通过CAP理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?

    3.1. CA without P

    如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但其实分区不是你想不想的问题,而是始终会存在,因此CA的系统更多的是允许分区后各子系统依然保持CA。

    3.2. CP without A

    如果不要求A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。

    3.3. AP wihtout C

    要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。

    小结

    对于多数大型互联网应用的场景,主机众多、部署分散。而且现在的集群规模越来越大,所以节点故障、网络故障是常态。这种应用一般要保证服务可用性达到N个9,即保证P和A,只有舍弃C(退而求其次保证最终一致性)。虽然某些地方会影响客户体验,但没达到造成用户流程的严重程度。

    对于涉及到钱财这样不能有一丝让步的场景,C必须保证。网络发生故障宁可停止服务,这是保证CA,舍弃P。貌似这几年国内银行业发生了不下10起事故,但影响面不大,报到也不多,广大群众知道的少。还有一种是保证CP,舍弃A,例如网络故障时只读不写。

    孰优孰劣,没有定论,只能根据场景定夺,适合的才是最好的

    发表在 设计 | 留下评论

    RESTful API 设计最佳实践

    RESTful API设计最佳实践:

    使用的名词而不是动词
    GET 方法和查询参数不能改变资源状态
    使用名词复数
    使用子资源来表达资源间的关系
    使用 HTTP header 来序列化格式
    使用 HATEOAS 约束
    (Richardson 提出的 REST 成熟度模型介绍)
    提供过滤、排序、字段选择、分页
    API 版本化:版本号使用简单的序号,并避免点符号
    充分使用 HTTP 状态码来处理错误

    继续阅读

    发表在 设计 | 留下评论