分布式技术必知必会
本文来自于极客时间《分布式原理与算法解析》
楔子
在众多计算机技术当中,分布式技术无疑是最璀璨的明珠之一。毫不夸张地说,没有分布式技术就没有互联网,也就没有现在的阿里巴巴、腾讯、亚马逊、Facebook、谷歌等科技巨头,更不会有以信息技术为核心的、对人类历史产生巨大变革的第三次工业革命。万维网、Email、DNS 等,都是分布式系统的典型代表。
随着分布式技术的不断发展,它也早已不再局限于传统的互联网等应用场景。两会期间,360 董事长周鸿祎更是大唱 I'M ABCDE 字母歌。IMABCDE 这 7 个字母分别代表 IoT 物联网、Mobile 移动计算、AI 人工智能、Blockchain 区块链、Cloud 云计算、Data 大数据、Edge 边缘计算,它们都是以分布式技术为基石。
所以分布式技术对于我们的职业生涯发展是非常重要的,那么本篇文章就来聊一聊分布式技术。
分布式因何而起:从单兵,到游击队,到集团军
我们先了解一下分布式的起源,如何从单台计算机发展到分布式,进而深入理解什么是分布式。
单机模式
单机模式:所有应用程序和数据均部署在一台电脑或服务器上,由一台计算机完成所有的处理。
以铁路售票系统为例,铁路售票系统包括用户管理、火车票管理和订单管理等模块,数据包括用户数据、火车票数据和订单数据等。如果使用单机模式,那么所有的模块和数据均会部署在同一台计算机上,也就是说数据存储、请求处理均由该计算机完成。这种模式的好处是功能、代码和数据集中,便于维护、管理和执行。
事物均有两面性,单机模式也存在相应的缺点。单个计算机的处理能力取决于 CPU 和内存等,但硬件的发展速度和性能是有限的,而且升级硬件的性价比也是我们要考虑的,由此决定了 CPU 和内存等硬件的性能将成为单机模式的瓶颈。
单机模式和战场上的单兵作战模式非常相似,单台计算机能力再强,就好比特种兵以一敌百,但终归能力有限。此外,将所有任务都交给一台计算机,也会存在将所有鸡蛋放到一个篮子里的风险,也就是单点故障问题。
归纳一下,单机模式的主要问题有两个:
性能有限
存在单点故障
数据并行模式
为解决单机模式的问题,并行计算得到了发展,进而出现了数据并行模式。并行计算采用消息共享模式,使用多台计算机并行执行多项任务,核心原理是每台计算机上执行相同的程序,并将数据拆分到不同的计算机上进行计算。
请注意,并行计算强调的是对数据进行拆分,任务程序在每台机器上运行。要达到这个目的,我们必须首先把单机模式中的应用和数据分离,才可能实现对数据的拆分。这里的应用就是执行任务的程序,任务就是提交的请求。以铁路售票系统为例,运行在服务器上的用户管理、火车票管理和订单管理等程序就是应用,用户提交的查询火车票、购买火车票的请求就是任务。
在单机模式中,应用和数据均在一台计算机或服务器上,要实现数据的并行,首先必须将应用和数据分离以便将应用部署到不同的计算机或服务器上。然后,对同类型的数据进行拆分,比方说,不同计算机或服务器上的应用可以到不同的数据库上获取数据执行任务。
将应用与数据分离,分别部署到不同的服务器上,但这只是第一步,我们还有第二步。对数据进行拆分,比如把同一类型的数据拆分到两个甚至更多的数据库中,这样应用服务器上的任务就可以针对不同数据并行执行了。
对于铁路售票系统来说,根据线路将用户、火车票和订单数据拆分到不同的数据库中,部署到不同的服务器上。比如京藏线的数据放在数据库服务器 1 上的数据库中,沪深线的数据放在数据库服务器 2 上的数据库中。
需要注意的是,为了更好地理解数据拆分的过程,我这里选择拆分数据库的方式进行讲解。由于数据库服务器本身的并发特性,因此你也可以根据自身的业务情况进行选择,比方说所有业务服务器共用一个数据库服务器,而不一定真的需要去进行数据库拆分。
可以看出,在数据并行模式中,每台计算机都是全量地从头到尾一条龙地执行一个程序,就像一个全能的铁道游击队战士。所以,你也可以将这种模式形象地理解成游击队模式,就和铁道游击队插曲的歌词有点类似:“我们扒飞车那个搞机枪,撞火车那个炸桥梁……”
这种模式的好处是,可以利用多台计算机并行处理多个请求,使得我们可以在相同的时间内完成更多的请求处理,解决了单机模式的计算效率瓶颈问题。但这种模式仍然存在如下几个问题,在实际应用中,我们需要对其进行相应的优化:
- 相同的应用部署到不同的服务器上,当大量用户请求过来时,如何均衡地转发到不同的应用服务器上呢?解决这个问题的方法是设计一个负载均衡器,后续会讲述负载均衡的相关原理。
- 当请求量较大时,对数据库的频繁读写操作,使得数据库的 IO 访问成为瓶颈。解决这个问题的方式是读写分离,读数据库只接收读请求,写数据库只接收写请求,当然读写数据库之间要进行数据同步,以保证数据一致性。
- 当有些数据成为热点数据时,会导致数据库访问频繁,压力增大。解决这个问题的方法是引入缓存机制,将热点数据加载到缓存中,一方面可以减轻数据库的压力,另一方面也可以提升查询效率。
从上面介绍可以看出,数据并行模式实现了多请求并行处理,但如果单个请求特别复杂,比方说需要几天甚至一周时间的时候,数据并行模式的整体计算效率还是不够高。
由此可见,数据并行模式的主要问题是:对提升单个任务的执行性能以及降低时延无效。
任务并行模式
那有没有办法可以提高单个任务的执行性能,或者缩短单个任务的执行时间呢?答案是肯定的。任务并行就是为解决这个问题而生的,那什么是任务并行呢?
任务并行指的是,将单个复杂的任务拆分为多个子任务,从而使得多个子任务可以在不同的计算机上并行执行。
我们仍以铁路售票系统为例,任务并行首先是对应用进行拆分,比如按照领域模型将用户管理、火车票管理、订单管理拆分成多个子系统,然后分别运行在不同的计算机或服务器上。换句话说,原本包括用户管理、火车票管理和订单管理的一个复杂任务,被拆分成了多个子任务在不同的服务器上执行,如下图所示:
可以看出,任务并行模式完成一项复杂任务主要有两个核心步骤:首先将单任务拆分成多个子任务,然后让多个子任务并行执行。这种模式和集团军模式很像,任务拆分者对应领导者,不同子系统对应不同兵种,不同子程序执行不同任务就像不同兵种执行不同命令一样,并且运行相同子系统或子任务的计算机又可以组成一个兵团。
在集团军模式中,由于多个子任务可以在多台计算机上运行,因此通过将同一任务的待处理数据分散到多个计算机上,在这些计算机上同时进行处理,就可以加快任务执行的速度。因为,只要一个复杂任务拆分出的任意子任务执行时间变短了,那么这个任务的整体执行时间就变短了。
当然,nothing is perfect。集团军模式在提供了更好的性能、扩展性、可维护性的同时,也带来了设计上的复杂性问题,毕竟对一个大型业务的拆分也是一个难题。不过,对于大型业务来讲,从长远收益来看,这个短期的设计阵痛是值得的。这也是许多大型互联网公司、高性能计算机构等,相继对业务进行拆分以及重构的一个重要原因。
分布式是什么?
讲了半天,那到底什么是分布式呢?
总结一下,分布式其实就是将相同或相关的程序运行在多台计算机上,从而实现特定目标的一种计算方式。
从这个定义来看,数据并行、任务并行其实都可以算作是分布式的一种形态。从这些计算方式的演变中不难看出,产生分布式的最主要驱动力量,是我们对性能、可用性以及可扩展性的不懈追求。
以上我们就学习了分布式的起源,从单机模式到数据并行模式,再到任务并行模式。这些模式的特点如下:
- 单机模式指的是,所有业务和数据均部署在同一台机器上。这种模式的好处是功能、代码和数据集中,便于维护、管理和执行,但计算效率是瓶颈。也就是说单机模式性能受限,也存在单点故障的问题。
- 数据并行模式指的是,对数据进行拆分,利用多台计算机并行执行多个相同任务。通过在相同的时间内完成多个相同任务,从而缩短所有任务的总体执行时间,但对提升单个任务的执行性能及降低时延无效。
- 任务并行模式指的是,单任务拆分成多个子任务,多个子任务并行执行,只要一个复杂任务中的任意子任务的执行时间变短了,那么这个业务的整体执行时间也就变短了。该模式在提高性能、扩展性、可维护性等的同时,也带来了设计上的复杂性问题,比如复杂任务的拆分。
在数据并行和任务并行这两个模式的使用上,很多人通常会比较疑惑,到底是采用数据并行还是任务并行呢?一个简单的原则就是:任务执行时间短,数据规模大、类型相同且无依赖,则可采用数据并行;如果任务复杂、执行时间长,且任务可拆分为多个子任务,则考虑任务并行。在实际业务中,通常是这两种模式并用。
分布式系统的评价指标
从分布式技术的起源可以看出,分布式系统的出现就是为了用廉价的普通机器,来解决单个计算机处理大规模复杂数据时存在的性能问题、资源瓶颈问题,以及可用性和可扩展性问题。换句话说,分布式的目的是用更多的机器,处理更多的数据和更复杂的任务。
由此可以看出,性能、资源、可用性和可扩展性是分布式系统的重要指标,接下来,我们一起来看看这几个指标吧。
性能(Performance)
性能指标,主要用于衡量一个系统处理各种任务的能力。无论是分布式系统还是单机系统,都会对性能有所要求。
不同的系统、服务要达成的目的不同,关注的性能自然也不尽相同,甚至是相互矛盾。常见的性能指标,包括吞吐量(Throughput)、响应时间(Response Time)和完成时间(Turnaround Time)。
吞吐量指的是系统在一定时间内可以处理的任务数。这个指标可以非常直接地体现一个系统的性能,就好比在客户非常多的情况下,要评判一个银行柜台职员的办事效率,你可以统计一下他在 1 个小时内接待了多少客户。常见的吞吐量指标有 QPS(Queries Per Second)、TPS(Transactions Per Second)和 BPS(Bits Per Second)。
- QPS,即每秒查询数,用于衡量一个系统每秒处理的查询数。这个指标通常用于读操作,越高说明对读操作的支持越好。我们在设计一个分布式系统的时候,如果应用主要是读操作,那么需要重点考虑如何提高 QPS,来支持高频的读操作。
- TPS,即每秒事务数,用于衡量一个系统每秒处理的事务数。这个指标通常用于写操作,越高说明对写操作的支持越好。我们在设计一个分布式系统的时候,如果应用主要是写操作,那么需要重点考虑如何提高 TPS,来支持高频的写操作。
- BPS,即每秒比特数,用于衡量一个系统每秒处理的数据量。对于一些网络系统、数据管理系统,我们不能简单地按照请求数或事务数来衡量其性能。因为请求与请求、事务与事务之间也存在着很大的差异,比方说,有的事务大需要写入更多的数据。那么在这种情况下,BPS 更能客观地反应系统的吞吐量。
响应时间指的是系统响应一个请求或输入需要花费的时间。响应时间直接影响到用户体验,对于时延敏感的业务非常重要。比如用户搜索导航,特别是用户边开车边搜索的时候,如果响应时间很长,就会直接导致用户走错路。
完成时间指的是系统真正完成一个请求或处理需要花费的时间。任务并行模式出现的其中一个目的,就是缩短整个任务的完成时间。特别是需要计算海量数据或处理大规模任务时,用户对完成时间的感受非常明显。
资源占用(Resource Usage)
资源占用指的是,一个系统提供正常能力需要占用的硬件资源,比如 CPU、内存、硬盘等。
一个系统在没有任何负载时的资源占用,叫做空载资源占用,体现了这个系统自身的资源占用情况。比如,你在手机上安装一个 App,安装的时候通常会提示你需要多少 KB,这就是该 App 的空载硬盘资源占用。对于同样的功能,空载资源占用越少,说明系统设计越优秀,越容易被用户接受。
一个系统满额负载时的资源占用,叫做满载资源占用,体现了这个系统全力运行时占用资源的情况,也体现了系统的处理能力。同样的硬件配置上,运行的业务越多,资源占用越少,说明这个系统设计得越好。
可用性(Availability)
可用性,通常指的是系统在面对各种异常时可以正确提供服务的能力。可用性是分布式系统的一项重要指标,衡量了系统的鲁棒性,是系统容错能力的体现。
系统的可用性可以用系统停止服务的时间与总的时间之比衡量。假设一个网站总的运行时间是 24 小时,在 24 小时内,如果网站故障导致不可用的时间是 4 个小时,那么系统的可用性就是 4/24=0.167,也就是 0.167 的比例不可用,或者说 0.833 的比例可用。
除此之外,系统的可用性还可以用某功能的失败次数与总的请求次数之比来衡量,比如对网站请求 1000 次,其中有 10 次请求失败,那么可用性就是 99%。
你可能经常在一个系统的宣传语中见到或听到 3 个 9(3N,3 Nines)、5 个 9(5N,5 Nines),这些宣传语中所说的 3 个 9、5 个 9,实际上就是系统厂商对可用性的一种标榜,表明该系统可以在 99.9% 或 99.999% 的时间里对外无故障地提供服务。
讲到了可用性,你可能还会想到一个非常近似的术语:可靠性(Reliability),那可靠性和可用性有什么区别呢?
可靠性:表示一个系统完全不出故障的概率,更多地用在硬件领域;
可用性:在允许部分组件失效的情况下,一个系统对外仍能正常提供服务的概率;
杰夫 · 迪恩(Jeff Dean)曾在 Google I/O 大会上透露:谷歌一个基于 1000 台通用计算机的集群,一年之内就有 1000+ 硬盘会出现故障。由于现在比较常见的分布式系统基本上都是基于通用计算机的,这就意味着在这些系统中无法实现真正的可靠,所以我们也会在一些场合见到可靠性和可用性交换使用的情况。
可扩展性(Scalability)
可扩展性,指的是分布式系统通过扩展集群机器规模来提高系统性能 (吞吐、响应时间、 完成时间)、存储容量、计算能力的特性,是分布式系统的特有性质。因为分布式系统的设计初衷,就是利用集群多机的能力处理单机无法解决的问题。然而,完成某一具体任务所需要的机器数目,即集群规模,取决于单个机器的性能和任务的要求。
当任务的需求随着具体业务不断提高时,除了升级系统的性能做垂直 / 纵向扩展外,另一个做法就是通过增加机器的方式去水平 / 横向扩展。
这里垂直 / 纵向扩展指的是,增加单机的硬件能力,比如 CPU 增强、内存增大等;水平 / 横向扩展指的就是,增加计算机数量。好的分布式系统总在追求"线性扩展性",也就是说系统的某一指标可以随着集群中的机器数量呈线性增长。
衡量系统可扩展性的常见指标是加速比(Speedup),也就是一个系统进行扩展后相对扩展前的性能提升。
如果你的扩展目标是为了提高系统吞吐量,则可以用扩展后和扩展前的系统吞吐量之比进行衡量;
如果你的目标是为了缩短完成时间,则可以用扩展前和扩展后的完成时间之比进行衡量;
不同场景下分布式系统的指标
我们都希望自己的分布式系统是高性能、高可用、高扩展和低资源占用的。但出于硬件成本、开发效率等因素的约束,我们无法在性能、可用性、可靠性和资源占用等方面做到面面俱到。因此在不同的业务场景中,我们需要有所取舍。
接下来,我们一起看一下典型的电商、IoT、电信、HPC(高性能计算)、大数据、云计算、区块链等业务或系统对不同指标的诉求。
- 电商系统:对于一个电商系统而言,最重要的是吞吐量,为了处理更多的用户访问或订单业务,牺牲一些硬件成本也是值得的。
- IoT:对于一个 IoT 系统而言,最重要的是资源占用指标,因为在一些功能极简的 IoT 设备上,RAM、ROM 等可用资源通常都是 KB 级的。
- 电信业务:对于电信业务而言,最重要的无疑是响应时间、完成时间,以及可用性。因为你在打电话时不希望你的声音半天才被对方听到,也不希望半天才听到对方的回应,更不希望你的电话无法拨出。
- HPC:HPC 系统最显著的特点是任务执行时间极长,一个天体物理任务的分析和计算通常耗时数周甚至数月。因此,通过水平扩展来提高系统的加速比,是 HPC 系统需要关注的。
- 大数据:大数据任务的处理时间可能相对 HPC 系统来讲比较短,但常见的完成时间也达到了小时级,所以扩展性也是大数据系统首先要考虑的。
- 云计算:对于一个云计算系统而言,常见任务是虚拟主机或容器的创建、资源调整、销毁等操作,如何减少这些操作的完成时间,从而提升用户体验是系统要重点关注的。另外,云计算系统本质上卖的是资源,那么降低系统本身的资源开销,也是系统设计的重中之重。
- 区块链:区块链的吞吐量比较低,比特币的 TPS 只有 7 次每秒,但平均一次交易的确认就需要 10 分钟左右,因此吞吐量和完成时间通常是区块链系统的首要目标。
按照不同维度,分布式系统的指标可以分为性能、资源占用、可用性、可扩展性这四大类。我们自然希望自己的系统,是高性能、高可用、高扩展和低资源占用的,但考虑到硬件成本、开发效率等因素,必须要在设计不同的系统、业务时有所取舍。
所以我们又分析了典型的电商、IoT、电信、HPC(高性能计算)、大数据、云计算、区块链等业务或系统的不同诉求,进而得出了相应的系统需要关注哪些指标。你在设计其他类型的系统时,可以按照这个思路进行取舍。
分布式互斥:有你没我,有我没你
想象一下,你正在一家餐厅使用自助咖啡机泡制咖啡,突然有个人过来挪走了你的杯子,开始泡制他自己的咖啡。你耐着性子等他操作完,继续泡制自己的咖啡。结果你开始没多久,他又回来中断了你泡制咖啡的过程。相信要不了几个回合,你和他就会上演一场"有你没我,有我没你"的格斗了。
这样现实的问题也同样存在于分布式世界,就像我们使用自助咖啡机时不希望被打扰一样,对于同一共享资源,一个程序正在使用的时候也不希望被其他程序打扰。这就要求同一时刻只能有一个程序能够访问这种资源。
在分布式系统里,这种排他性的资源访问方式,叫作分布式互斥(Distributed Mutual Exclusion),而这种被互斥访问的共享资源就叫作临界资源(Critical Resource)。
接下来,我们一起看看如何才能让分布式系统里的程序互斥地访问临界资源。
霸道总裁:集中式算法
对于前面提到的咖啡机问题,我们首先想到的就是,增加一个协调者来约束大家使用自助咖啡机,解决强行插入打断别人的问题。
类似地,我们引入一个协调者程序,得到一个分布式互斥算法。每个程序在需要访问临界资源时,先给协调者发送一个请求。如果当前没有程序使用这个资源,协调者直接授权程序访问;否则,按照先来后到的顺序为请求程序排一个号。如果有程序使用完资源,则通知协调者,协调者从排号的队列里取出排在最前面的请求,并给它发送授权消息。拿到授权消息的程序,可以直接去访问临界资源。
这个互斥算法,就是我们所说的集中式算法,也可以叫做中央服务器算法。之所以这么称呼,是因为协调者代表着集中程序或中央服务器。
如图所示,程序 1、2、3、4 为普通运行程序,另一个程序为协调者。当程序 2、程序 3、程序 4 需要使用临界资源时,它们会向协调者发起申请,请求协调者授权。但不巧的是,程序 1 正在使用临界资源,这时协调者根据程序 2、3、4 的申请时间顺序,依次将它们放入等待队列。
程序 1 使用完临界资源后,通知协调者释放授权。此时协调者从等待队列中取出程序 2,并给它发放授权,然后程序 2 就可以使用临界资源了。
从上述流程可以看出,一个程序完成一次临界资源访问,需要如下几个流程和消息交互:
向协调者发送请求授权信息,1 次消息交互;
协调者向程序发放授权信息,1 次消息交互;
程序使用完临界资源后,向协调者发送释放授权,1 次消息交互;
因此每个程序完成一次临界资源访问,需要进行 3 次消息交互。不难看出,集中式算法的优点在于直观、简单、信息交互量少、易于实现,并且所有程序只需和协调者通信,程序之间无需通信。但是,这个算法的问题也出在了协调者身上。
- 一方面,协调者会成为系统的性能瓶颈。想象一下,如果有 100 个程序要访问临界资源,那么协调者要处理 100*3=300 条消息。也就是说,协调者处理的消息数量会随着需要访问临界资源的程序数量线性增加。
- 另一方面,容易引发单点故障问题。协调者故障,会导致所有的程序均无法访问临界资源,导致整个系统不可用。
因此,在使用集中式算法的时候,一定要选择性能好、可靠性高的服务器来运行协调者。
总结一下:集中式算法具有简单、易于实现的特点,但可用性、性能易受协调者影响。但在可靠性和性能有一定保障的情况下,比如中央服务器计算能力强、性能高、故障率低,或者中央服务器进行了主备备份,主故障后备可以立马升为主,且数据可恢复的情况下,集中式算法可以适用于比较广泛的应用场景。
民主协商:分布式算法
既然引入协调者会带来一些问题,这时你可能就会想,不用协调者是否可以实现对临界资源的互斥访问呢?想象一下,当你需要使用自助咖啡机的时候,是不是可以先去征求其他人的意见呢?在确认其他人都没在使用也暂时不会使用咖啡机时,你就可以放心大胆地去泡制自己的咖啡了。
同理,我们可以把这套算法用于分布式系统。当一个程序要访问临界资源时,先向系统中的其他程序发送一条请求消息,在接收到所有程序返回的同意消息后,才可以访问临界资源。其中,请求消息需要包含所请求的资源、请求者的 ID,以及发起请求的时间。
这就是民主协商法,在分布式领域中,我们称之为分布式算法,或者使用组播和逻辑时钟的算法。
比如程序 1、2、3 需要访问共享资源 A。在时间戳为 8 的时刻,程序 1 想要使用资源 A,于是向程序 2 和 3 发起使用资源 A 的申请,希望得到它们的同意。在时间戳为 12 的时刻,程序 3 想要使用资源 A,于是向程序 1 和 2 发起访问资源 A 的请求。
由于程序 2 暂时不访问资源 A,因此同意了程序 1 和 3 的资源访问请求(请求消息中包含了请求的资源、请求者的 ID、发起请求的事件)。对于程序 3 来说,由于程序 1 提出请求的时间更早,因此同意程序 1 先使用资源。
如图所示,程序 1 接收到其他所有程序的同意消息之后,开始使用资源 A。当程序 1 使用完资源 A 后,释放使用权限,向请求队列中需要使用资源 A 的程序 3 发送同意使用资源的消息,并将程序 3 从请求队列中删除。此时,程序 3 收到了其他所有程序的同意消息,获得了使用资源 A 的权限,开始使用临界资源 A 的旅程。
从上述流程可以看出,一个程序完成一次临界资源的访问,需要进行如下的信息交互:
向其他 n-1 个程序发送访问临界资源的请求,总共需要 n-1 次消息交互;
需要接收到其他 n-1 个程序回复的同意消息,方可访问资源,总共需要 n-1 次消息交互;
因此一个程序要成功访问临界资源,至少需要 2*(n-1) 次消息交互。假设,现在系统中的 n 个程序都要访问临界资源,则会同时产生 2*n*(n-1) 条消息。总结来说,在大型系统中使用分布式算法,消息数量会随着需要访问临界资源的程序数量呈指数级增加,容易导致高昂的沟通成本。
从上述分析不难看出,分布式算法根据"先到先得"以及"投票全票通过"的机制,让每个程序按时间顺序公平地访问资源,简单粗暴、易于实现。但这个算法可用性很低,主要包括两个方面的原因:
- 当系统内需要访问临界资源的程序增多时,容易产生信令风暴,也就是程序收到的请求完全超过了自己的处理能力,而导致自己正常的业务无法开展。
- 一旦某一程序发生故障,无法发送同意消息,那么其他程序均处在等待回复的状态中,使得整个系统处于停滞状态,导致整个系统不可用。所以,相对于集中式算法的协调者故障,分布式算法的可用性更低。
针对可用性低的一种改进办法是,如果检测到一个程序故障,则直接忽略这个程序,无需再等待它的同意消息。这就好比在自助餐厅,一个人离开餐厅了,那你在使用咖啡机前,也无需征得他的同意。但这样的话,每个程序都需要对其他程序进行故障检测,这无疑带来了更大的复杂性。
因此,分布式算法适合节点数目少且变动不频繁的系统,且由于每个程序均需通信交互,因此适合 P2P 结构的系统。比如,运行在局域网中的分布式文件系统,具有 P2P 结构的系统等。
那么在工作中,什么样的场景适合采用分布式算法呢?
Hadoop 是我们非常熟悉的分布式系统,其中的分布式文件系统 HDFS 的文件修改就是一个典型的应用分布式算法的场景。
如下图所示,处于同一个局域网内的节点 1、2、3 中都有同一份文件的备份信息,且它们可以相互通信。这个共享文件,就是临界资源。当节点 1 想要修改共享的文件时,需要进行如下操作:
节点 1 向节点 2、3 发送文件修改请求;
节点 2、3 发现自己不需要使用资源,因此同意节点 1 的请求;
节点 1 收到其他所有计算机的同意消息后,开始修改该文件;
节点 1 修改完成后,向节点 2、3 发送文件修改完成的消息,并发送修改后的文件数据;
节点 2 和 3 收到计算机 1 的新文件数据后,更新本地的备份文件;
归纳一下:分布式算法是一个"先到先得"和"投票全票通过"的公平访问机制,但通信成本较高,可用性也比集中式算法低,适用于临界资源使用频度较低,且系统规模较小的场景。
轮值 CEO:令牌环算法
除了集中式算法、分布式算法以外,还有什么方法可以实现分布式互斥吗?答案是肯定的,毕竟方法总比问题多,华为独创的轮值 CEO 其实就给了我们一个很好的启示。在华为的轮值 CEO 体系里,CEO 就是临界资源,同时只能有一个人担任,由多名高管轮流出任 CEO。
类似地,程序访问临界资源问题也可按照轮值 CEO 的思路实现。 如下图所示,所有程序构成一个环结构,令牌按照顺时针(或逆时针)方向在程序之间传递,收到令牌的程序有权访问临界资源,访问完成后将令牌传送到下一个程序;若该程序不需要访问临界资源,则直接把令牌传送给下一个程序。
在分布式领域,这个算法叫作令牌环算法,也可以叫作基于环的算法。为了便于理解与记忆,你完全可以把这个方法形象地理解为轮值 CEO 法。
因为在使用临界资源前,不需要像分布式算法那样挨个征求其他程序的意见了,所以相对而言,在令牌环算法里单个程序具有更高的通信效率。同时,在一个周期内,每个程序都能访问到临界资源,因此令牌环算法的公平性很好。
但是,不管环中的程序是否想要访问资源,都需要接收并传递令牌,所以也会带来一些无效通信。假设系统中有 100 个程序,那么程序 1 访问完资源后,即使其它 99 个程序不需要访问,也必须要等令牌在其他 99 个程序传递完后,才能重新访问资源,这就降低了系统的实时性。
综上,令牌环算法非常适合通信模式为令牌环方式的分布式系统,例如移动自组织网络系统。一个典型的应用场景就是无人机通信。
无人机在通信时,工作原理类似于对讲机,同一时刻只能发送信息或接收信息。因此,通信中的上行链路(即向外发送信息的通信渠道)是临界资源。
所有的无人机组成一个环,按照顺时针方向通信。每个无人机只知道其前一个发送信息的无人机,和后一个将要接收信息的无人机。拥有令牌的无人机可以向外发送信息,其他无人机只能接收数据。拥有令牌的无人机通信完成后,会将令牌传送给后一个无人机。所有的无人机轮流通信并传输数据,从而消除了多个无人机对通信资源的争夺,使得每个无人机都能接收到其他无人机的信息,降低了通信碰撞导致的丢包率,保证了网络通信的稳定性,提高了多个无人机之间的协作效率。
令牌环算法是一种更加公平的算法,通常会与通信令牌结合,从而取得很好的效果。特别是当系统支持广播或组播通信模式时,该算法更加高效、可行。
对于集中式和分布式算法都存在的单点故障问题,在令牌环中则可以轻松解决。比如某一个程序(例如上图的无人机 2)出现故障,则直接将令牌传递给故障程序的下一个程序(例如,上图中无人机 1 直接将令牌传送给无人机 3),从而很好地解决单点故障问题,提高系统的健壮性,带来更好的可用性。但这要求每个程序都要记住环中的参与者信息,这样才能知道在跳过一个参与者后令牌应该传递给谁。
小结一下:令牌环算法的公平性高,在改进单点故障后,稳定性也很高,适用于系统规模较小,并且系统中每个程序使用临界资源的频率高且使用时间比较短的场景。
补充:有适合大规模系统中的分布式互斥算法吗?
可以看到,上面提到的集中式、分布式和令牌环 3 个互斥算法,都不适用于规模过大、节点数量过多的系统。那么,什么样的互斥算法适用于大规模系统呢?
由于大规模系统的复杂性,我们很自然地想到要用一个相对复杂的互斥算法。时下有一个很流行的互斥算法,两层结构的分布式令牌环算法,把整个广域网系统中的节点组织成两层结构,可以用于节点数量较多的系统,或者是广域网系统。
我们知道,广域网由多个局域网组成,因此在该算法中,局域网是较低的层次,广域网是较高的层次。每个局域网中包含若干个局部进程和一个协调进程。局部进程在逻辑上组成一个环形结构,在每个环形结构上有一个局部令牌 T 在局部进程间传递。局域网与局域网之间通过各自的协调进程进行通信,这些协调进程同样组成一个环结构,这个环就是广域网中的全局环。在这个全局环上,有一个全局令牌在多个协调进程间传递。
本小节我们就剖析了什么是分布式互斥,以及为什么需要分布式互斥。然后介绍了 3 类典型的分布式互斥方法,即:集中式算法、分布式算法,以及令牌环算法,并列举了对应的适用场景,相信你一定可以为你的场景选择一个合适的分布式互斥算法。
分布式选举:国不可一日无君
相信你对集群的概念并不陌生。简单说,集群一般是由两个或两个以上的服务器组建而成,每个服务器都是一个节点。我们经常会听到数据库集群、管理集群等概念,也知道数据库集群提供了读写功能,管理集群提供了管理、故障恢复等功能。
那么对于一个集群来说,多个节点到底是怎么协同,怎么管理的呢。比如数据库集群,如何保证写入的数据在每个节点上都一致呢?也许你会说,这还不简单,选一个"领导"来负责调度和管理其他节点就可以了啊。这个想法一点儿也没错,这个"领导",在分布式中叫做主节点,而选"领导"的过程在分布式领域中叫作分布式选举。
主节点,在一个分布式集群中负责对其他节点进行协调和管理,也就是说,其他节点都必须听从主节点的安排。主节点的存在,就可以保证其他节点有序运行,以及数据库集群中的写入数据在每个节点上的一致性。这里的一致性是指,数据在每个集群节点中都是一样的,不存在不同的情况。
当然,主节点如果主故障了,集群就会天下大乱,就好比一个国家的皇帝驾崩了,国家大乱一样。比如数据库集群中主节点故障后,可能导致每个节点上的数据会不一致。
这就应了那句话:国不可一日无君,对应到分布式系统中就是:集群不可一刻无主。总结来说,选举的作用就是选出一个主节点,由它来协调和管理其他节点,以保证集群有序运行和节点间数据的一致性。
那么如何在集群中选出一个合适的主呢?这是一个技术活儿,目前常见的选主方法有基于序号选举的算法(比如 Bully 算法)、多数派算法(比如 Raft 算法、ZAB 算法)等,下面我们一起来看看这几种算法。
长者为大:Bully 算法
Bully 算法是一种霸道的集群选主算法,为什么说是霸道呢?因为它的选举原则是长者为大,即在所有活着的节点中,选取 ID 最大的节点作为主节点。
在 Bully 算法中,节点的角色有两种:普通节点和主节点。初始化时,所有节点都是平等的,都是普通节点,并且都有成为主的权利。但是选主成功后,有且仅有一个节点成为主节点,其他所有节点都是普通节点。当且仅当主节点故障或与其他节点失去联系后,才会重新选主。
Bully 算法在选举过程中,需要用到以下 3 种消息:
Election 消息,用于发起选举;
Alive 消息,对 Election 消息的应答;
Victory 消息,竞选成功的主节点向其他节点发送的宣誓主权的消息。
Bully 算法选举的原则是“长者为大”,意味着它的假设条件是,集群中每个节点均知道其他节点的 ID。在此前提下,其具体的选举过程如下:
- 集群中每个节点判断自己的 ID 是否为当前活着的节点中 ID 最大的,如果是,则直接向其他节点发送 Victory 消息,宣誓自己的主权;
- 如果自己不是当前活着的节点中 ID 最大的,则向比自己 ID 大的所有节点发送 Election 消息,并等待其他节点的回复;
- 若在给定的时间范围内,本节点没有收到其他节点回复的 Alive 消息,则认为自己成为主节点,并向其他节点发送 Victory 消息,宣誓自己成为主节点;若接收到来自比自己 ID 大的节点的 Alive 消息,则等待其他节点发送 Victory 消息;
- 若本节点收到比自己 ID 小的节点发送的 Election 消息,则回复一个 Alive 消息,告知其他节点,我比你大,重新选举;
目前已经有很多开源软件采用了 Bully 算法进行选主,比如 MongoDB 的副本集故障转移功能。MongoDB 的分布式选举中,采用节点的最后操作时间戳来表示 ID,时间戳最新的节点其 ID 最大,也就是说时间戳最新的、活着的节点是主节点。
所以 Bully 算法的选择特别霸道和简单,谁活着且谁的 ID 最大谁就是主节点,其他节点必须无条件服从。这种算法的优点是,选举速度快、算法复杂度低、简单易实现。
但这种算法的缺点在于,需要每个节点有全局的节点信息,因此额外信息存储较多;其次,任意一个比当前主节点 ID 大的新节点或节点故障后恢复加入集群的时候,都可能会触发重新选举,成为新的主节点。如果该节点频繁退出、加入集群,就会导致频繁切主。
民主投票:Raft 算法
Raft 算法是典型的多数派投票选举算法,其选举机制与我们日常生活中的民主投票机制类似,核心思想是少数服从多数。也就是说,Raft 算法中,获得投票最多的节点成为主。
采用 Raft 算法选举,集群节点的角色有 3 种:
- Leader,即主节点,同一时刻只有一个 Leader,负责协调和管理其他节点;
- Candidate,即候选者,每一个节点都可以成为 Candidate,节点在该角色下才可以被选为新的 Leader;
- Follower,Leader 的跟随者,不可以发起选举;
Raft 选举的流程,可以分为以下几步:
- 初始化时,所有节点均为 Follower 状态。
- 开始选主时,所有节点的状态由 Follower 转化为 Candidate,并向其他节点发送选举请求。
- 其他节点根据接收到的选举请求的先后顺序,回复是否同意成为主。这里需要注意的是,在每一轮选举中,一个节点只能投出一张票。
- 若发起选举请求的节点获得超过一半的投票,则成为主节点,其状态转化为 Leader,其他节点的状态则由 Candidate 降为 Follower。Leader 节点与 Follower 节点之间会定期发送心跳包,以检测主节点是否活着。
- 当 Leader 节点的任期到了,即发现其他服务器开始下一轮选主周期时,Leader 节点的状态由 Leader 降级为 Follower,进入新一轮选主。
关于 Raft 的更多细节,可以参考我的这一篇文章:https://www.cnblogs.com/traditional/p/17439255.html
总之 Raft 算法具有选举速度快、算法复杂度低、易于实现的优点;缺点是,它要求系统内每个节点都可以相互通信,且需要获得过半的投票数才能选主成功,因此通信量大。该算法选举稳定性比 Bully 算法好,这是因为当有新节点加入或节点故障恢复后,会触发选主,但不一定会真正切主,除非新节点或故障后恢复的节点获得投票数过半,才会导致切主。
具有优先级的民主投票:ZAB 算法
ZAB(ZooKeeper Atomic Broadcast)选举算法是为 ZooKeeper 实现分布式协调功能而设计的。相较于 Raft 算法的投票机制,ZAB 算法增加了通过节点 ID 和数据 ID 作为参考进行选主,节点 ID 和数据 ID 越大,表示数据越新,优先成为主。相比较于 Raft 算法,ZAB 算法尽可能保证数据的最新性,所以 ZAB 算法可以说是对 Raft 算法的改进。
使用 ZAB 算法选举时,集群中每个节点拥有 3 种角色:
Leader:主节点;
Follower:跟随者节点;
Observer:观察者节点,无投票权;
选举过程中,集群中的节点拥有 4 个状态:
- Looking 状态:即选举状态,当节点处于该状态时,它会认为当前集群中没有 Leader,因此自己进入选举状态。
- Leading 状态:即领导者状态,表示已经选出主,且当前节点为 Leader。
- Following 状态:即跟随者状态,集群中已经选出主后,其他非主节点状态更新为 Following,表示对 Leader 的追随。
- Observing 状态:即观察者状态,表示当前节点为 Observer,持观望态度,没有投票权和选举权。
投票过程中,每个节点都有一个唯一的三元组 (server_id, server_zxID, epoch),其中 server_id 表示本节点的唯一 ID;server_zxID 表示本节点存放的数据 ID,数据 ID 越大表示数据越新,选举权重越大;epoch 表示当前选取轮数,一般用逻辑时钟表示。
ZAB 选举算法的核心是"少数服从多数,ID 大的节点优先成为主",因此选举过程中通过 (vote_id, vote_zxID) 来表明投票给哪个节点,其中 vote_id 表示被投票节点的 ID,vote_zxID 表示被投票节点的服务器 zxID。ZAB 算法选主的原则是:server_zxID 最大者成为 Leader;若 server_zxID 相同,则 server_id 最大者成为 Leader。
接下来,我们以 3 个节点的集群为例,介绍一下 ZAB 选主的过程。
第一步:当系统刚启动时,3 个节点当前投票均为第一轮投票,即 epoch=1,且 zxID 均为 0。此时每个节点都推选自己,并将选票信息 <epoch, vote_id, vote_zxID> 广播出去。
第二步:根据判断规则,由于 3 个节点的 epoch、zxID 都相同,因此比较 server_id,较大者即为推选对象,因此节点 1 和节点 2 将 vote_id 改为 3,更新自己的投票箱并重新广播自己的投票。
第三步:此时系统内所有节点都推选了节点 3,因此节点 3 当选 Leader,处于 Leading 状态,并向其他节点发送心跳包,维护连接;节点 1 和节点 2 则处于 Following 状态。
总的来说,ZAB 算法性能高,对系统无特殊要求,采用广播方式发送信息。若集群中有 n 个节点,每个节点同时广播,则集群中信息量为 \(n * (n-1)\) 个消息,容易出现广播风暴;且除了投票,还增加了对比节点 ID 和数据 ID,这就意味着还需要知道所有节点的 ID 和数据 ID,所以选举时间相对较长。但该算法选举稳定性比较好,当有新节点加入或节点故障恢复后,会触发选主,但不一定会真正切主,除非新节点或故障后恢复的节点的数据 ID 和节点 ID 最大,且获得投票数过半,才会导致切主。
本节我们就介绍了什么是分布式选举,以及为什么需要分布式选举。然后了解了实现分布式选举的 3 种方法,即:Bully 算法、Raft 算法,以及 ZooKeeper 中的 ZAB 算法,并通过实例展示了各类方法的选举流程。
总的来说,分布式选举问题,是从多个节点中选出一个主节点,而相关的选举方法几乎都有一个共同特点:每个节点都有选举权和被选举权。大部分选举方法采用多数策略,也就是说一个节点只有得到了大部分节点的同意或认可才能成为主节点,然后主节点向其他节点宣告主权。
其实这个选主过程就是一个分布式共识问题,因为每个节点在选出主节点之前都可以认为自己会成为主节点,也就是说集群节点"存异";而通过选举的过程选出主节点,让所有的节点都认可该主节点,就叫"求同"。由此可见,分布式共识的本质就是"存异求同"。
所以从本质上看,分布式选举问题,其实就是传统的分布式共识方法,通过多数投票策略选择一个主节点。
拜占庭将军问题:有叛徒的情况下,如何才能达成共识?
如果使用 Raft、ZAB 之类的共识算法,需要满足一个前提条件,就是节点之间不能造假。对于我们平时在内网搭建的集群来说,节点之间都是可信任的,但如果涉及到区块链就不一定了。什么意思呢?下面我们就通过拜占庭将军问题,来解释一下。
在学习分布式理论的时候,总是会讨论到拜占庭将军问题,很多人觉得难理解。其实,拜占庭将军问题(The Byzantine Generals Problem)是借拜占庭将军的故事展现了分布式共识问题,还探讨和论证了解决的办法。而大多数人觉得它难理解,除了因为分布式共识问题比较复杂之外,还与莱斯利·兰伯特(Leslie Lamport)的讲述方式有关,他在一些细节上(比如口信消息型拜占庭问题之解的算法过程)没有说清楚。
实际上,它是分布式领域最复杂的一个容错模型,一旦搞懂它,你就能掌握分布式共识问题的解决思路,还能更深刻地理解常用的共识算法。在设计分布式系统的时候,也能根据场景特点选择适合的算法,或者设计适合的算法了。那么接下来,我们就以战国时期六国抗秦的故事为主线串联起整篇文章,来彻底读懂、学透。
苏秦的困境
战国时期,齐、楚、燕、韩、赵、魏、秦七雄并立,后来秦国的势力不断强大起来,成了东方六国的共同威胁。于是这六个国家决定联合,全力抗秦,免得被秦国各个击破。一天,苏秦作为合纵长,挂六国相印,带着六国的军队叩关函谷,驻军在了秦国边境,为围攻秦国作准备。但是,因为各国军队分别驻扎在秦国边境的不同地方,所以军队之间只能通过信使互相联系,这时苏秦就面临了一个很严峻的问题:如何统一大家的作战计划?
万一有些诸侯国在暗通秦国,发送误导性的作战信息怎么办?如果信使被敌人截杀,甚至被敌人间谍替换,又该怎么办?这些都会导致自己的作战计划被扰乱,然后出现有的诸侯国在进攻,有的诸侯国在撤退的情况。而这时,秦国一定会趁机出兵,把他们逐一击破。
所以,如何达成共识,制定统一的作战计划呢?苏秦他很愁。
这个故事,是拜占庭将军问题的一个简化表述,苏秦面临的就是典型的共识难题,也就是如何在可能有误导信息的情况下,采用合适的通讯机制,让多个将军达成共识,制定一致性的作战计划?你可以先停下来想想,这个问题难在哪儿?我们又是否有办法,帮助诸侯国们达成共识呢?
二忠一叛的难题
为了便于你理解和层层深入,我先假设只有 3 个国家要攻打秦国,这三个国家的三位将军,咱们简单点儿,分别叫齐、楚、燕。同时,又因为秦国很强大,所以只有半数以上的将军参与进攻,才能击败敌人。在这个期间,将军们彼此之间需要通过信使传递消息,然后协商一致之后,才能在同一时间点发动进攻。
举个例子,有一天,这三位将军各自一脸严肃地讨论明天是进攻还是撤退,并让信使传递信息,按照少数服从多数的原则投票表决,两个人意见一致就可以了,比如:
齐根据侦查情况决定撤退;
楚和燕根据侦查信息,决定进攻;
那么按照原则,齐也会进攻。最终,3 支军队同时进攻,大败秦军。
可是问题来了: 一旦有人在暗通秦国,就会出现作战计划不一致的情况。比如齐向楚、燕分别发送了「撤退」的消息,燕向齐和楚发送了「进攻」的消息。撤退和进攻是一比一,无论楚投进攻还是撤退,都会成为二比一,这个时候还是会形成一个一致性的作战方案。
但是楚当了叛徒,在暗中配合秦国,让信使向齐发送了「撤退」,向燕发送了「进攻」,那么:
燕看到的是,撤退: 进攻 = 1: 2
齐看到的是,撤退: 进攻 = 2: 1
按照「少数服从多数」的原则,就会出现燕单独进攻秦军,当然,最后肯定是因为寡不敌众,被秦军给灭了。
在这里你可以看到,叛将楚通过发送误导信息,非常轻松地干扰了齐和燕的作战计划,导致这两位忠诚将军被秦军逐一击败,这就是所说的二忠一叛难题。 那么苏秦应该怎么解决这个问题呢?我们来帮苏秦出出主意。
解决办法一:口信消息型拜占庭问题之解
先来说说第一个解决办法。首先,三位将军都分拨一部分军队,由苏秦率领,苏秦参与作战计划讨论并执行作战指令。这样,3 位将军的作战讨论,就变为了 4 位将军的作战讨论,这能够增加讨论中忠诚将军的数量。
然后呢,4 位将军还约定了,如果没有收到命令,就执行预设的默认命令,比如「撤退」。除此之外,还约定一些流程来发送作战信息、执行作战指令,比如,进行两轮作战信息协商。为什么要执行两轮呢?先卖个关子,你一会儿就知道了。
第一轮:
先发送作战信息的将军作为指挥官,其他的将军作为副官;
指挥官将他的作战信息发送给每位副官;
每位副官,将从指挥官处收到的作战信息,作为他的作战指令;如果没有收到作战信息,将把默认的「撤退」作为作战指令;
第二轮:
除了第一轮的指挥官外,剩余的 3 位将军将分别作为指挥官,向另外 2 位将军发送作战信息;
然后,这 3 位将军按照「少数服从多数」,执行收到的作战指令;
为了更直观地理解苏秦的整个解决方案,我们来演示一下作战信息协商过程。而且我们会分别以忠诚将军和叛将先发送作战信息为例来演示, 这样可以完整地演示叛将对作战计划干扰破坏的可能性。
为了演示方便,假设苏秦先发起作战信息(苏秦不会叛变,否则没法玩了),作战指令是「进攻」。那么在第一轮作战信息协商中,苏秦向齐、楚、燕发送作战指令「进攻」。
在第二轮作战信息协商中,齐、楚、燕分别作为指挥官,向另外 2 位发送作战信息「进攻」。但因为楚已经叛变了,所以,为了干扰作战计划,他就对着干,发送「撤退」作战指令。
最终,齐和燕收到的作战信息都是「进攻、进攻、撤退」,按照原则,将军们一起执行作战指令「进攻」,实现了作战计划的一致性,保证了作战的胜利。
那么,如果是叛徒楚先发送作战信息,干扰作战计划,结果会有所不同么?我们来具体看一看。在第一轮作战信息协商中,楚向苏秦发送作战指令「进攻」,向齐、燕发送作战指令「撤退」。
然后,在第二轮作战信息协商中,苏秦、齐、燕分别作为指挥官,向另外两位发送作战信息。
最终,苏秦、齐和燕收到的作战信息都是「撤退、撤退、进攻」,按照原则,将军们一起执行作战指令「撤退」,实现了作战计划的一致性。也就是说,无论叛将楚如何捣乱,苏秦、齐和燕,都执行一致的作战计划,保证作战的胜利。
这个解决办法,其实是兰伯特在论文《The Byzantine Generals Problem》中提到的口信消息型拜占庭问题之解:如果叛将人数为 m,将军人数不能少于 3m + 1 ,那么拜占庭将军问题就能解决了。 不过,作者在论文中没有讲清楚一些细节,下面我们补充一下。
这个算法有个前提,就是叛将人数 m,或者说能容忍的叛将数 m,是已知的。在这个算法中,叛将数 m 决定递归循环的次数(也就是说,叛将数 m 决定将军们要进行多少轮作战信息协商),即 m+1 轮(这里只有楚是叛变的,那么就进行了两轮)。你也可以从另外一个角度理解:n 位将军,最多能容忍 (n - 1) / 3 位叛将。关于这个公式,我们只需要记住就好了。
不过,这个算法虽然能解决拜占庭将军问题,但它有一个限制:如果叛将人数为 m,那么将军总人数必须不小于 3m + 1。在二忠一叛的问题中,在存在 1 位叛将的情况下,必须增加 1 位将军,将 3 位将军协商共识,转换为 4 位将军协商共识,这样才能实现忠诚将军的一致性作战计划。那么有没有办法,在不增加将军人数的时候,直接解决二忠一叛的难题呢?
解决办法二:签名消息型拜占庭问题之解
其实,苏秦还可以通过签名的方式,在不增加将军人数的情况下,解决二忠一叛的难题。首先,苏秦要通过印章、虎符等信物,实现这样几个特性:
忠诚将军的签名无法伪造,而且对他签名消息的内容进行任何更改都会被发现;
任何人都能验证将军签名的真伪;
这时,如果忠诚的将军,比如齐先发起作战信息协商,一旦叛将楚修改或伪造收到的作战信息,那么燕在接收到楚的作战信息的时候,会发现齐的作战信息被修改,楚已叛变,这时他执行齐发送的作战信息。
如果叛变将军楚先发送误导的作战信息,那么齐和燕将发现楚发送的作战信息是不一致的,知道楚已经叛变。这个时候,他们可以先处理叛将,然后再重新协商作战计划。
燕向齐表示,自己和楚的作战信息是「进攻」,但是齐收到楚的作战信息是「撤退」,于是齐便知道楚叛变了;同理,齐向燕表示,自己和楚的作战信息是撤退,但是燕收到楚的作战信息是「进攻」,于是燕也知道楚叛变了。
这个解决办法,是兰伯特在论文中提到的签名消息型拜占庭问题之解。而通过签名机制约束叛将的叛变行为,任何叛变行为都会被发现,也就会实现无论有多少忠诚的将军和多少叛将,忠诚的将军们总能达成一致的作战计划。如果当时苏秦能够具备分布式系统设计的思维,掌握这几种算法,应该就不用担心作战计划被干扰了吧。
为了理解拜占庭将军问题,我们用苏秦协商作战的故事为例,现在让我们跳回现实世界,回到计算机世界的分布式场景中:
故事里的各位将军,你可以理解为计算机节点;
忠诚的将军,你可以理解为正常运行的计算机节点;
叛变的将军,你可以理解为出现故障并会发送误导信息的计算机节点;
信使被杀,可以理解为通讯故障、信息丢失;
信使被间谍替换,可以理解为通讯被中间人攻击,攻击者在恶意伪造信息和劫持通讯;
这样一来,你是不是就理解了计算机分布式场景中面临的问题,并且知道了解决的办法呢?
那么我想强调的是,拜占庭将军问题描述的是最困难的、也是最复杂的一种分布式故障场景,除了存在故障行为,还存在恶意行为的一个场景。你要注意,在存在恶意节点行为的场景中(比如在数字货币的区块链技术中),必须使用拜占庭容错算法(Byzantine Fault Tolerance,BFT)。除了故事中提到两种算法,常用的拜占庭容错算法还有:PBFT 算法,PoW 算法。
而在计算机分布式系统中,最常用的是非拜占庭容错算法,即故障容错算法(Crash Fault Tolerance,CFT)。CFT 解决的是分布式的系统中存在故障,但不存在恶意节点的场景下的共识问题。 也就是说,这个场景可能会丢失消息,或者有消息重复,但不存在错误消息,或者伪造消息的情况。常见的算法有 Paxos 算法、Raft 算法、ZAB 算法。
那么,如何在实际场景选择合适的算法类型呢?答案是:如果能确定该环境中各节点是可信赖的,不存在篡改消息或者伪造消息等恶意行为(例如 DevOps 环境中的分布式路由寻址系统),推荐使用非拜占庭容错算法;反之,推荐使用拜占庭容错算法,例如在区块链中使用 PoW 算法。
分布式事务:All or nothing
对于网上购物的每一笔订单来说,电商平台一般都会有两个核心步骤:一是订单业务执行下单操作,二是库存业务执行减库存操作。
通常这两个业务会运行在不同的机器上,甚至是运行在不同区域的机器上。针对同一笔订单,当且仅当订单操作和减库存操作一致时,才能保证交易的正确性。也就是说一笔订单,只有这两个操作都完成,才能算做处理成功,否则处理失败,充分体现了 All or nothing 的思想。
在分布式领域中,这个问题就是分布式事务问题,那么这一小节我们就一起打卡分布式事务吧。
什么是分布式事务?
要想理解分布式事务,我们首先来看一下什么是事务。事务,其实是包含一系列操作的、一个有边界的工作序列,有明确的开始和结束标志,且要么被完全执行,要么完全失败,即 all or nothing。通常情况下,我们所说的事务指的都是本地事务,也就是在单机上的事务。
而分布式事务,就是在分布式系统中运行的事务,由多个本地事务组合而成。在分布式场景下,对事务的处理操作可能来自不同的机器,甚至是来自不同的操作系统。上面提到的电商处理订单问题,就是典型的分布式事务。
要深入理解分布式事务,我们首先需要了解它的特征。分布式事务是多个事务的组合,那么事务的特征 ACID,也是分布式事务的基本特征。其中 ACID 具体含义如下:
- 原子性(Atomicity),即事务最终的状态只有两种,全部执行成功和全部不执行。若处理事务的任何一项操作不成功,就会导致整个事务失败。一旦操作失败,所有操作都会被取消(即回滚),使得事务仿佛没有被执行过一样。
- 一致性(Consistency),是指事务操作前和操作后,数据的完整性保持一致或满足完整性约束。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元。用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和给 B 的账户增加 200 元 ; 一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况。
- 隔离性(Isolation),是指当系统内有多个事务并发执行时,多个事务不会相互干扰,即一个事务内部的操作及使用的数据,对其他并发事务是隔离的。
- 持久性(Durability),是指一个事务完成了,那么它对数据库所做的更新就被永久保存下来了。即使发生系统崩溃或宕机等故障,只要数据库能够重新被访问,那么一定能够将其恢复到事务完成时的状态。
分布式事务基本能够满足 ACID,其中的 C 是强一致性,也就是所有操作均执行成功,才提交最终结果,以保证数据一致性或完整性。但随着分布式系统规模不断扩大,复杂度急剧上升,达成强一致性所需时间周期较长,限定了复杂业务的处理。为了适应复杂业务,出现了 BASE 理论,该理论的一个关键点就是采用最终一致性代替强一致性。
介绍完什么是分布式事务,以及事务的基本特征后,就进入"怎么做"的阶段啦。所以接下来,我们就看看如何实现分布式事务吧。
实际上,分布式事务主要是解决在分布式环境下,组合事务的一致性问题。实现分布式事务有以下 3 种基本方法:
基于 XA 协议的二阶段提交协议方法;
三阶段提交协议方法;
基于消息的最终一致性方法。
其中,基于 XA 协议的二阶段提交协议方法和三阶段提交协议方法,采用了强一致性,遵从 ACID。基于消息的最终一致性方法,采用了最终一致性,遵从 BASE 理论。
基于 XA 协议的二阶段提交方法
XA 是一个分布式事务协议,规定了事务管理器和资源管理器接口。因此 XA 协议可以分为两部分,即事务管理器和本地资源管理器。
XA 实现分布式事务的原理,就类似于前面介绍的集中式算法:事务管理器作为协调者,负责各个本地资源的提交和回滚;而资源管理器就是分布式事务的参与者,通常由数据库实现,比如 Oracle、DB2 等商业数据库都实现了 XA 接口。
基于 XA 协议的二阶段提交方法中,二阶段提交协议(The two-phase commit protocol,2PC),用于保证分布式系统中事务提交时的数据一致性,是 XA 在全局事务中用于协调多个资源的机制。
那么,两阶段提交协议如何保证分布在不同节点上的分布式事务的一致性呢?为了保证它们的一致性,我们需要引入一个协调者来管理所有的节点,并确保这些节点正确提交操作结果,若提交失败则放弃事务。接下来,我们看看两阶段提交协议的具体过程。
两阶段提交协议的执行过程,分为投票(voting)和提交(commit)两个阶段。
投票为第一阶段,协调者(Coordinator,即事务管理器)会向事务的参与者(Cohort,即本地资源管理器)发起执行操作的 CanCommit 请求,并等待参与者的响应。参与者接收到请求后,会执行请求中的事务操作,记录日志信息但不提交,待参与者执行成功,则向协调者发送 Yes 消息,表示同意操作;若不成功,则发送 No 消息,表示终止操作。
当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了提交阶段。在提交阶段,协调者会根据所有参与者返回的信息向参与者发送 DoCommit 或 DoAbort 指令:
- 若协调者收到的都是 Yes 消息,则向参与者发送 DoCommit 消息,参与者会完成剩余的操作并释放资源,然后向协调者返回 HaveCommitted 消息;
- 如果协调者收到的消息中包含 No 消息,则向所有参与者发送 DoAbort 消息,此时发送 Yes 的参与者则会根据之前执行操作时的回滚日志对操作进行回滚,然后所有参与者会向协调者发送 HaveCommitted 消息;
- 协调者接收到 HaveCommitted 消息,就意味着整个事务结束了;
所以二阶段提交的算法思路可以概括为:协调者下发请求事务操作,参与者将操作结果通知协调者,协调者根据所有参与者的反馈结果决定各参与者是要提交操作还是撤销操作。
虽然基于 XA 的二阶段提交算法基本满足了事务的 ACID 特性,但依然有些不足。
- 同步阻塞问题:二阶段提交算法在执行过程中,所有参与节点都是事务阻塞型的。也就是说,当本地资源管理器占有临界资源时,其他资源管理器如果要访问同一临界资源,会处于阻塞状态。
- 单点故障问题:基于 XA 的二阶段提交算法类似于集中式算法,一旦事务管理器发生故障,整个系统都处于停滞状态。尤其是在提交阶段,一旦事务管理器发生故障,资源管理器会由于等待管理器的消息,而一直锁定事务资源,导致整个系统被阻塞。
- 数据不一致问题:在提交阶段,当协调者向参与者发送 DoCommit 请求之后,如果发生了局部网络异常,或者在发送提交请求的过程中协调者发生了故障,就会导致只有一部分参与者接收到了提交请求并执行提交操作,但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。
三阶段提交方法
三阶段提交协议(Three-phase commit protocol,3PC),是对二阶段提交(2PC)的改进。为了解决两阶段提交的同步阻塞和数据不一致问题,三阶段提交引入了超时机制和准备阶段。
- 同时在协调者和参与者中引入超时机制。如果协调者或参与者在规定的时间内没有接收到来自其他节点的响应,就会根据当前的状态选择提交或者终止整个事务。
- 在第一阶段和第二阶段中间引入了一个准备阶段,也就是在提交阶段之前,加入了一个预提交阶段。在预提交阶段排除一些不一致的情况,保证在最后提交之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC 把 2PC 的提交阶段一分为二,这样三阶段提交协议就有 CanCommit、PreCommit、DoCommit 三个阶段。
1)CanCommit 阶段
CanCommit 阶段与 2PC 的投票阶段类似:协调者向参与者发送请求操作(CanCommit 请求),询问参与者是否可以执行事务提交操作,然后等待参与者的响应;参与者收到 CanCommit 请求之后,回复 Yes,表示可以顺利执行事务;否则回复 No。
2)PreCommit 阶段
协调者根据参与者的回复情况,来决定是否可以进行 PreCommit 操作。
- 如果所有参与者回复的都是 Yes,那么协调者就会执行事务的预执行:
- 发送预提交请求:协调者向参与者发送 PreCommit 请求,进入预提交阶段。
- 事务预提交:参与者接收到 PreCommit 请求后执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中。
- 响应反馈:如果参与者成功执行了事务操作,则返回 ACK 响应,同时开始等待最终指令。
- 假如任何一个参与者向协调者发送了 No 消息,或者等待超时之后,协调者都没有收到参与者的响应,就执行中断事务的操作:
- 发送中断请求:协调者向所有参与者发送 Abort 消息。
- 终断事务:参与者收到 Abort 消息之后,或超时后仍未收到协调者的消息,执行事务的终断操作。
3)DoCommit 阶段
DoCmmit 阶段进行真正的事务提交,根据 PreCommit 阶段协调者发送的消息,进入执行提交阶段或事务中断阶段。
- 执行提交阶段:
- 发送提交请求:协调者接收到所有参与者发送的 Ack 响应,从预提交状态进入到提交状态,并向所有参与者发送 DoCommit 消息。
- 事务提交:参与者接收到 DoCommit 消息之后,正式提交事务。完成事务提交之后,释放所有锁住的资源。
- 响应反馈:参与者提交完事务之后,向协调者发送 Ack 响应。
- 完成事务:协调者接收到所有参与者的 Ack 响应之后,完成事务。
- 事务中断阶段:
- 发送中断请求:协调者向所有参与者发送 Abort 请求。
- 事务回滚:参与者接收到 Abort 消息之后,利用其在 PreCommit 阶段记录的 Undo 信息执行事务的回滚操作,并释放所有锁住的资源。
- 反馈结果:参与者完成事务回滚之后,向协调者发送 Ack 消息。
- 终断事务:协调者接收到参与者反馈的 Ack 消息之后,执行事务的中断,并结束事务。
在 DoCommit 阶段,当参与者向协调者发送 Ack 消息后,如果长时间没有得到协调者的响应,在默认情况下,参与者会自动将超时的事务进行提交,不会像两阶段提交那样被阻塞住。
基于分布式消息的最终一致性方案
2PC 和 3PC 这两种方法,有两个共同的缺点,一是都需要锁定资源,降低系统性能;二是,没有解决数据不一致的问题。因此,便有了通过分布式消息来确保事务最终一致性的方案。
在 eBay 的分布式系统架构中,架构师解决一致性问题的核心思想就是:将需要分布式处理的事务通过消息或者日志的方式异步执行,消息或日志可以存到本地文件、数据库或消息队列中,再通过业务规则进行失败重试。这个案例,就是使用基于分布式消息的最终一致性方案,解决了分布式事务的问题。
基于分布式消息的最终一致性方案的事务处理,引入了一个消息中间件(Message Queue,MQ),用于在多个应用之间进行消息传递。基于消息中间件协商多个节点分布式事务执行操作的示意图,如下所示。
以网上购物为例,假设用户 A 在某电商平台下了一个订单,需要支付 50 元,发现自己的账户余额共 150 元,就使用余额支付,支付成功之后,订单状态修改为支付成功,然后通知仓库发货。
在该事件中,涉及到了订单系统、支付系统、仓库系统,这三个系统是相互独立的应用,通过远程服务进行调用。
根据基于分布式消息的最终一致性方案,用户 A 通过终端手机首先在订单系统上操作,然后整个购物的流程如下所示。
- 1)订单系统把订单消息发给消息中间件,消息状态标记为待确认。
- 2)消息中间件收到消息后,进行消息持久化操作,即在消息存储系统中新增一条状态为待发送的消息。
- 3)消息中间件返回消息持久化结果(成功 / 失败),订单系统根据返回结果判断如何进行业务操作。失败,放弃订单,结束(必要时向上层返回失败结果);成功,则创建订单。
- 4)订单操作完成后,把操作结果(成功 / 失败)发送给消息中间件。
- 5)消息中间件收到业务操作结果后,根据结果进行处理:失败,删除消息存储中的消息,结束;成功,则更新消息存储中的消息状态为待发送(可发送),并执行消息投递。
- 6)如果消息状态为可发送,则 MQ 会将消息发送给支付系统,表示已经创建好订单,需要对订单进行支付。支付系统也按照上述方式进行订单支付操作。
- 7)订单系统支付完成后,会将支付消息返回给消息中间件,中间件将消息传送给订单系统。订单系统再调用库存系统,进行出货操作。
可以看出,分布式事务中,当且仅当所有的事务均成功时整个流程才成功。所以分布式事务的一致性是实现分布式事务的关键问题,目前来看还没有一种很简单、完美的方案可以应对所有场景。
然后用一张表格总结一下这几种方法:
知识扩展:刚性事务与柔性事务
在讨论事务的时候,我们经常会听到刚性事务与柔性事务,但却很难区分这两种事务。那么就来说说什么是刚性事务、柔性事务,以及两者之间有何区别?
刚性事务,遵循 ACID 原则,具有强一致性。比如,数据库事务。
柔性事务,其实就是根据不同的业务场景使用不同的方法实现最终一致性,也就是说我们可以根据业务的特性做部分取舍,容忍一定时间内的数据不一致。
总结来讲,与刚性事务不同,柔性事务允许一定时间内不同节点的数据不一致,但要求最终一致。而柔性事务的最终一致性,遵循的是 BASE 理论。
BASE 理论包括基本可用(Basically Available)、柔性状态(Soft State)和最终一致性(Eventual Consistency)。
- 基本可用:分布式系统出现故障的时候,允许损失一部分功能的可用性。比如,某些电商 618 大促的时候,会对一些非核心链路的功能进行降级处理。
- 柔性状态:在柔性事务中,允许系统存在中间状态,且这个中间状态不会影响系统整体可用性。比如,数据库读写分离,写库同步到读库(主库同步到从库)会有一个延时,其实就是一种柔性状态。
- 最终一致性:事务在操作过程中可能会由于同步延迟等问题导致不一致,但最终状态下,数据都是一致的。
可见 BASE 理论为了支持大型分布式系统,通过牺牲强一致性,保证最终一致性,来获得高可用性,是对 ACID 原则的弱化。具体到今天的三种分布式事务实现方式,二阶段提交、三阶段提交方法,遵循的是 ACID 原则,而消息最终一致性方案遵循的就是 BASE 理论。
以上我们就从事务的 ACID 特性出发,介绍了分布式事务的概念、特征,以及如何实现分布式事务。而实现分布式事务有三种实现方式:基于 XA 协议的二阶段提交方法,三阶段方法以及基于分布式消息的最终一致性方法。
二阶段和三阶段方法是维护强一致性的算法,它们针对刚性事务,实现的是事务的 ACID 特性。而基于分布式消息的最终一致性方案更适用于大规模分布式系统,它维护的是事务的最终一致性,遵循的是 BASE 理论,因此适用于柔性事务。
在分布式系统的设计与实现中,分布式事务是不可或缺的一部分。可以说,没有实现分布式事务的分布式系统,不是一个完整的分布式系统。分布式事务的实现过程看似复杂,但将方法分解剖析后,你就会发现分布式事务的实现是有章可循的。
分布式锁:关键重地,非请勿入
关于分布式锁,应该是一个老生常谈的话题了,可以参考我的另一篇文章,这里就不赘述了。
分布式体系结构之集中式结构:一人在上,万人在下
云这个话题对我们来说已经非常熟悉了。可以说,云在我们的生活中无处不在,比如平时看的视频通常就是放在云上的。当我们要播放一段视频时,请求会先转发到云上,从云上下载数据到本地,然后播放。在这里你肯定会疑惑,云上资源那么丰富吗,可以存放这么多东西吗?
云上的资源确实丰富,因为它可以尽可能地把更多的服务器组织起来,作为一个统一的资源,为多个用户提供服务。这里的重点是,把多个服务器管理起来,作为一个统一的资源提供服务。而如何组织,就是分布式体系结构的范畴了。
你会发现,很多场景下,我们的请求都会汇总到一台服务器上,由这台服务器统一协调我们的请求和其他服务器之间的关系。这种由一台服务器统一管理其他服务器的方式,就是分布式体系结构中的集中式结构(也称为 Master/Slave 架构),其中统一管理其他服务器的服务器是主,其他服务器是从,可以形象地比喻为"一人在上,万人在下"。
什么是集中式结构?
集中式结构就是,由一台或多台服务器组成中央服务器,系统内的所有数据都存储在中央服务器中,系统内所有的业务也均先由中央服务器处理。多个节点服务器与中央服务器连接,并将自己的信息汇报给中央服务器,由中央服务器统一进行资源和任务调度:中央服务器根据这些信息,将任务下达给节点服务器;节点服务器执行任务,并将结果反馈给中央服务器。
集中式结构最大的特点,就是部署结构简单。这是因为,集中式系统的中央服务器往往是多个具有较强计算能力和存储能力的计算机,为此中央服务器进行统一管理和调度任务时,无需考虑对任务的多节点部署,而节点服务器之间无需通信和协作,只要与中央服务器通信协作即可,具体示意图如下所示:
现在我们理解了什么是集中式结构,为了加深理解,接下来我们以 Google Borg、Kubernetes 和 Apache Mesos 三个经典的集群管理系统为例,深入学习集中式结构的原理。
Google Borg
Borg 是 Google 内部使用的集群管理系统,采用了典型的集中式结构,负责提交、调度、开始、重启和管理 Google 运行在其上的所有应用。在 Borg 中,一个集群称为一个 Cell,每个 Cell 里面有一个 Leader,称为 BorgMaster,即为中央服务器;其他服务器为节点服务器或从服务器,被称为 Borglet。
首先看一下 BorgMaster,它由两个进程组成,一个是 Borgmaster 主进程,一个是独立的 scheduler 进程:
- 主进程处理客户端的 RPC 请求,比如任务的执行状态更新或者查询等,同时管理系统中所有实体的状态(比如服务器、任务等),并负责和 Borglet 通信。
- scheduler 进程负责任务调度,通过任务对资源的需求以及当前 Borglet 所在服务器的资源情况进行匹配,为任务寻找一个合适的节点服务器执行。
接下来,我们一起看看 Borglet。它是运行在每个节点机器上的一个 agent,负责任务的拉起、停止、重启等,并管理和搜集本服务器资源,将任务的状态、服务器状态等信息上报给 BorgMaster。而 BorgMaster 会周期性地轮询每个 Borglet,以获取节点服务器的状态和资源信息等。
Borg 的整体架构示意图如下所示:
Borg 的主要用户是 Google 的开发者以及运行 Google 应用和服务的系统管理员(网站可靠性工程师,简称 SRE)。用户以 Job 的形式向 Borg 提交工作,每个 Job 由一个或多个运行相同程序的 Task 组成。每个 Job 运行在一个 Borg Cell 中,并将一组机器当做一个单元进行管理。
Borg 可以运行各种各样的任务,这些任务主要分为两类:
- 第一类是长服务,即长时间运行不停止的服务,并且要求能够处理短暂的、延迟敏感的请求(延迟要求在几微秒到几百毫秒之间)。这些服务主要用于面向终端用户的服务(比如 Gmail、Google Docs、Web 搜索),以及内部的一些基础设施服务(比如 BigTable)。
- 第二类是批处理任务,通常需要几秒到几天的时间来完成的批处理 Job,这些 Job 对短时间的性能波动并不是非常敏感。
这些负载通常在 Cell 之间混合分布,每个 Cell 随着主要租户以及时间的不同会运行各种不同的应用:批处理类型的 Job 来了又走,而许多面向终端用户的 Job 又期望一个能长时间使用的模式。
对于这些不同的服务,要求 Borg 能很好地处理所有的情况。Borg 主要有三大优点:
开发者只需关注应用,不需要关注底层资源管理。它隐藏了资源管理以及错误处理,因此用户能集中精力开发应用。
高可靠性和可用性,支持多种应用。
支持上千级服务器的管理和运行。
Borg 并不是第一个解决这些问题的系统,但却是少数能在这么大规模处理这些问题的同时,还能实现这样的弹性和完整性的系统之一。
Kubernetes
Kubernetes 是 Google 开源的容器集群管理系统,是 Borg 的一个开源版本,用于自动部署、扩展和管理容器化应用程序。其核心是,在集群的节点上运行容器化应用,可以进行自动化容器操作,包括部署、调度和在节点间弹性伸缩等。
Kubernetes 也是典型的集中式结构,一个 Kubernetes 集群,主要由 Master 节点和 Worker 节点组成,以及客户端命令行工具 kubectl 和其他附加项。
我们先来看看 Master 节点,它运行在中心服务器上,Master 节点由 API Server、Scheduler、Cluster State Store 和 Control Manger Server 组成,负责对集群进行调度管理。
API Server:是所有 REST 命令的入口,负责处理 REST 的操作,确保它们生效,并执行相关业务逻辑。
Scheduler:根据容器需要的资源以及当前 Worker 所在的节点服务器的资源信息,自动为容器选择合适的节点服务器。
Cluster State Store:集群状态存储,默认采用 etcd,etcd 是一个分布式 key-value 存储,主要用来做共享配置和服务发现。
Control Manager:用于执行大部分的集群层次的功能,比如执行生命周期功能(命名空间创建和生命周期、事件垃圾收集、已终止垃圾收集、级联删除垃圾收集等)和 API 业务逻辑。
接下来是 Worker 节点,它作为真正的工作节点,运行在从节点服务器,包括 kubelet 和 kube-proxy 核心组件,负责运行业务应用的容器。
- kubelet:用于通过命令行与 API Server 进行交互,根据接收到的请求对 Worker 节点进行操作。也就是说,通过与 API Server 进行通信,接收 Master 节点根据调度策略发出的请求或命令,在 Worker 节点上管控容器(Pod),并管控容器的运行状态(比如,重新启动出现故障的 Pod)等。Pod 是 Kubernetes 的最小工作单元,每个 Pod 包含一个或多个容器。
- kube-proxy:负责为容器(Pod)创建网络代理 / 负载平衡服务,从 API Server 获取所有 Server 信息,并根据 Server 信息创建代理服务,这种代理服务称之为 Service。Kube-proxy 主要负责管理 Service 的访问入口,即实现集群内的 Pod 客户端访问 Service,或者是集群外访问 Service。具有相同服务的一组 Pod 可抽象为一个 Service,每个 Service 都有一个虚拟 IP 地址(VIP)和端口号供客户端访问。
Kubernetes 架构示意图如下所示:
图中, Kube DNS 负责为整个集群提供 DNS 服务;CNI 是 Container Network Interface 的一个标准的通用接口,用于连接容器管理系统和网络插件。
与 Borg 不同的是,Kubernetes 主要是一个容器编排引擎,不仅支持 Docker,还支持 Rocket(另一种容器技术)。Kubernetes 也已经被很多公司采用,比如网易云、华为在需要使用容器进行资源隔离以运行相关业务的场景下,采用了大规模 Kubernetes 集群。
在容器管理方面,Kubernetes 有很多优势。
- 自动化容器的部署和复制:Kubernetes 执行容器编排,因此不必人工编写这些任务的脚本。
- 将容器组织为组,弹性伸缩:Kubernetes 引入 Pod 机制,Pod 代表着能够作为单一应用程序加以控制的一组容器集合。通过 Pod 机制,Kubernetes 实现了多个容器的协作,能够有效避免将太多功能集中到单一容器镜像这样的错误实践中。此外,软件可以向外扩展跨越多个 Pods 实现初步部署,且相关部署可随时进行规模伸缩。
- 容器间负载均衡:Services 用于将具备类似功能的多个 Pod 整合为一组,可轻松进行配置以实现其可发现性、可观察性、横向扩展以及负载均衡。
- 易于版本控制与滚动更新:Kubernetes 采取"滚动方式"实现编排,且可跨越部署范围内的全部 Pod。这些滚动更新可进行编排,并以预定义方式配合当前可能尚不可用的 Pods 数量,以及暂时存在的闲置 Pods 数量。Kubernetes 利用新的应用程序镜像版本对已部署 Pods 进行更新,并在发现当前版本存在不稳定问题时回滚至早期部署版本。
Mesos
理解了 Google Borg 和 Kubernetes 的集中式结构,接下来我们再看看 Apache 旗下的开源分布式资源管理框架 Mesos 吧。它被称为是分布式系统的内核,最初由加州大学伯克利分校的 AMPLab 开发,后在 Twitter 得到广泛使用。
Mesos 的开发受到了 Borg 系统的启发,也是采用的典型的集中式架构。Mesos 与 Borg 不同之处在于,Borg 的 Master 直接对接用户应用,也就是说用户可以向 Borg 的 Master 直接请求任务。但 Mesos 不可以,Mesos 只负责底层资源的管理和分配,并不涉及存储、 任务调度等功能,因此 Mesos Master 对接的是 Spark、Hadoop、Marathon 等框架,用户的任务需要提交到这些框架上。也正因为此,Mesos 的任务调度框架是双层结构。
在 Mesos 中,一个集群包括 Mesos Master 和多个 Mesos Agent。其中 Mesos Master 运行在中央服务器,Mesos Agent 运行在节点服务器上。
Mesos Master 负责收集和管理所有 Agent 所在服务器的资源和状态,并且对接 Spark、Hadoop 等框架,将集群中服务器的资源信息告知给这些框架,以便这些框架进行任务资源匹配和调度。Mesos Agent 负责任务的拉起、停止、重启等,并负责收集所在服务器的资源 (比如 CPU、内存等) 信息和状态,上报给 Mesos Master。
Mesos Master 通常采用一主两备的方式,以方便故障处理和恢复。而 Mesos Master 的选主策略,采用的是前面说过的 ZAB 算法。
Mesos 架构示意图如下所示:
如上所述,Mesos 对接的是框架,并且可以同时对接多个框架,目前已经被很多公司使用。比如,国外的 Twitter、Apple、Airbnb、Uber 等,国内的爱奇艺、去哪儿、携程、当当等。这些公司选择 Mesos,主要是因为它具有如下优势:
- 效率:Mesos 对物理资源进行了逻辑抽象,在应用层而不是物理层分配资源,通过容器而不是虚拟机(VM)分配任务。因为应用程序的调度器知道如何最有效地利用资源,所以在应用层分配资源能够为每个应用程序的特殊需求做考量;而通过容器分配任务则能更好地进行"装箱"。
- 可扩展性:Mesos 可扩展设计的关键是两级调度架构,其中 Framework 进行任务调度,Mesos Master 进行资源分配。由于 Master 不必知道每种类型的应用程序背后复杂的调度逻辑,不必为每个任务做调度,因此可以用非常轻量级的代码实现,更易于扩展集群规模。
- 模块化:每接入一种新的框架,Master 无需增加新的代码,并且 Agent 模块可以复用,为此开发者可以专注于应用和框架的选择。这,就使得 Mesos 可以支持多种框架,适应不同的应用场景。
随着分布式应用程序和微服务的流行,越来越多的用户正在寻找一种技术,来帮助他们管理这些复杂的应用程序。而 Mesos 为数据中心带来的这些好处,就使得越来越多的人关注 Mesos 及其相关项目。
本节我们分享了分布式系统中的集中式架构,并以 Borg、Kubernetes、Mesos 这三款知名的集群管理系统为例,与你描述了集中式架构的设计目的、框架结构,以及各组件模块的功能等。
- Borg 是 Google 公司内部使用的集群管理系统,既可以执行长服务,也可以执行批处理任务,是一个具有强大功能的、复杂的集群管理系统。
- Kubernetes 是 Borg 的简化开源版,是一个正在兴起的集群管理系统。Mesos 和 Kubernetes 都是为帮助应用程序在群集环境中运行而创建的,Kubernetes 更加专注于运行容器群集,具有更多功能。
- Mesos 是非常典型的开源集群管理系统,在 Mesos 之上,可以搭载诸如 Spark、Hadoop 等框架,甚至可以在 Mesos 上集成 Kubernetes,扩展性强。
可以发现,这三种集群管理系统虽然具有不同的功能组件,但整体框架采用的都是集中式架构。因此,你只要理解了一个集群管理系统的架构,再去理解其他集中式的集群管理架构就会很容易了。
分布式体系结构之非集中式结构:众生平等
上一节我们学习了分布式体系结构中的集中式结构,虽然很多云上的管理都采用了集中式结构,但是这种结构对中心服务器性能要求很高,而且存在单点瓶颈和单点故障问题。
于是为了解决这个问题,分布式领域中又出现了另一经典的系统结构,即非集中式结构,也叫作分布式结构。那什么是非集中式结构呢,它的原理是什么样的,又有哪些集群采用了这种结构呢?
什么是非集中式结构?
在非集中式结构中,服务的执行和数据的存储被分散到不同的服务器集群,服务器集群间通过消息传递进行通信和协调。
在非集中式结构中,没有中央服务器和节点服务器之分,所有的服务器地位都是平等(对等)的,也就是我们常说的众生平等。这样一来,相比于集中式结构,非集中式结构就降低了某一个或者某一簇计算机集群的压力,在解决了单点瓶颈和单点故障问题的同时,还提升了系统的并发度,比较适合大规模集群的管理。
所以近几年来,Google、 Amazon、Facebook、阿里巴巴、腾讯等互联网公司在一些业务中也相继采用了非集中式结构。接下来,我们了解一下 3 种典型的非集中式架构系统,包括 Akka 集群、Redis 集群和 Cassandra 集群,来深入理解非集中式架构。
未完待续
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏