《Java架构师的第一性原理》30分布式计算之分布式算法

极客时间 韩健 121.分布式协议与算法实战

00 开篇词 | 想成为分布式高手?那就先把协议和算法烂熟于心吧

为什么要单独讲分布式协议和算法呢?

在我看来,它其实就是决定分布式系统如何运行的核心规则和关键步骤。 如果一个人想真正搞懂分布式技术,开发出一个分布式系统,最先需要掌握的就是这部分知识。

举个例子,学数学的时候,我们总是会学到很多公式或者定理,我上学的时候,还觉得这些定理枯燥至极。但后来我明白了,这些定理和公式其实就是前人花了很长时间思考、验证、总结出来的规律,如果我们能在这之上做事情,更容易快速地找到正确答案。同样,你学习咱们这个专栏也是这个道理。

分布式算法是分布式技术中的核心。

因为分布式系统里,最重要的事情,就是如何选择或设计适合的算法,解决一致性和可用性相关的问题了。

InfluxDB 系统,它的护城河就是以分布式算法为核心的分布式集群能力。
 
我将课程划分了三个模块,分别是理论篇、协议和算法篇以及实战篇。
  • 理论篇,我会带你搞懂分布式架构设计核心且具有“实践指导性”的基础理论,这里面会涉及典型的分布式问题,以及如何认识分布式系统中相互矛盾的特性,帮助你在实战中根据场景特点选择适合的分布式算法。
  • 协议和算法篇,会让你掌握它们的原理、特点、适用场景和常见误区等。比如,你以为开发分布式系统使用 Raft 算法就可以了,其实它比较适合性能要求不高的强一致性场景;又比如在面试时,如果被问到“Paxos 和 Raft 的区别在哪里”,你都会在第二部分中找到答案。
  • 实战篇,教你如何将所学知识落地,我会带你掌握分布式基础理论和分布式算法在工程实践中的应用。比如,剖析 InfluxDB 企业版的 CP 架构和 AP 架构的设计和背后的思考,以及Raft、Quorum NWR、Anti-Entropy 等分布式算法的具体实现。
除此之外,我还会带你剖析 Hashicorp Raft 的实现,并以一个分布式 KV 系统的开发实战为例,来聊聊如何使用 Raft 算法实际开发一个分布式系统,以此让你全面拥有分布式算法的实战能力。
总体来说,学完这次课程,你会有以下几个收获:
  • 破除你对分布式协议和算法的困惑,帮助你建立信心;
  • 可落地的4大分布式基础理论;
  • 8 个最常用的分布式协议和算法;.
  • 3大实战案例手把手教学;
  • 以实战为中心的分布式内容体系;

01丨拜占庭将军问题:有叛徒的情况下,如何才能达成共识?

拜占庭将军问题(The Byzantine Generals Problem),它其实是借拜占庭将军的故事展现了分布式共识问题,还探讨和论证了解决的办法。

实际上,它是分布式领域最复杂的一个容错模型,一旦搞懂它,你就能掌握分布式共识问题的解决思路,还能更深刻地理解常用的共识算法,在设计分布式系统的时候,也能根据场景特点选择适合的算法,或者设计适合的算法了。

1)苏秦的困境

所以,如何达成共识,制定统一的作战计划呢?苏秦他很愁。

这个故事,是拜占庭将军问题的一个简化表述,苏秦面临的就是典型的共识难题,也就是如何在可能有误导信息的情况下,采用合适的通讯机制,让多个将军达成共识,制定一致性的作战计划?

2)二忠一叛的难题

3)苏秦该怎么办

解决办法一:口信消息型拜占庭问题之解。

这个解决办法,其实是兰伯特在论文《The Byzantine Generals Problem》中提到的口信消息型拜占庭问题之解:如果叛将人数为 m,将军人数不能少于 3m + 1 ,那么拜占庭将军问题就能解决了。

这个算法有个前提,也就是叛将人数 m,或者说能容忍的叛将数 m,是已知的。在这个算法中,叛将数 m 决定递归循环的次数(也就是说,叛将数 m 决定将军们要进行多少轮作战信息协商),即 m+1 轮(所以,你看,只有楚是叛变的,那么就进行了两轮)。你也可以从另外一个角度理解:n 位将军,最多能容忍 (n - 1) / 3 位叛将。关于这个公式,你只需要记住就好了,推导过程你可以参考论文。

不过,这个算法虽然能解决拜占庭将军问题,但它有一个限制:如果叛将人数为 m,那么将军总人数必须不小于 3m + 1。

在二忠一叛的问题中,在存在 1 位叛将的情况下,必须增加 1 位将军,将 3 位将军协商共识,转换为 4 位将军协商共识,这样才能实现忠诚将军的一致性作战计划。那么有没有办法,在不增加将军人数的时候,直接解决二忠一叛的难题呢?

解决办法二:签名消息型拜占庭问题之解

4)内容小结

那么我想强调的是,拜占庭将军问题描述的是最困难的,也是最复杂的一种分布式故障场景,除了存在故障行为,还存在恶意行为的一个场景。你要注意,在存在恶意节点行为的场景中(比如在数字货币的区块链技术中),必须使用拜占庭容错算法(Byzantine FaultTolerance,BFT)。除了故事中提到两种算法,常用的拜占庭容错算法还有:PBFT 算法,PoW 算法(为了重点突出,这些内容我会在后面讲解)。

而在计算机分布式系统中,最常用的是非拜占庭容错算法,即故障容错算法(Crash FaultTolerance,CFT)。CFT 解决的是分布式的系统中存在故障,但不存在恶意节点的场景下的共识问题。 也就是说,这个场景可能会丢失消息,或者有消息重复,但不存在错误消息,或者伪造消息的情况。常见的算法有 Paxos 算法、Raft 算法、ZAB 协议(这些内容我同样会在后面讲解)。

那么,如何在实际场景选择合适的算法类型呢?答案是:如果能确定该环境中各节点是可信赖的,不存在篡改消息或者伪造消息等恶意行为(例如 DevOps 环境中的分布式路由寻址系统),推荐使用非拜占庭容错算法;反之,推荐使用拜占庭容错算法,例如在区块链中使用 PoW 算法。

02丨CAP理论:分布式系统的PH试纸,用它来测酸碱度

很多同学可能都有这样的感觉,每次要开发分布式系统的时候,就会遇到一个非常棘手的问题,那就是如何根据业务特点,为系统设计合适的分区容错一致性模型,以实现集群能力。这个问题棘手在当发生分区错误时,应该如何保障系统稳定运行,不影响业务。

这和我之前经历的一件事比较像,当时,我负责自研 InfluxDB 系统的项目,接手这个项目后,我遇到的第一个问题就是,如何为单机开源版的 InfluxDB 设计分区容错一致性模型。因为 InfluxDB 有 META 和 DATA 两个节点,它们的功能和数据特点不同,所以我还需要考虑这两个逻辑单元的特点,然后分别设计分区容错一致性模型。

那个时候,我想到了 CAP 理论,并且在 CAP 理论的帮助下,成功地解决了问题。讲到这儿,你可能会问了:为什么 CAP 理论可以解决这个问题呢?

因为在我看来,CAP 理论是一个很好的思考框架,它对分布式系统的特性做了高度抽象,比如抽象成了一致性、可用性和分区容错性,并对特性间的冲突(也就是 CAP 不可能三角)做了总结。一旦掌握它,你就像拥有了引路人,自然而然就能根据业务场景的特点进行权衡,设计出适合的分区容错一致性模型。

1)CAP 三指标

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分区容错性(Partition Tolerance)

一致性说的是客户端的每次读操作,不管访问哪个节点,要么读到的都是同一份最新的数据,要么读取失败。

你可以把一致性看作是分布式系统对访问本系统的客户端的一种承诺:不管你访问哪个节点,要么我给你返回的都是绝对一致的数据,要么你都读取失败。你可以看到,一致性强调的不是数据完整,而是各节点间的数据一致。

一致性这个指标,描述的是分布式系统非常重要的一个特性,强调的是数据的一致。也就是说,在客户端看来,集群和单机在数据一致性上是一样的。
 不过集群毕竟不是单机,当发生分区故障的时候,有时不能仅仅因为节点间出现了通讯问题,节点中的数据会不一致,就拒绝写入新数据,之后在客户端查询数据时,就一直返回给客户端出错信息。这句话怎么理解呢?我来举个例子。
业务集群中的一些关键系统,比如名字路由系统,如果仅仅因为发生了分布故障,节点中的数据会不一致,集群就拒绝写入新的路由信息,之后,当客户端查询相关路由信息时,系统就一直返回给客户端出错信息,那么相关的服务都将因为获取不到指定路由信息而不可用、瘫痪,这可以说是灾难性的故障了。
 
这个时候,我们就需要牺牲数据的一致性,每个节点使用本地数据来响应客户端请求,来保证服务可用,这就是我要说的另外一个指标,可用性。
 
可用性说的是任何来自客户端的请求,不管访问哪个节点,都能得到响应数据,但不保证是同一份最新数据。你也可以把可用性看作是分布式系统对访问本系统的客户端的另外一种承诺:我尽力给你返回数据,不会不响应你,但是我不保证每个节点给你的数据都是最新的。这个指标强调的是服务可用,但不保证数据的一致。
 
我还是用一个例子,帮助你理解一下。比如,用户可以选择向节点 1 或节点 2 发起读操作,如果不管节点间的数据是否一致,只要节点服务器收到请求,就响应 X 的值,那么,2 个节点的服务是满足可用性的。

 

最后的分区容错性说的是,当节点间出现任意数量的消息丢失或高延迟的时候,系统仍然可以继续提供服务。也就是说,分布式系统在告诉访问本系统的客户端:不管我的内部出现什么样的数据同步问题,我会一直运行,提供服务。这个指标,强调的是集群对分区故障的容错能力。

因为分布式系统与单机系统不同,它涉及到多节点间的通讯和交互,节点间的分区故障是必然发生的,所以我要提醒你,在分布式系统中分区容错性是必须要考虑的。

现在你了解了一致性、可用性和分区容错性,那么你在设计分布式系统时,是选择一致性?还是可用性?还是分区容错性?还是都可以选择呢?这三个特性有什么冲突么?这些问题就与我接下来要讲的“CAP 不可能三角”有关了。

2)CAP 不可能三角

CAP 不可能三角说的是对于一个分布式系统而言,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)3 个指标不可兼得,只能在 3 个指标中选择 2 个。

 

CAP 不能三角最初是埃里克·布鲁尔(Eric Brewer)基于自己的工程实践,提出的一个猜想,后被赛斯·吉尔伯特(Seth Gilbert)和南希·林奇(Nancy Lynch)证明,证明过程可以参考论文《Brewer’s conjecture and the feasibility of consistent, available,partition-tolerant web services》,你记住结论就好了。不过,为了帮你阅读论文,我补充一点:

基于证明严谨性的考虑,赛斯·吉尔伯特(Seth Gilbert)和南希·林奇(Nancy Lynch)对指标的含义做了预设和限制,比如,将一致性限制为原子性。

3)如何使用 CAP 理论

我们都知道,只要有网络交互就一定会有延迟和数据丢失,而这种状况我们必须接受,还必须保证系统不能挂掉。所以就像我上面提到的,节点间的分区故障是必然发生的。也就是说,分区容错性(P)是前提,是必须要保证的。
现在就只剩下一致性(C)和可用性(A)可以选择了:要么选择一致性,保证数据绝对一致;要么选择可用性,保证服务可用。那么 CP 和 AP 的含义是什么呢?
  • 当选择了一致性(C)的时候,如果因为消息丢失、延迟过高发生了网络分区,部分节点无法保证特定信息是最新的,那么这个时候,当集群节点接收到来自客户端的写请求时,因为无法保证所有节点都是最新信息,所以系统将返回写失败错误,也就是说集群拒绝新数据写入。
  • 当选择了可用性(A)的时候,系统将始终处理客户端的查询,返回特定信息,如果发生了网络分区,一些节点将无法返回最新的特定信息,它们将返回自己当前的相对新的信息。

这里我想强调一点,大部分人对 CAP 理论有个误解,认为无论在什么情况下,分布式系统都只能在 C 和 A 中选择 1 个。 其实,在不存在网络分区的情况下,也就是分布式系统正常运行时(这也是系统在绝大部分时候所处的状态),就是说在不需要 P 时,C 和 A 能够同时保证。只有当发生分区故障的时候,也就是说需要 P 时,才会在 C 和 A 之间做出选择。而且如果各节点数据不一致,影响到了系统运行或业务运行(也就是说会有负面的影响),推荐选择 C,否则选 A。

4)我的场景

那么我当时是怎么根据场景特点,进行 CAP 权衡,设计适合的分布式系统呢?为了便于你理解,我先来说说背景。

开源版的 InfluxDB,缺乏集群能力和可用性,而且,InfluxDB 是由 META 节点和 DATA节点 2 个逻辑单元组成,这 2 个节点的功能和数据特点不同,需要我们分别为它们设计分区容错一致性模型。

我具体是这么设计的:

  • 作为分布式系统,分区容错性是必须要实现的,不能因为节点间出现了分区故障,而出现整个系统不能用的情况
  • 考虑到 META 节点保存的是系统运行的关键元信息,比如数据库名、表名、保留策略信息等,所以必须保持所有节点的一致性,这样才能避免由于各节点元信息不一致,导致时序数据记录不一致或者影响系统运行。比如,数据库 Telegraf 的信息在一些节点上存在,在另外一些节点上不存在,那么将导致向某些节点写入时序数据记录失败,所以,我选择 CAP 理论中的 C 和 P,采用 CP 架构。
  • DATA 节点保存的是具体的时序数据记录,比如一条记录 CPU 负载的时序数据,“cpu_usage,host=server01,location=cn-sz user=23.0,system=57.0”。虽然不是系统运行相关的元信息,但服务会被访问频繁,水平扩展、性能、可用性等是关键,所以,我选择了 CAP 理论中的 A 和 P,采用 AP 架构。

你看,我用 CAP 理论进行思考,并分别设计了 InfluxDB 的 META 节点和 DATA 节点的分区容错一致性模型,而你也可以采用类似的思考方法,设计出符合自己业务场景的分区容错一致性模型。

那么假设我当时没有受到 CAP 理论的影响,或者对 CAP 理论理解不深入,DATA 节点不采用 AP 架构,而是直接使用了现在比较流行的分区容错一致性算法,比如使用 Raft 算法,会有什么痛点呢?

  • 受限于 Raft 的强领导者模型。所有请求都在领导者节点上处理,整个集群的性能等于单机性能。这样会造成集群接入性能低下,无法支撑海量或大数据量的时序数据。
  • 受限于强领导者模型,以及 Raft 的节点和副本一一对应的限制,无法实现水平扩展,分布式集群扩展了读性能,但写性能并没有提升。这样会出现写性能低下,和因为架构上的限制,无法提升写性能的问题。
  • Raft 的“一切以领导者为准”的日志复制特性,会导致 DATA 节点丢数据,出现时序数据记录缺失的问题。CA 模型,在分布式系统中不存在。因为舍弃 P,意味着舍弃分布式系统,就比如单机版关系型数据库 MySQL,如果 MySQL 要考虑主备或集群部署时,它必须考虑 P。CP 模型,采用 CP 模型的分布式系统,一旦因为消息丢失、延迟过高发生了网络分区,就影响用户的体验和业务的可用性。因为为了防止数据不一致,集群将拒绝新数据的写

关于 Raft 算法的一些细节(比如强领导模型),我会在 07 讲详细带你了解,这里你知道有这么回事儿就可以了。

最后我想再次强调的是,一致性不等同于完整性, 有些技术团队基于数据完整性的考虑,使用 Raft 算法实现 DATA 节点的数据的分布式一致性容错,恰恰是这个设计,会导致DATA 节点丢数据。我希望你能注意到这一点。

那么在这里,我也想考考你:如果 META 节点采用 AP 架构,会有什么痛点呢?你可以思考一下。

5)内容小结

本节课我主要带你了解了 CAP 理论,以及 CAP 理论的应用,我希望你明确的重点如下:

  • CA 模型,在分布式系统中不存在。因为舍弃 P,意味着舍弃分布式系统,就比如单机版关系型数据库 MySQL,如果 MySQL 要考虑主备或集群部署时,它必须考虑 P。
  • CP 模型,采用 CP 模型的分布式系统,一旦因为消息丢失、延迟过高发生了网络分区,就影响用户的体验和业务的可用性。因为为了防止数据不一致,集群将拒绝新数据的写入,典型的应用是 ZooKeeper,Etcd 和 HBase。
  • AP 模型,采用 AP 模型的分布式系统,实现了服务的高可用。用户访问系统的时候,都能得到响应数据,不会出现响应错误,但当出现分区故障时,相同的读操作,访问不同的节点,得到响应数据可能不一样。典型应用就比如 Cassandra 和 DynamoDB。
在我看来,CAP 理论像 PH 试纸一样,可以用来度量分布式系统的酸碱值,帮助我们思考如何设计合适的酸碱度,在一致性和可用性之间进行妥协折中,设计出满足场景特点的分布式系统。关于酸(Acid)和碱(Base),我会在 03 和 04 讲带你了解。
 
最后我想说的是,在当前分布式系统开发中,延迟是非常重要的一个指标,比如,在 QQ后台的名字路由系统中,我们通过延迟评估服务可用性,进行负载均衡和容灾;再比如,在Hashicorp/Raft 实现中,通过延迟评估领导者节点的服务可用性,以及决定是否发起领导者选举。所以,我希望你在分布式系统的开发中,也能意识到延迟的重要性,能通过延迟来衡量服务的可用性。

03丨ACID理论:CAP的酸,追求一致性

提到 ACID,我想你并不陌生,很多同学也会觉得它容易理解,在单机上实现 ACID 也不难,比如可以通过锁、时间序列等机制保障操作的顺序执行,让系统实现 ACID 特性。但是,一说要实现分布式系统的 ACID 特性,很多同学就犯难了。那么问题来了,为什么分布式系统的 ACID 特性在实现上,比较难掌握呢?

在我看来,ACID 理论是对事务特性的抽象和总结,方便我们实现事务。你可以理解成:如果实现了操作的 ACID 特性,那么就实现了事务。而大多数人觉得比较难,是因为分布式系统涉及多个节点间的操作。加锁、时间序列等机制,只能保证单个节点上操作的 ACID 特性,无法保证节点间操作的 ACID 特性。

那么怎么做才会让实现不那么难呢?答案是你要掌握分布式事务协议,比如二阶段提交协议和 TCC(Try-Confirm-Cancel)。这也是我接下来重点和你分享的内容。

不过在带你了解二阶段提交协议和 TCC 之前,咱们先继续看看苏秦的故事,看这回苏秦又遇到了什么事儿。

1)苏秦的新问题

那么对苏秦来说,他面临的问题是,如何高效协同赵、魏、韩一起行动,并且保证当有一方不方便行动时,取消整个计划。苏秦面对的这个新问题,就是典型的如何实现分布式事务的问题,赵、魏、韩明天攻打秦国,这三个操作组成一个分布式事务,要么全部执行,要么全部不执行。了解了这个问题之后,我们看看如何通过二阶段提交协议和 TCC,来帮助苏秦解决这个难题。
2)二阶段提交协议
二阶段提交协议,顾名思义,就是通过二阶段的协商来完成一个提交操作,那么具体是怎么操作的呢?
二阶段提交协议最早是用来实现数据库的分布式事务的,不过现在最常用的协议是 XA 协议。这个协议是 X/Open 国际联盟基于二阶段提交协议提出的,也叫作 X/Open Distributed Transaction Processing(DTP)模型,比如 MySQL 就是通过 MySQL XA实现了分布式事务。
但是不管是原始的二阶段提交协议,还是 XA 协议,都存在一些问题:
  • 在提交请求阶段,需要预留资源,在资源预留期间,其他人不能操作(比如,XA 在第一阶段会将相关资源锁定);
  • 数据库是独立的系统。
 因为上面这两点,我们无法根据业务特点弹性地调整锁的粒度,而这些都会影响数据库的并发性能。那用什么办法可以解决这些问题呢?答案就是 TCC。
 
3)TCC(Try-Confirm-Cancel)
TCC 是 Try(预留)、Confirm(确认)、Cancel(撤销) 3 个操作的简称,它包含了预留、确认或撤销这 2 个阶段。那么你如何使用 TCC 协议,解决苏秦面临的问题呢?
其实在我看来,TCC 本质上是补偿事务,它的核心思想是针对每个操作都要注册一个与其对应的确认操作和补偿操作(也就是撤销操作)。 它是一个业务层面的协议,你也可以将TCC 理解为编程模型,TCC 的 3 个操作是需要在业务代码中编码实现的,为了实现一致性,确认操作和补偿操作必须是等幂的,因为这 2 个操作可能会失败重试。
另外,TCC 不依赖于数据库的事务,而是在业务中实现了分布式事务,这样能减轻数据库的压力,但对业务代码的入侵性也更强,实现的复杂度也更高。所以,我推荐在需要分布式事务能力时,优先考虑现成的事务型数据库(比如 MySQL XA),当现有的事务型数据库不能满足业务的需求时,再考虑基于 TCC 实现分布式事务。
4)内容小结
本节课我主要带你了解了实现分布式系统 ACID 特性的方法,二阶段提交协议和 TCC,我希望你明确这样几个重点。
  • 二阶段提交协议,不仅仅是协议,也是一种非常经典的思想。二阶段提交在达成提交操作共识的算法中应用广泛,比如 XA 协议、TCC、Paxos、Raft 等。我希望你不仅能理解二阶段提交协议,更能理解协议背后的二阶段提交的思想,当后续需要时,能灵活地根据二阶段提交思想,设计新的事务或一致性协议。
  • 幂等性,是指同一操作对同一系统的任意多次执行,所产生的影响均与一次执行的影响相同,不会因为多次执行而产生副作用。常见的实现方法有 Token、索引等。它的本质是通过唯一标识,标记同一操作的方式,来消除多次执行的副作用。
  • Paxos、Raft 等强一致性算法,也采用了二阶段提交操作,在“提交请求阶段”,只要大多数节点确认就可以,而具有 ACID 特性的事务,则要求全部节点确认可以。所以可以将具有 ACID 特性的操作,理解为最强的一致性。
另外,我想补充一下,三阶段提交协议,虽然针对二阶段提交协议的“协调者故障,参与者长期锁定资源”的痛点,通过引入了询问阶段和超时机制,来减少资源被长时间锁定的情况,不过这会导致集群各节点在正常运行的情况下,使用更多的消息进行协商,增加系统负载和响应延迟。也正是因为这些问题,三阶段提交协议很少被使用,所以,你只要知道有这么个协议就可以了,但如果你想继续研究,可以参考《Concurrency Control andRecovery in Database Systems》来学习。
 
最后我想强调的是,你可以将 ACID 特性理解为 CAP 中一致性的边界,最强的一致性,也就是 CAP 的酸(Acid)。根据 CAP 理论,如果在分布式系统中实现了一致性,可用性必然受到影响。比如,如果出现一个节点故障,则整个分布式事务的执行都是失败的。实际上,绝大部分场景对一致性要求没那么高,短暂的不一致是能接受的,另外,也基于可用性和并发性能的考虑,建议在开发实现分布式系统,如果不是必须,尽量不要实现事务,可以考虑采用强一致性或最终一致性。

04丨BASE理论:CAP的碱,追求可用性

难道没有现成的库或者方案,来实现合适的 AP 模型?是的,的确没有。因为它是一个动态模型,是基于业务场景特点妥协折中后设计实现的。不过,你可以借助 BASE 理论帮助你达成目的。

在我看来,BASE 理论是 CAP 理论中的 AP 的延伸,是对互联网大规模分布式系统的实践总结,强调可用性。几乎所有的互联网后台分布式系统都有 BASE 的支持,这个理论很重要,地位也很高。一旦掌握它,你就能掌握绝大部分场景的分布式系统的架构技巧,设计出适合业务场景特点的、高可用性的分布式系统。

而它的核心就是基本可用(Basically Available)和最终一致性(Eventuallyconsistent)。也有人会提到软状态(Soft state),在我看来,软状态描述的是实现服务可用性的时候系统数据的一种过渡状态,也就是说不同节点间,数据副本存在短暂的不一致。你只需要知道软状态是一种过渡状态就可以了,我们不多说。

1)实现基本可用的 4 板斧

在我看来,基本可用是说,当分布式系统在出现不可预知的故障时,允许损失部分功能的可用性,保障核心功能的可用性。就像弹簧一样,遇到外界的压迫,它不是折断,而是变形伸缩,不断适应外力,实现基本的可用。

具体说的话,你可以把基本可用理解成,当系统节点出现大规模故障的时候,比如专线的光纤被挖断、突发流量导致系统过载(出现了突发事件,服务被大量访问),这个时候可以通过服务降级,牺牲部分功能的可用性,保障系统的核心功能可用。

就拿 12306 订票系统基本可用的设计为例,这个订票系统在春运期间,因为开始售票后先到先得的缘故,会出现极其海量的请求峰值,如何处理这个问题呢?

咱们可以在不同的时间,出售不同区域的票,将访问请求错开,削弱请求峰值。比如,在春运期间,深圳出发的火车票在 8 点开售,北京出发的火车票在 9 点开售。这就是我们常说的流量削峰
另外,你可能已经发现了,在春运期间,自己提交的购票请求,往往会在队列中排队等待处理,可能几分钟或十几分钟后,系统才开始处理,然后响应处理结果,这就是你熟悉的延迟响应。 你看,12306 订票系统在出现超出系统处理能力的突发流量的情况下,会通过牺牲响应时间的可用性,保障核心功能的运行。
而 12306 通过流量削峰和延迟响应,是不是就实现了基本的可用呢?现在它不会再像最初的时候那样,常常 404 了吧?
再比如,你正负责一个互联网系统,突然出现了网络热点事件,好多用户涌进来,产生了海量的突发流量,系统过载了,大量图片因为网络超时无法显示。那么这个时候你可以通过哪些方法,保障系统的基本可用呢?
相信你马上就能想到体验降级, 比如用小图片来替代原始图片,通过降低图片的清晰度和大小,提升系统的处理能力。
然后你还能想到过载保护, 比如把接收到的请求放在指定的队列中排队处理,如果请求等待时间超时了(假设是 100ms),这个时候直接拒绝超时请求;再比如队列满了之后,就清除队列中一定数量的排队请求,保护系统不过载,实现系统的基本可用。
你看,和 12306 的设计类似,只不过你负责的互联网系统是通过牺牲部分功能的可用性,保障核心功能的运行。
我说了这么多,主要是想强调:基本可用在本质上是一种妥协,也就是在出现节点故障或系统过载的时候,通过牺牲非核心功能的可用性,保障核心功能的稳定运行。
我希望你能在后续的分布式系统的开发中,不仅掌握流量削峰、延迟响应、体验降级、过载保护这 4 板斧,更能理解这 4 板斧背后的妥协折中,从而灵活地处理不可预知的突发问题。带你了解了基本可用之后,我再来说说 BASE 理论中,另一个非常核心的内容:最终一致性。 

2)最终的一致

在我看来,最终一致性是说,系统中所有的数据副本在经过一段时间的同步后,最终能够达到一个一致的状态。也就是说,在数据一致性上,存在一个短暂的延迟。
 
几乎所有的互联网系统采用的都是最终一致性,只有在实在无法使用最终一致性,才使用强一致性或事务,比如,对于决定系统运行的敏感元数据,需要考虑采用强一致性,对于与钱有关的支付系统或金融系统的数据,需要考虑采用事务。
 
你可以将强一致性理解为最终一致性的特例,也就是说,你可以把强一致性看作是不存在延迟的一致性。在实践中,你也可以这样思考: 如果业务的某功能无法容忍一致性的延迟(比如分布式锁对应的数据),需要实现的是强一致性;如果能容忍短暂的一致性的延迟(比如 QQ 状态数据),就可以考虑最终一致性。
 
那么如何实现最终一致性呢?你首先要知道它以什么为准,因为这是实现最终一致性的关键。一般来说,在实际工程实践中有这样几种方式:
  • 以最新写入的数据为准,比如 AP 模型的 KV 存储采用的就是这种方式;
  • 以第一次写入的数据为准,如果你不希望存储的数据被更改,可以以它为准。
那实现最终一致性的具体方式是什么呢?常用的有这样几种。
  • 读时修复:在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 ReadRepair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据。
  • 写时修复:在写入数据,检测数据的不一致时,进行修复。比如 Cassandra 的 HintedHandoff 实现。具体来说,Cassandra 集群的节点之间远程写数据的时候,如果写失败就将数据缓存下来,然后定时重传,修复数据的不一致性。
  • 异步修复:这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。
在这里,我想强调的是因为写时修复不需要做数据一致性对比,性能消耗比较低,对系统运行影响也不大,所以我推荐你在实现最终一致性时优先实现这种方式。而读时修复和异步修复因为需要做数据的一致性对比,性能消耗比较多,在开发实际系统时,你要尽量优化一致性对比的算法,降低性能消耗,避免对系统运行造成影响。
 
另外,我还想补充一点,在实现最终一致性的时候,我推荐同时实现自定义写一致性级别(All、Quorum、One、Any), 让用户可以自主选择相应的一致性级别,比如可以通过设置一致性级别为 All,来实现强一致性。
3)如何使用 BASE 理论
我以自研 InfluxDB 系统中 DATA 节点的集群实现为例,带你来使用 BASE 理论。咱们先来看看如何保障基本可用。
DATA 节点的核心功能是读和写,所以基本可用是指读和写的基本可用。那么我们可以通过分片和多副本,实现读和写的基本可用。也就是说,将同一业务的数据先分片,然后再以多份副本的形式分布在不同的节点上。比如下面这张图,这个 3 节点 2 副本的集群,除非超过一半的节点都故障了,否则是能保障所有数据的读写的。
那么如果实现最终一致性呢?就像我上文提到的样子,我们可以通过写时修复和异步修复实现最终一致性。另外,还实现自定义写一致性级别,支持 All、Quorum、One、Any 4 种写一致性级别,用户在写数据的时候,可以根据业务数据的特点,设置不同的写一致性级别。

4)内容小结

本节课我主要带你了解了 BASE 理论,以及 BASE 理论的应用,我希望你明确几个重点:

  • BASE 理论是对 CAP 中一致性和可用性权衡的结果,它来源于对大规模互联网分布式系统实践的总结,是基于 CAP 定理逐步演化而来的。它的核心思想是,如果不是必须的话,不推荐实现事务或强一致性,鼓励可用性和性能优先,根据业务的场景特点,来实现非常弹性的基本可用,以及实现数据的最终一致性。
  • BASE 理论主张通过牺牲部分功能的可用性,实现整体的基本可用,也就是说,通过服务降级的方式,努力保障极端情况下的系统可用性。
  • ACID 理论是传统数据库常用的设计理念,追求强一致性模型。BASE 理论支持的是大型分布式系统,通过牺牲强一致性获得高可用性。BASE 理论在很大程度上,解决了事务型系统在性能、容错、可用性等方面痛点。另外我再多说一句,BASE 理论在 NoSQL 中应用广泛,是 NoSQL 系统设计的事实上的理论支撑。
最后我强调一下,对于任何集群而言,不可预知的故障的最终后果,都是系统过载。如何设计过载保护,实现系统在过载时的基本可用,是开发和运营互联网后台的分布式系统的重中之重。那么我建议你,在开发实现分布式系统,要充分考虑如何实现基本可用。 

04丨BASE理论:CAP的碱,追求可用性

难道没有现成的库或者方案,来实现合适的 AP 模型?是的,的确没有。因为它是一个动态模型,是基于业务场景特点妥协折中后设计实现的。不过,你可以借助 BASE 理论帮助你达成目的。

05 | Paxos算法(一):如何在多个节点间确定某变量的值?

提到分布式算法,就不得不提 Paxos 算法,在过去几十年里,它基本上是分布式共识的代名词,因为当前最常用的一批共识算法都是基于它改进的。比如,Fast Paxos 算法、Cheap Paxos 算法、Raft 算法、ZAB 协议等等。而很多同学都会在准确和系统理解 Paxos算法上踩坑,比如,只知道它可以用来达成共识,但不知道它是如何达成共识的。

这其实侧面说明了 Paxos 算法有一定的难度,可分布式算法本身就很复杂,Paxos 算法自然也不会例外,当然了,除了这一点,还跟兰伯特有关。

兰伯特提出的 Paxos 算法包含 2 个部分:
  • 一个是 Basic Paxos 算法,描述的是多节点之间如何就某个值(提案 Value)达成共识;
  • 另一个是 Multi-Paxos 思想,描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。

可因为兰伯特提到的 Multi-Paxos 思想,缺少代码实现的必要细节(比如怎么选举领导者),所以在理解上比较难。

为了让你理解 Paxos 算法,接下来我会用 2 节课的时间,分别以 Basic Paxos 和 Multi-Paxos 为核心,带你了解 Basic Paxos 如何达成共识,以及针对 Basic Paxos 的局限性Multi-Paxos 又是如何改进的。今天咱们先来聊聊 Basic Paxos。

在我看来,Basic Paxos 是 Multi-Paxos 思想的核心,说白了,Multi-Paxos 就是多执行几次 Basic Paxos。所以掌握它之后,你能更好地理解后几讲基于 Multi-Paxos 思想的共识算法(比如 Raft 算法),还能掌握分布式共识算法的最核心内容,当现在的算法不能满足业务需求,进行权衡折中,设计自己的算法。

1)来一道思考题

假设我们要实现一个分布式集群,这个集群是由节点 A、B、C 组成,提供只读 KV 存储服务。你应该知道,创建只读变量的时候,必须要对它进行赋值,而且这个值后续没办法修改。因此一个节点创建只读变量后就不能再修改它了,所以所有节点必须要先对只读变量的值达成共识,然后所有节点再一起创建这个只读变量。

那么,当有多个客户端(比如客户端 1、2)访问这个系统,试图创建同一个只读变量(比如 X),客户端 1 试图创建值为 3 的 X,客户端 2 试图创建值为 7 的 X,这样要如何达成共识,实现各节点上 X 值的一致呢?带着这个问题,我们进入今天的学习。

 

 在一些经典的算法中,你会看到一些既形象又独有的概念(比如二阶段提交协议中的协调者),Basic Paxos 算法也不例外。为了帮助人们更好地理解 Basic Paxos 算法,兰伯特在讲解时,也使用了一些独有而且比较重要的概念,提案、准备(Prepare)请求、接受(Accept)请求、角色等等,其中最重要的就是“角色”。因为角色是对 Basic Paxos 中最核心的三个功能的抽象,比如,由接受者(Acceptor)对提议的值进行投票,并存储接受的值。

2)你需要了解的三种角色

在 Basic Paxos 中,有提议者(Proposer)、接受者(Acceptor)、学习者(Learner)三种角色,他们之间的关系如下:

 

看着是不是有些复杂,其实并不难理解:

  • 提议者(Proposer):提议一个值,用于投票表决。为了方便演示,你可以把图 1 中的客户端 1 和 2 看作是提议者。但在绝大多数场景中,集群中收到客户端请求的节点,才是提议者(图 1 这个架构,是为了方便演示算法原理)。这样做的好处是,对业务代码没有入侵性,也就是说,我们不需要在业务代码中实现算法逻辑,就可以像使用数据库一样访问后端的数据。
  • 接受者(Acceptor):对每个提议的值进行投票,并存储接受的值,比如 A、B、C 三个节点。 一般来说,集群中的所有节点都在扮演接受者的角色,参与共识协商,并接受和存储数据。
  • 学习者(Learner):被告知投票的结果,接受达成共识的值,存储保存,不参与投票的过程。一般来说,学习者是数据备份节点,比如“Master-Slave”模型中的 Slave,被动地接受数据,容灾备份。
讲到这儿,你可能会有疑惑:前面不是说接收客户端请求的节点是提议者吗?这里怎么又是接受者呢?这是因为一个节点(或进程)可以身兼多个角色。想象一下,一个 3 节点的集群,1 个节点收到了请求,那么该节点将作为提议者发起二阶段提交,然后这个节点和另外2 个节点一起作为接受者进行共识协商,就像下图的样子:

 

 

其实,这三种角色,在本质上代表的是三种功能:
  • 提议者代表的是接入和协调功能,收到客户端请求后,发起二阶段提交,进行共识协商;
  • 接受者代表投票协商和存储数据,对提议的值进行投票,并接受达成共识的值,存储保存;
  • 学习者代表存储数据,不参与共识协商,只接受达成共识的值,存储保存。
因为一个完整的算法过程是由这三种角色对应的功能组成的,所以理解这三种角色,是你理解 Basic Paxos 如何就提议的值达成共识的基础。那么接下来,咱们看看如何使用 BasicPaxos 达成共识,解决开篇提到的那道思考题。

3)如何达成共识

想象这样一个场景,现在疫情这么严重,每个村的路都封得差不多了,就你的村委会不作为,迟迟没有什么防疫的措施。你决定给村委会提交个提案,提一些防疫的建议,除了建议之外,为了和其他村民的提案做区分,你的提案还得包含一个提案编号,来起到唯一标识的作用。

与你的做法类似,在 Basic Paxos 中,兰伯特也使用提案代表一个提议。不过在提案中,除了提案编号,还包含了提议值。为了方便演示,我使用[n, v]表示一个提案,其中 n 为提案编号,v 为提议值。

我想强调一下,整个共识协商是分 2 个阶段进行的(也就是我在 03 讲提到的二阶段提交)。那么具体要如何协商呢?

我们假设客户端 1 的提案编号为 1,客户端 2 的提案编号为 5,并假设节点 A、B 先收到来自客户端 1 的准备请求,节点 C 先收到来自客户端 2 的准备请求。

4)准备(Prepare)阶段

先来看第一个阶段,首先客户端 1、2 作为提议者,分别向所有接受者发送包含提案编号的准备请求:

 

你要注意,在准备请求中是不需要指定提议的值的,只需要携带提案编号就可以了,这是很多同学容易产生误解的地方。

接着,当节点 A、B 收到提案编号为 1 的准备请求,节点 C 收到提案编号为 5 的准备请求后,将进行这样的处理:

 

由于之前没有通过任何提案,所以节点 A、B 将返回一个 “尚无提案”的响应。也就是说节点 A 和 B 在告诉提议者,我之前没有通过任何提案呢,并承诺以后不再响应提案编号小于等于 1 的准备请求,不会通过编号小于 1 的提案。

节点 C 也是如此,它将返回一个 “尚无提案”的响应,并承诺以后不再响应提案编号小于等于 5 的准备请求,不会通过编号小于 5 的提案。
另外,当节点 A、B 收到提案编号为 5 的准备请求,和节点 C 收到提案编号为 1 的准备请求的时候,将进行这样的处理过程:

 

当节点 A、B 收到提案编号为 5 的准备请求的时候,因为提案编号 5 大于它们之前响应的准备请求的提案编号 1,而且两个节点都没有通过任何提案,所以它将返回一个 “尚无提案”的响应,并承诺以后不再响应提案编号小于等于 5 的准备请求,不会通过编号小于 5 的提案。

当节点 C 收到提案编号为 1 的准备请求的时候,由于提案编号 1 小于它之前响应的准备请求的提案编号 5,所以丢弃该准备请求,不做响应。

5)接受(Accept)阶段

第二个阶段也就是接受阶段,首先客户端 1、2 在收到大多数节点的准备响应之后,会分别发送接受请求:

 

 

当客户端 1 收到大多数的接受者(节点 A、B)的准备响应后,根据响应中提案编号最大的提案的值,设置接受请求中的值。因为该值在来自节点 A、B 的准备响应中都为空(也就是图 5 中的“尚无提案”),所以就把自己的提议值 3 作为提案的值,发送接受请求[1, 3]。

当客户端 2 收到大多数的接受者的准备响应后(节点 A、B 和节点 C),根据响应中提案编号最大的提案的值,来设置接受请求中的值。因为该值在来自节点 A、B、C 的准备响应中都为空(也就是图 5 和图 6 中的“尚无提案”),所以就把自己的提议值 7 作为提案的值,发送接受请求[5, 7]。

当三个节点收到 2 个客户端的接受请求时,会进行这样的处理:

 

当节点 A、B、C 收到接受请求[1, 3]的时候,由于提案的提案编号 1 小于三个节点承诺能通过的提案的最小提案编号 5,所以提案[1, 3]将被拒绝。

当节点 A、B、C 收到接受请求[5, 7]的时候,由于提案的提案编号 5 不小于三个节点承诺能通过的提案的最小提案编号 5,所以就通过提案[5, 7],也就是接受了值 7,三个节点就 X 值为 7 达成了共识。

讲到这儿我想补充一下,如果集群中有学习者,当接受者通过了一个提案时,就通知给所有的学习者。当学习者发现大多数的接受者都通过了某个提案,那么它也通过该提案,接受该提案的值。

通过上面的演示过程,你可以看到,最终各节点就 X 的值达成了共识。那么在这里我还想强调一下,Basic Paxos 的容错能力,源自“大多数”的约定,你可以这么理解:当少于一半的节点出现故障的时候,共识协商仍然在正常工作。

6)内容小结

本节课我主要带你了解了 Basic Paxos 的原理和一些特点,我希望你明确这样几个重点。

  • 你可以看到,Basic Paxos 是通过二阶段提交的方式来达成共识的。二阶段提交是达成共识的常用方式,如果你需要设计新的共识算法的时候,也可以考虑这个方式。
  • 除了共识,Basic Paxos 还实现了容错,在少于一半的节点出现故障时,集群也能工作。它不像分布式事务算法那样,必须要所有节点都同意后才提交操作,因为“所有节点都同意”这个原则,在出现节点故障的时候会导致整个集群不可用。也就是说,“大多数节点都同意”的原则,赋予了 Basic Paxos 容错的能力,让它能够容忍少于一半的节点的故障。
  • 本质上而言,提案编号的大小代表着优先级,你可以这么理解,根据提案编号的大小,接受者保证三个承诺,具体来说:如果准备请求的提案编号,小于等于接受者已经响应的准备请求的提案编号,那么接受者将承诺不响应这个准备请求;如果接受请求中的提案的提案编号,小于接受者已经响应的准备请求的提案编号,那么接受者将承诺不通过这个提案;如果接受者之前有通过提案,那么接受者将承诺,会在准备请求的响应中,包含已经通过的最大编号的提案信息。

06丨Paxos算法(二):Multi-Paxos不是一个算法,而是统称

经过上节课的学习,你应该知道,Basic Paxos 只能就单个值(Value)达成共识,一旦遇到为一系列的值实现共识的时候,它就不管用了。虽然兰伯特提到可以通过多次执行 Basic Paxos 实例(比如每接收到一个值时,就执行一次 Basic Paxos 算法)实现一系列值的共识。但是,很多同学读完论文后,应该还是两眼摸黑,虽然每个英文单词都能读懂,但还是不理解兰伯特提到的 Multi-Paxos,为什么 Multi-Paxos 这么难理解呢?

在我看来,兰伯特并没有把 Multi-Paxos 讲清楚,只是介绍了大概的思想,缺少算法过程的细节和编程所必须的细节(比如缺少选举领导者的细节)。这也就导致每个人实现的 Multi-Paxos 都不一样。不过从本质上看,大家都是在兰伯特提到的 Multi-Paxos 思想上补充细节,设计自己的 Multi-Paxos 算法,然后实现它(比如 Chubby 的 Multi-Paxos 实现、Raft 算法、ZAB 协议等)。
所以在这里,我补充一下:兰伯特提到的 Multi-Paxos 是一种思想,不是算法。而Multi-Paxos 算法是一个统称,它是指基于 Multi-Paxos 思想,通过多个 Basic Paxos实例实现一系列值的共识的算法(比如 Chubby 的 Multi-Paxos 实现、Raft 算法等)。这一点尤其需要你注意。
为了帮你掌握 Multi-Paxos 思想,我会先带你了解,对于 Multi-Paxos 兰伯特是如何思考的,也就是说,如何解决 Basic Paxos 的痛点问题;然后我再以 Chubby 的 Multi-Paxos实现为例,具体讲解一下。为啥选它呢?因为 Chubby 的 Multi-Paxos 实现,代表了Multi-Paxos 思想在生产环境中的真正落地,它将一种思想变成了代码实现。

1)兰伯特关于 Multi-Paxos 的思考

熟悉 Basic Paxos 的同学(可以回顾一下05 讲)可能还记得,Basic Paxos 是通过二阶段提交来达成共识的。在第一阶段,也就是准备阶段,接收到大多数准备响应的提议者,才能发起接受请求进入第二阶段(也就是接受阶段):

 

 

而如果我们直接通过多次执行 Basic Paxos 实例,来实现一系列值的共识,就会存在这样几个问题:
  • 如果多个提议者同时提交提案,可能出现因为提案冲突,在准备阶段没有提议者接收到大多数准备响应,协商失败,需要重新协商。你想象一下,一个 5 节点的集群,如果 3个节点作为提议者同时提案,就可能发生因为没有提议者接收大多数响应(比如 1 个提议者接收到 1 个准备响应,另外 2 个提议者分别接收到 2 个准备响应)而准备失败,需要重新协商。
  • 2 轮 RPC 通讯(准备阶段和接受阶段)往返消息多、耗性能、延迟大。你要知道,分布式系统的运行是建立在 RPC 通讯的基础之上的,因此,延迟一直是分布式系统的痛点,是需要我们在开发分布式系统时认真考虑和优化的。
那么如何解决上面的 2 个问题呢?可以通过引入领导者和优化 Basic Paxos 执行来解决,咱们首先聊一聊领导者。

2)领导者(Leader)

我们可以通过引入领导者节点,也就是说,领导者节点作为唯一提议者,这样就不存在多个提议者同时提交提案的情况,也就不存在提案冲突的情况了:

 

在这里,我补充一点:在论文中,兰伯特没有说如何选举领导者,需要我们在实现 Multi-Paxos 算法的时候自己实现。 比如在 Chubby 中,主节点(也就是领导者节点)是通过执行 Basic Paxos 算法,进行投票选举产生的。

那么,如何解决第二个问题,也就是如何优化 Basic Paxos 执行呢?

3)优化 Basic Paxos 执行

我们可以采用“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制,优化 Basic Paxos 执行。也就是说,领导者节点上,序列中的命令是最新的,不再需要通过准备请求来发现之前被大多数节点通过的提案,领导者可以独立指定提案中的值。这时,领导者在提交命令时,可以省掉准备阶段,直接进入到接受阶段:

 

你看,和重复执行 Basic Paxos 相比,Multi-Paxos 引入领导者节点之后,因为只有领导者节点一个提议者,只有它说了算,所以就不存在提案冲突。另外,当主节点处于稳定状态时,就省掉准备阶段,直接进入接受阶段,所以在很大程度上减少了往返的消息数,提升了性能,降低了延迟。

讲到这儿,你可能会问了:在实际系统中,该如何实现 Multi-Paxos 呢?接下来,我以Chubby 的 Multi-Paxos 实现为例,具体讲解一下。

4)Chubby 的 Multi-Paxos 实现

既然兰伯特只是大概的介绍了 Multi-Paxos 思想,那么 Chubby 是如何补充细节,实现Multi-Paxos 算法的呢?

首先,它通过引入主节点,实现了兰伯特提到的领导者(Leader)节点的特性。也就是说,主节点作为唯一提议者,这样就不存在多个提议者同时提交提案的情况,也就不存在提案冲突的情况了。

另外,在 Chubby 中,主节点是通过执行 Basic Paxos 算法,进行投票选举产生的,并且在运行过程中,主节点会通过不断续租的方式来延长租期(Lease)。比如在实际场景中,几天内都是同一个节点作为主节点。如果主节点故障了,那么其他的节点又会投票选举出新的主节点,也就是说主节点是一直存在的,而且是唯一的。

其次,在 Chubby 中实现了兰伯特提到的,“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制。

最后,在 Chubby 中,实现了成员变更(Group membership),以此保证节点变更的时候集群的平稳运行。

最后,我想补充一点:在 Chubby 中,为了实现了强一致性,读操作也只能在主节点上执行。 也就是说,只要数据写入成功,之后所有的客户端读到的数据都是一致的。具体的过程,就是下面的样子。

所有的读请求和写请求都由主节点来处理。当主节点从客户端接收到写请求后,作为提议者,执行 Basic Paxos 实例,将数据发送给所有的节点,并且在大多数的服务器接受了这个写请求之后,再响应给客户端成功:

 

当主节点接收到读请求后,处理就比较简单了,主节点只需要查询本地数据,然后返回给客户端就可以了:

Chubby 的 Multi-Paxos 实现,尽管是一个闭源的实现,但这是 Multi-Paxos 思想在实际场景中的真正落地,Chubby 团队不仅编程实现了理论,还探索了如何补充细节。其中的思考和设计非常具有参考价值,不仅能帮助我们理解 Multi-Paxos 思想,还能帮助我们理解其他的 Multi-Paxos 算法(比如 Raft 算法)。

5)内容小结

本节课我主要带你了解了 Basic Paxos 的局限,以及 Chubby 的 Multi-Paxos 实现。我希望你明确的重点如下:

  • 兰伯特提到的 Multi-Paxos 是一种思想,不是算法,而且还缺少算法过程的细节和编程所必须的细节,比如如何选举领导者等,这也就导致了每个人实现的 Multi-Paxos 都不一样。而 Multi-Paxos 算法是一个统称,它是指基于 Multi-Paxos 思想,通过多个Basic Paxos 实例实现一系列数据的共识的算法(比如 Chubby 的 Multi-Paxos 实现、Raft 算法等)。
  • Chubby 实现了主节点(也就是兰伯特提到的领导者),也实现了兰伯特提到的 “当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段” 这个优化机制,省掉 BasicPaxos 的准备阶段,提升了数据的提交效率,但是所有写请求都在主节点处理,限制了集群处理写请求的并发能力,约等于单机。
  • 因为在 Chubby 的 Multi-Paxos 实现中,也约定了“大多数原则”,也就是说,只要大多数节点正常运行时,集群就能正常工作,所以 Chubby 能容错(n - 1)/2 个节点的故障。
  • 本质上而言,“当领导者处于稳定状态时,省掉准备阶段,直接进入接受阶段”这个优化机制,是通过减少非必须的协商步骤来提升性能的。这种方法非常常用,也很有效。比如,Google 设计的 QUIC 协议,是通过减少 TCP、TLS 的协商步骤,优化 HTTPS 性能。我希望你能掌握这种性能优化思路,后续在需要时,可以通过减少非必须的步骤,优化系统性能。 

最后,我想说的是,我个人比较喜欢 Paxos 算法(兰伯特的 Basic Paxos 和 Multi-Paxos),虽然 Multi-Paxos 缺失算法细节,但这反而给我们提供了思考空间,让我们可以反复思考和考据缺失的细节,比如在 Multi-Paxos 中到底需不需要选举领导者,再比如如何实现提案编号等等。

但我想强调,Basic Paxos 是经过证明的,而 Multi-Paxos 是一种思想,缺失实现算法的必须编程细节,这就导致,Multi-Paxos 的最终算法实现,是建立在一个未经证明的基础之上的,正确性是个问号。

与此同时,实现 Multi-Paxos 算法,最大的挑战是如何证明它是正确的。 比如 Chubby的作者做了大量的测试,和运行一致性检测脚本,验证和观察系统的健壮性。在实际使用时,我不推荐你设计和实现新的 Multi-Paxos 算法,而是建议优先考虑 Raft 算法,因为Raft 的正确性是经过证明的。当 Raft 算法不能满足需求时,你再考虑实现和优化 Multi-Paxos 算法。

07丨Raft算法(一):如何选举领导者?

通过前两节课,我带你打卡了 Paxos 算法,今天我想和你聊聊最常用的共识算法,Raft 算法。Raft 算法属于 Multi-Paxos 算法,它是在兰伯特 Multi-Paxos 思想的基础上,做了一些简化和限制,比如增加了日志必须是连续的,只支持领导者、跟随者和候选人三种状态,在理解和算法实现上都相对容易许多。

除此之外,Raft 算法是现在分布式系统开发首选的共识算法。绝大多数选用 Paxos 算法的系统(比如 Cubby、Spanner)都是在 Raft 算法发布前开发的,当时没得选;而全新的系统大多选择了 Raft 算法(比如 Etcd、Consul、CockroachDB)。
 
对你来说,掌握这个算法,可以得心应手地处理绝大部分场景的容错和一致性需求,比如分布式配置系统、分布式 NoSQL 存储等等,轻松突破系统的单机限制。
 
如果要用一句话概括 Raft 算法,我觉得是这样的:从本质上说,Raft 算法是通过一切以领导者为准的方式,实现一系列值的共识和各节点日志的一致。这句话比较抽象,我来做个比喻,领导者就是 Raft 算法中的霸道总裁,通过霸道的“一切以我为准”的方式,决定了日志中命令的值,也实现了各节点日志的一致。
 

 我会用三讲的时间,分别以领导者选举、日志复制、成员变更为核心,讲解 Raft 算法的原理,在实战篇中,会带你进一步剖析 Raft 算法的实现,介绍基于 Raft 算法的分布式系统开发实战。那么我希望从原理到实战,在帮助你掌握分布式系统架构设计技巧和开发实战能力的同时,加深你对 Raft 算法的理解。

在课程开始之前,我们先来看一道思考题。

假设我们有一个由节点 A、B、C 组成的 Raft 集群(如图所示),因为 Raft 算法一切以领导者为准,所以如果集群中出现了多个领导者,就会出现不知道谁来做主的问题。在这样一个有多个节点的集群中,在节点故障、分区错误等异常情况下,Raft 算法如何保证在同一个时间,集群中只有一个领导者呢?带着这个问题,我们正式进入今天的学习。

 

 既然要选举领导者,那要从哪些成员中选举呢?除了领导者,Raft 算法还支持哪些成员身份呢?这部分内容是你需要掌握的,最基础的背景知识。

1)有哪些成员身份?

成员身份,又叫做服务器节点状态,Raft 算法支持领导者(Leader)、跟随者(Follower)和候选人(Candidate) 3 种状态。为了方便讲解,我们使用不同的图形表示不同的状态。在任何时候,每一个服务器节点都处于这 3 个状态中的 1 个。

  • 跟随者:就相当于普通群众,默默地接收和处理来自领导者的消息,当等待领导者心跳信息超时的时候,就主动站出来,推荐自己当候选人。
  • 候选人:候选人将向其他节点发送请求投票(RequestVote)RPC 消息,通知其他节点来投票,如果赢得了大多数选票,就晋升当领导者。
  • 领导者:蛮不讲理的霸道总裁,一切以我为准,平常的主要工作内容就是 3 部分,处理写请求、管理日志复制和不断地发送心跳信息,通知其他节点“我是领导者,我还活着,你们现在不要发起新的选举,找个新领导者来替代我。”

需要你注意的是,Raft 算法是强领导者模型,集群中只能有一个“霸道总裁”。

2)选举领导者的过程

那么这三个成员是怎么选出来领导者的呢?为了方便你理解,我以图例的形式演示一个典型的领导者选举过程。

首先,在初始状态下,集群中所有的节点都是跟随者的状态。

 

Raft 算法实现了随机超时时间的特性。也就是说,每个节点等待领导者节点心跳信息的超时时间间隔是随机的。通过上面的图片你可以看到,集群中没有领导者,而节点 A 的等待超时时间最小(150ms),它会最先因为没有等到领导者的心跳信息,发生超时。

这个时候,节点 A 就增加自己的任期编号,并推举自己为候选人,先给自己投上一张选票,然后向其他节点发送请求投票 RPC 消息,请它们选举自己为领导者。

 

如果其他节点接收到候选人 A 的请求投票 RPC 消息,在编号为 1 的这届任期内,也还没有进行过投票,那么它将把选票投给节点 A,并增加自己的任期编号。

 

 

如果候选人在选举超时时间内赢得了大多数的选票,那么它就会成为本届任期内新的领导者。

 

 

节点 A 当选领导者后,他将周期性地发送心跳消息,通知其他服务器我是领导者,阻止跟随者发起新的选举,篡权。
 

 

讲到这儿,你是不是发现领导者选举很容易理解?与现实中的议会选举也蛮类似?当然,你可能还是对一些细节产生一些疑问:

  • 节点间是如何通讯的呢?
  • 什么是任期呢?
  • 选举有哪些规则?
  • 随机超时时间又是什么?

3)选举过程四连问

老话说,细节是魔鬼。这些细节也是很多同学在学习 Raft 算法的时候比较难掌握的,所以我认为有必要具体分析一下。咱们一步步来,先来看第一个问题。
 
节点间如何通讯?
在 Raft 算法中,服务器节点间的沟通联络采用的是远程过程调用(RPC),在领导者选举中,需要用到这样两类的 RPC:
  • 请求投票(RequestVote)RPC,是由候选人在选举期间发起,通知各节点进行投票;
  • 日志复制(AppendEntries)RPC,是由领导者发起,用来复制日志和提供心跳消息
我想强调的是,日志复制 RPC 只能由领导者发起,这是实现强领导者模型的关键之一,希望你能注意这一点,后续能更好地理解日志复制,理解日志的一致是怎么实现的。
 
什么是任期?
我们知道,议会选举中的领导者是有任期的,领导者任命到期后,要重新开会再次选举。Raft 算法中的领导者也是有任期的,每个任期由单调递增的数字(任期编号)标识,比如节点 A 的任期编号是 1。任期编号是随着选举的举行而变化的,这是在说下面几点。
  • 跟随者在等待领导者心跳信息超时后,推举自己为候选人时,会增加自己的任期号,比如节点 A 的当前任期编号为 0,那么在推举自己为候选人时,会将自己的任期编号增加为 1。
  • 如果一个服务器节点,发现自己的任期编号比其他节点小,那么它会更新自己的编号到较大的编号值。比如节点 B 的任期编号是 0,当收到来自节点 A 的请求投票 RPC 消息时,因为消息中包含了节点 A 的任期编号,且编号为 1,那么节点 B 将把自己的任期编号更新为 1。
我想强调的是,与现实议会选举中的领导者的任期不同,Raft 算法中的任期不只是时间段,而且任期编号的大小,会影响领导者选举和请求的处理。
  • 在 Raft 算法中约定,如果一个候选人或者领导者,发现自己的任期编号比其他节点小,那么它会立即恢复成跟随者状态。比如分区错误恢复后,任期编号为 3 的领导者节点B,收到来自新领导者的,包含任期编号为 4 的心跳消息,那么节点 B 将立即恢复成跟随者状态。
  • 还约定如果一个节点接收到一个包含较小的任期编号值的请求,那么它会直接拒绝这个请求。比如节点 C 的任期编号为 4,收到包含任期编号为 3 的请求投票 RPC 消息,那么它将拒绝这个消息
在这里,你可以看到,Raft 算法中的任期比议会选举中的任期要复杂。同样,在 Raft 算法中,选举规则的内容也会比较多。
 
选举有哪些规则?
在议会选举中,比成员的身份、领导者的任期还要重要的就是选举的规则,比如一人一票、弹劾制度等。“无规矩不成方圆”,在 Raft 算法中,也约定了选举规则,主要有这样几点。
  • 领导者周期性地向所有跟随者发送心跳消息(即不包含日志项的日志复制 RPC 消息),通知大家我是领导者,阻止跟随者发起新的选举。
  • 如果在指定时间内,跟随者没有接收到来自领导者的消息,那么它就认为当前没有领导者,推举自己为候选人,发起领导者选举。
  • 在一次选举中,赢得大多数选票的候选人,将晋升为领导者。
  • 在一个任期内,领导者一直都会是领导者,直到它自身出现问题(比如宕机),或者因为网络延迟,其他节点发起一轮新的选举。
  • 在一次选举中,每一个服务器节点最多会对一个任期编号投出一张选票,并且按照“先来先服务”的原则进行投票。比如节点 C 的任期编号为 3,先收到了 1 个包含任期编号为 4 的投票请求(来自节点 A),然后又收到了 1 个包含任期编号为 4 的投票请求(来自节点 B)。那么节点 C 将会把唯一一张选票投给节点 A,当再收到节点 B 的投票请求RPC 消息时,对于编号为 4 的任期,已没有选票可投了。
 

 

 当任期编号相同时,日志完整性高的跟随者(也就是最后一条日志项对应的任期编号值更大,索引号更大),拒绝投票给日志完整性低的候选人。比如节点 B、C 的任期编号都是 3,节点 B 的最后一条日志项对应的任期编号为 3,而节点 C 为 2,那么当节点 C请求节点 B 投票给自己时,节点 B 将拒绝投票。

我想强调的是,选举是跟随者发起的,推举自己为候选人;大多数选票是指集群成员半数以上的选票;大多数选票规则的目标,是为了保证在一个给定的任期内最多只有一个领导者。

其实在选举中,除了选举规则外,我们还需要避免一些会导致选举失败的情况,比如同一任期内,多个候选人同时发起选举,导致选票被瓜分,选举失败。那么在 Raft 算法中,如何避免这个问题呢?答案就是随机超时时间。

 

如何理解随机超时时间?

在议会选举中,常出现未达到指定票数,选举无效,需要重新选举的情况。在 Raft 算法的选举中,也存在类似的问题,那它是如何处理选举无效的问题呢?

其实,Raft 算法巧妙地使用随机选举超时时间的方法,把超时时间都分散开来,在大多数情况下只有一个服务器节点先发起选举,而不是同时发起选举,这样就能减少因选票瓜分导致选举失败的情况。

我想强调的是,在 Raft 算法中,随机超时时间是有 2 种含义的,这里是很多同学容易理解出错的地方,需要你注意一下:

  • 跟随者等待领导者心跳信息超时的时间间隔,是随机的;
  • 当没有候选人赢得过半票数,选举无效了,这时需要等待一个随机时间间隔,也就是说,等待选举超时的时间间隔,是随机的。

 4)内容小结

以上就是本节课的全部内容了,本节课我主要带你了解了 Raft 算法的特点、领导者选举等。我希望你明确这样几个重点。

  • Raft 算法和兰伯特的 Multi-Paxos 不同之处,主要有 2 点。首先,在 Raft 中,不是所有节点都能当选领导者,只有日志最完整的节点,才能当选领导者;其次,在 Raft 中,日志必须是连续的。
  • Raft 算法通过任期、领导者心跳消息、随机选举超时时间、先来先服务的投票原则、大多数选票原则等,保证了一个任期只有一位领导,也极大地减少了选举失败的情况。
  • 本质上,Raft 算法以领导者为中心,选举出的领导者,以“一切以我为准”的方式,达成值的共识,和实现各节点日志的一致。
在本讲,我们使用 Raft 算法在集群中选出了领导者节点 A,那么选完领导者之后,领导者需要处理来自客户的写请求,并通过日志复制实现各节点日志的一致(下节课我会重点带你了解这一部分内容)。

08 | Raft算法(二):如何复制日志?

09丨Raft算法(三):如何解决成员变更的问题?

10丨一致哈希算法:如何分群,突破集群的“领导者”限制?

11丨Gossip协议:流言蜚语,原来也可以实现一致性

12丨QuorumNWR算法:想要灵活地自定义一致性,没问题!

13丨PBFT算法:有人作恶,如何达成共识?

14丨PoW算法:有办法黑比特币吗?

15丨ZAB协议:如何实现操作的顺序性?

16丨InfluxDB企业版一致性实现剖析:他山之石,可以攻玉

17丨HashicorpRaft(一):如何跨过理论和代码之间的鸿沟?

18丨HashicorpRaft(二):如何以“集群节点”为中心使用API?

19丨基于Raft的分布式KV系统开发实战(一):如何设计架构?

20丨基于Raft的分布式KV系统开发实战(二):如何实现代码?

加餐丨拜占庭将军问题:如何基于签名消息实现作战计划的一致性?

结束语丨静下心来,享受技术的乐趣 

 

92 RAFT协议

92.1 RAFT协议 in Redis

如果节点A向节点B发送ping消息,节点B没有在规定的时间内响应pong,那么节点A会标记节点B为pfail疑似下线状态,同时把B的状态通过消息的形式发送给其他节点,如果超过半数以上的节点都标记B为pfail状态,B就会被标记为fail下线状态,此时将会发生故障转移,优先从复制数据较多的从节点选择一个成为主节点,并且接管下线节点的slot,整个过程和哨兵非常类似,都是基于Raft协议做选举。

92.2 RAFT协议 in RocketMQ

Master和Slave之间是怎么同步数据的呢?

而消息在master和slave之间的同步是根据raft协议来进行的:

  1. 在broker收到消息后,会被标记为uncommitted状态
  2. 然后会把消息发送给所有的slave
  3. slave在收到消息之后返回ack响应给master
  4. master在收到超过半数的ack之后,把消息标记为committed
  5. 发送committed消息给所有slave,slave也修改状态为committed

92.3 RAFT协议 in Kafka

temp

93 ZAB协议

93.1 ZAB协议 in Zookeeper

ZAB协议包括两种基本的模式:崩溃恢复和消息广播。

当整个 Zookeeper 集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数

据同步。当集群中超过半数机器与该 Leader 服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案(超过半数同意)来进行事务请求处理。

选举算法和流程:FastLeaderElection(默认提供的选举算法)

目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下:

  1. 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。
  2. 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。
  3. 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为leader,服务器1,2成为follower。
  4. 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为follower。
  5. 服务器5启动,后面的逻辑同服务器4成为follower。

 

99 直接读这些牛人的原文

悟空聊架构:2万字 | 写了八篇分布式算法,我“悟空”了

 

 

posted @ 2023-12-21 14:04  沙漏哟  阅读(20)  评论(0编辑  收藏  举报