关于分布式的许多理论和概念总感觉不是很清晰,所以花时间梳理了一下。如有不实之处,望指正哈!
1、分布式系统
在处理数据时,我们常常为了得到最理想的计算效果,在“空间”和“时间”之间做选择,因为计算机的存储能力是要受到客观物理条件的限制的,而芯片的计算能力也会受到单位计算单元上瞬时计算的能力限制。为了解决大量数据的存储和计算能力不足的问题,我们一般的选择是:
- 纵向扩展
通过升级硬件设备,比如升级存储量更高的硬盘、单位时间运算速度更高的芯片,以提升计算性能和存储能力。但这种硬件升级会受到计算机本身架构的局限,而且进一步升级所需要的成本也将是指数上升、所获得的边际效益快速下降。 - 横向扩展
通过使用多台计算机组合的形式,并行地进行存储和计算。通过这种方式,扩展容易,也能够解决单机下存储和计算的局限。
使用第二种方式构建的计算机系统叫做 分布式系统,它具有以下三个特点:一组计算机、通过网络传递信息、协调计算机的行为。作为通过网络连接的多台计算机系统,分布式系统的设计目标一般包括:
性能:分布式系统对外提供服务时的延时和吞吐率是必须要满足用户需要的;
可用性:这是分布式系统的核心需求,表示分布式系统是否处于整体可服务并且一直可服务的状态;
容错性:分布式系统发生故障时,系统有对故障进行规避和恢复的能力;
扩展性:增加一台机器不会改变或极少改变其2系统行为,并能获得近似线性的性能提升;
通过搭建合理的系统架构,恰当的数据管理,分布式系统是可以满足以上设计目标的。尽管如此,这是需要付出代价的。分布式的方式不仅增加了工程的复杂性,在实现过程中还需要克服种种问题和困难,甚至在理论上会出现不可逾越的障碍。
一套分布式系统的主要物理要素包括节点的数目以及节点间的距离。仅这两点的更改就会引入以下限制:
- 节点数目的增加会导致系统整体出错概率的增大;
- 节点数目的增加会导致节点间通信量的增加;
- 节点间距离的增加会导致系统最优(或部分)性能变差;
抛开工程视角,仅从理论层面看,分布式系统也存在着如下三类视角的系统划分:
- 保持一致:
系统中相关数据间的逻辑关系应当是正确的和完整的。极端情况下,从系统中任意部分读取的数据应当都为最近写入的数据; - 处理失效:
分布式系统可能出现的失效状况有三类:节点失效、网络失效、拜占庭失效。极端情况下,系统的执行和操作不会受到任何系统内部失效的影响; - 时钟同步:
分布式系统有两种模型:同步系统和异步系统。同步系统会确保所有执行过程的步调一致,且各执行过程有精确的时钟,即任意处理过程都能够得到精确的执行流程的偏序关系,也就意味着每个处理过程和通信都在有限的时间内进行。异步系统则相反,没有任何时序性保证,即各处理过程是完全以自己的节拍在运行,不存在有效的同步时钟,也意味着过程间的通信延时可能会趋于无穷大。
针对物理层面和理论层面存在的种种问题,我们希望能够找到这些问题的答案:是否可以通过硬件技术和软件算法来克服困难,实现一个理想的或接近理想的分布式系统,以达成模拟那台“超级计算机”的设计目标?不幸的是,在实际应用中,理想的分布式系统是不可能实现的。
我们可以从一致性问题(Consensus Problem),也就是分布式系统必须要解决的一个问题出发,来考虑一个分布式算法是否正确时的标准:
- 一致性(Agreement):
每个正确的执行过程应该在相同的值上达成一致; - 完整性(Integrity):每个正确的执行过程最多只能决定一个值,或者说如果决定了某个值的话,这个值一定是被某个执行过程提出的;
- 终止性(Termination):
非失败进程最终会做出一个决定,它描述了算法必须在有限时间内结束,不能无限循环下去; - 合法性(Validity):
进程的决定值必须是其他进程提交的请求值,或者说,如果所有正确的执行过程提出了相同的值,那么所有正确的执行过程都会决定这个值。它是为了排除进程初始值对自身的干扰;
基于以上要求,我们可以推导出在分布式系统领域非常重要的定理:FLP不可能性(这在下面会有说明),仅这一条定理就已经打碎了我们模拟“超级计算机”的幻想。不过从务实的角度考虑,虽然不能实现理想的分布式系统,但我们是否可以通过对系统主要设计指标进行一定的妥协,来设计出一个理论上可行、能满足实际需求的分布式系统呢?实际上,CAP定理(这在下面会有说明)已经为我们回答了这个问题。
在计算的世界里,一切都是有代价的。我们必须根据实际的业务场景,在关键的业务指标中进行权衡,进而选择合适的解决方案,来达成我们对系统的主要设计指标。
2、分布式协调
什么是分布式协调?就是用来解决在分布式环境中多个进程之间的同步控制,让其有序的访问临界资源。对于单机环境,临界资源的访问我们通常只要使用一个调度算法就能搞定,但在分布式环境下,情况会变的不一样。
比方说,我们有三台服务器,每台机器上跑一个应用程序,然后我们将这三台机器通过网络将其连接起来,构成一个对用户而言透明的系统来对外提供服务,这就是我们通常看到的 分布式系统。那么,我们如何在这个分布式系统中进行调度呢?我们假设有一个可访问的资源[Peer Source],这三台物理机上的进程都会去竞争这个资源,但我们又不希望他们同时进行访问,这个时候我们就需要一个 协调器,来保证分布式系统中多个进程对临界资源的有序访问。
分布式协调的出现,是为了防止分布式系统中多个进程之间相互干扰,来让他们通过一个协调器协同工作。而分布式协调技术要实现的核心内容,其实就是 分布式锁。我们需要这么一套锁机制:它应该是独占的,如果某个进程要使用该资源的时候都要先获得锁;获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源;进程使用完该资源以后就将锁释放掉,来让其他进程来获得锁。但这并不是将原来在单台机器上对线程调度的原语,通过网络实现在分布式环境中,实际情况要复杂的多。
在分布式系统中,所有在同一台机器上的假设都不复存在,因为网络是不可靠的!比如,在单机环境下,你对服务的调用如果成功就是成功,失败就是失败。但是在分布式环境中,由于网络的不可靠,你对一个服务的调用失败了并不表示一定是失败的,可能是对方执行成功了,但是响应返回的时候失败了。再比如,A服务器和B服务器都去调用C服务器上的服务,在时间轴上A先调用、B后调用,但可能由于网络延迟,A的请求先于B请求到达。总之,在同一台服务器上的种种假设,我们都需要重新思考。
3、经典问题
3.1、两军问题
两支军队需要协同开进,他们必须互相联络来约定攻击时间,但信差可能会被截获、会被路路阻隔而延误,所以两支军队彼此之间需要知道对方是否已知晓约定的时间,收到消息之后还要进行确认。比如说,A1通知A2在某个时间点发起攻击,消息发出去之后A1并不知道A2是否收到了这个消息;A2收到消息之后,为了让A1放心,A2要发送一个确认的返回信息给A1,但返回的消息也面临着消息被截获、丢失的可能,那么A2也可能会停止此次行动。
这就是两军问题(two-army problem):通信的双方在一个不可靠的通信链路上,试图通过 ACK(确认字符)来达成 共识(Consensus),永远都可能会有一个在途的ACK需要进行确认,因此无法达成共识。
- 通信的双方需要达成共识;
- 假设的前提是信道不稳定,有丢包、延迟或者重发,但消息不会被篡改;
两军问题适用于任何有可能通信失败情况下的两点通信,也被称为“两军悖论”,是在计算机通信领域首个被证明无解的难题。经管我们传输信息仍有可能出现丢失、监听,但我们现阶段能通过一种相对可靠的方式来解决大部分情形,比如TCP协议的“三次握手”。
3.2、拜占庭问题
拜占庭这个国家 ①想要进攻一个强大的敌国,为此派出了一批军队去包围这个敌国,依靠信差进行通信。基于一些原因,这批军队不能集合在一起单点突破,且任一支军队单独进攻都毫无胜算。但他们不确定他们中是否有叛徒或间谍,来扰乱整体军队的秩序,导致达成的共识并不代表大多数人的意见。
拜占庭问题(Byzantine Generals Problem)关注的并不是消息传递的通信信道是否稳定,两军问题已经证明在消息可能丢失的不可靠信道上试图通过消息传递的方式达到一致性是无解的。它关注的是,如果信道绝对可信,如何去保证分布式系统容错性的问题。所谓拜占庭失效即是指一方向另一方发送消息,另一方没有收到或者收到了错误的消息的情形。
拜占庭问题其实是对现实世界的模型化,由于硬件错误、网络拥塞或断开以及遭到恶意攻击、消息篡改,计算机和网络可能出现不可预料的行为。从分布式系统的角度来说,如果要保证分布式系统的一致性和可用性,就必须处理错误节点,防止系统出现用户可以观察到的错误。特别是它提出了一个错误模型,错误节点可以做任意事情(不受protocol限制),比如不响应、发送错误信息、对不同节点发送不同决定、不同错误节点联合起来发错误指令等。
拜占庭错误(Byzantine Failure ②)的上限是,如果某个一致性算法能够保证在系统出现f个拜占庭错误时保持系统一致,那么这个算法也能够保证在出现f个任意其他错误的时候也保持系统一致。而Byzantine Failure的下限就是‘Fail-Stop’模型。这个模型的假设是:当一个节点出错时,这个节点会停止运行,并且其他所有节点都知道这个节点发生了错误。用同样的逻辑,如果某个一致性算法不能保证在系统出现f个错误的时候保持一致,那么这个算法也就没法处理其他f个任意其他问题。
拜占庭问题是可解的,在同步假设(同步保证 safety,或者同步保证 liveness)下,我们可以找到两种办法:口头协议或书面协议,具体的使用场景和实现可以阅读附录里的参考链接。
3.3、脑裂问题
分布式通常假设网络是异步的,意味着网络可能会导致任意的重复、丢失、延迟或者乱序的节点间消息传递。在实际应用中,TCP状态机会保证节点间消息传递的不丢失、不重复、时序性。但是,在Socket级别上,节点接发消息仍会阻塞、超时等。
检测到网络失败是困难的,因为我们唯一能得到其他节点状态的信息就是通过网络来得到,甚至连延迟与网络失败也无从区分。这里就会产生一个 网络分区问题(Network Partition,也叫脑裂问题):存在A\B\C\D\E四个节点,A\B处于同一子网,C\D\E处于另一子网,中间通过交换机链接。若两个子网间的交换机故障了即发生了网络分区,A\B和C\D\E便不能通讯。即,不同节点自身没有失效但之间的通信发生了中断都可认为是发生了Partitions。特别是,在这种情况下,分裂的双方可能都会从对方一侧重启应用程序,进而服务重复。
当分区产生后,我们没有渠道去了解到其他节点到底发生了什么事, 它们是否存活?或者已经crash?是否有收到消息?是否正在尝试回应?当网络最终恢复后,我们需要重新建立连接然后尝试解决在不一致状态时的不一致。
网络分区策略(Network Division Tactics),它意味着一些分布式系统是分区容错的,即使在将它们被分成多个子系统之后,它们的工作方式与以前一样。
不同的分布式系统会对一致性和持久性相互影响做权衡。如果你像Zookeeper来写入,那你会得到一个强一致性保证:写操作对所有人可见,比如这个写操作在一半以下的节点失败后仍然能够保证;如果你像MySQL来写入,取决于你的事务一致性级别,你的写操作会对所有人、你可见,或者最终的一致性。CAP理论告诉我们,要么得到一致性要么得到高可用性,我们多数也是丢失数据,而不是达到CAP理论的极限。
我们一般将网络分区策略分为乐观和悲观:
- 乐观策略认为,应该让分区节点照常运行,这其实提高了可用性而牺牲了一致性。分区问题结束后,为使系统处于一致性状态,可能需要进行自动协调或人工干预。目前这种方法的一个实现是Hazelcast,即网络恢复后将自动采取措施修复网络分区带来的问题。
- 悲观策略认为,应该牺牲可用性以换取一致性。一旦检测到网络分区,对子分区的访问要马上限制。这允许具有大多数的子分区保持可用,而其余的子分区则下降到自动防护模式。这种方法的一个实现是MongoDB副本集。
4、常见的方法论
面对着庞大复杂的分布式系统,很多时候我们关注的不是单节点的系统异常,更多的是系统整体的稳定和健壮,而分布式系统的核心就是解决一个问题:不同节点间如何达成共识。节点异常(节点宕机、恢复、重启)或网络异常(消息延迟、丢失、重复、乱序,还有网络分区)等场景下,并由此衍生出许多概念、模型:为探究共识问题最大能解决程度,于是有 FLP、CAP 等边界理论;为在特定条件和范围内解决共识问题,于是有 Paxos、Raft、Zab和Viewstamped Replication(简称 VR);为构建这些一致性协议,于是有选举、多数派、租约和逻辑时钟 ③。
一致性问题是分布式理论中的根本性问题。就是说,在节点异常或网络异常等场景下,相互独立的节点之间如何达成决议的问题。一个分布式系统要满足一致性的标准,这我们上面提到过:一致性[Agreement],完整性[Integrity],终止性[Termination],还有合法性[Validity]。同时,一致性具备有两个属性:强一致性(safety),它要求所有节点状态一致、共进退;可用性(liveness),它要求分布式系统不间断地对外提供服务。FLP已经证明在一个理想的模型中,不能同时满足safety和liveness。
在实际应用中,我们需要根据具体的业务场景具体分析,或保证强一致,或在节点宕机、网络分区的时候保证高可用,来权衡/取舍中做出最优解。
4.1、选举
leader其实就是一堆服务器中的协调者,某一个时刻只能有一个leader且所有服务器都承认这个leader。选举(Leader Election)算法就是在一组进程中,选举出一个leader且让进程都同意这个leader。
假设有N个process,每个process都对应一个序号ID,都可以提出选举。如果有多个进程同时发起选举,那么只有具有最大ID的进程会完成选举。Leader Election算法一般有两种:
Ring Algorithm:
process组成一个环,第N个n_i可以和n_(i+1)节点通信。如果进程i发现原来的leader挂了,它就会发起选举,发送一个包含自己ID的选举消息 p_i。然后当某个进程j收到了这个消息的时候,会将这个ID与自己的ID比较,如果p_i>p_j,则继续转发这条消息;如果p_i<p_j且它之前没有转发过这条消息,就把p_i取代然后把自己的选举消息发送出去;如果发现收到的ID与自己相等,代表自己成为leader,然后把自己当选的消息发送出去。
最坏情况分析:一个进程发起选举,如果它的上一个进程具有最大标识符,则选举消息到达上一个进程需要(N—1)次传递,然后消息又要N次才能完成选举,最后需要N个消息告诉大家它当选了,所以需要(3N-1)个消息。最好情况就是发起选举的进程就是leader,只需要(2N)个消息。
环算法实用价值很少,如果在选举过程中有进程崩溃,选举就无法完成。
Bully Algorithm:
当某一进程发现leader挂掉,如果自己ID最大,就宣布自己是leader,否则向ID比自己高的进程发起选举。发起选举如果没有收到回应,就宣布自己是leader;收到了回应就等待比自己ID大的进程宣布结果;当收到ID比自己低的进程发来的选举消息时,就向更高的进程发起选举。
最坏情况分析:只需5个消息传递时间
- 最小ID进程发起选举
- 第二大ID进程回应
- 第二大ID进程向最大ID进程发起选举
- 最大ID进程无回应,timtout
- 第二大ID进程宣布当选
在一致性算法Paxos、ZAB、Raft中,为提升决议效率均有节点充当leader的角色。比如在ZAB中,使用zxid标识节点,具有最大zxid的节点表示其所具备的事务(transaction)最新,被选为leader。
4.2、多数派
多数派思维(quorum)是帮助我们在网络分区的情况下达成决议一致性,在Leader选举的场景下帮助我们选出唯一Leader。
在网络分化的场景下以上选举算法会遇到一个问题:被分隔的节点都认为自己具有最大的序号、将产生多个leader,这时候就需要引入多数派的思路。这在分布式系统中很常见,主要用来在leader选举中,网络分化场景下只有具备多数派节点的部分才可能选出leader,这避免了多leader的产生。
假如节点总数为2f+1,则一项决议得到多于f节点赞成则获得通过。这也是为什么leader选择中节点通常都是奇数的原因。
4.3、租约
租约(Lease)确保一个时刻最多只有一个leader,避免只使用心跳机制产生双主的问题。
leader选举中一个很重要的问题,上面没有提到:怎么判断leader不可用,或者什么时候应该发起重新选举?最容易想到的是通过心跳(heart beat)判别leader状态是否正常,但是在网络拥塞或瞬断的情况下,这容易导致出现 “双主”。租约就是解决这个问题的常用方法。
Lease的原理不复杂,中心思想是在每次租约时长内只有一个节点获得租约、到期后必须重新颁发租约。假设我们有租约颁发节点Z,节点0、1和2竞选leader,租约过程如下:
- 节点0、1、2在Z上注册自己,Z根据一定的规则(例如FIFO)颁发租约给节点,该租约同时对应一个有效时长,比如节点0获得租约(成为leader);
- leader(节点0)宕机,但只有租约到期后才重新发起选举,比如节点1获得租约(成为leader);
※ 附录:
注:
① 拜占庭帝国即东罗马帝国,这是由莱斯利·兰伯特借罗马历史提出的点对点通信中的基本问题。
② Byzantine Failure(拜占庭故障)是指分布式系统中的某一恶意节点允许做任意事情去干扰系统的正常运行,比如选择性不传递消息、选择性伪造消息等。它是用来研究如何在这样的Failure下,整个系统不会Fail。
③ 逻辑时钟,也被称为Lamport时间戳(Lamport timestamps),我们一般讨论最多的是在Lamport时间戳基础上演进的两种时钟方法:Vector clock和Version vector。限于篇幅,这里并不展开讲。
参考: