前两天的高德交叉面的时候,面试官把我PUA了,其中问到为啥cap理论中一般只能实现两个?这让我这捉襟见肘的脑容量瞬间爆炸了,赶紧在极客时间上面搞了个分布式理论算法原理与实践课程,来学习一下吧,下面的内容就是学习该课程的相关笔记。
分布式系统里,最重要的事情,就是如何选择或设计适合的算法,解决一致性和可用性相关的问题了。
拜占庭帝国想要进攻一个强大的敌人,为此派出了10支军队去包围这个敌人。这个敌人虽不比拜占庭帝国,但也足以抵御5支常规拜占庭军队的同时袭击。基于一些原因,这10支军队不能集合在一起单点突破,必须在分开的包围状态下同时攻击。他们任一支军队单独进攻都毫无胜算,除非有至少6支军队同时袭击才能攻下敌国。他们分散在敌国的四周,依靠通信兵相互通信来协商进攻意向及进攻时间。困扰这些将军的问题是,他们不确定他们中是否有叛徒,叛徒可能擅自变更进攻意向或者进攻时间。在这种状态下,拜占庭将军们能否找到一种分布式的协议来让他们能够远程协商,从而赢取战斗?这就是著名的拜占庭将军问题。(其中不会去考虑通信兵是否会被截获或者无法传达信息等问题,即信道没有问题。有一个相似问题叫两军问题)
拜占庭将军问题解决方法一:如果叛将人数为 m,将军人数不能少于 3m + 1 ,那么拜占庭将军问题就能解决了(你也可以从另外一个角度理解:n 位将军,最多能容忍 (n - 1) / 3 位叛将)然后通过m+1轮口信协商才能解决该问题。r
在二忠一叛的问题中,在存在 1 位叛将的情况下,必须增加 1 位将军,将 3 位将军协商共识,转换为 4 位将军协商共识,这样才能实现忠诚将军的一致性作战计划。
第一轮:
先发送作战信息的将军作为指挥官,其他的将军作为副官;
指挥官将他的作战信息发送给每位副官;
每位副官,将从指挥官处收到的作战信息,作为他的作战指令;如果没有收到作战信息,将把默认的“撤退”作为作战指令。
第二轮
除了第一轮的指挥官外,剩余的 3 位将军将分别作为指挥官,向另外 2 位将军发送作战信息;然后,这 3 位将军按照“少数服从多数”,执行收到的作战指令
签名消息指的就是带有数字签名的消息,你可以这么理解“数字签名”:类似在纸质合同上进行签名来确认合同内容和证明身份.主要是用消息的摘要算法和非对称加密来实现的。
忠诚将军的签名无法伪造,而且对他签名消息的内容进行任何更改都会被发现;任何人都能验证将军签名的真伪。
签名消息的拜占庭问题之解,也是需要进行 m+1 轮,从另外一个角度理解:n 位将军,能容忍 (n - 2) 位叛将。
拜占庭容错算法(Byzantine Fault Tolerance,BFT):在存在恶意攻击节点行为的场景中(比如在数字货币的区块链技术中),可使用BFT算法去处理,类似的算法还包括PBFT 算法,PoW 算法。
非拜占庭容错算法,即故障容错算法(Crash FaultTolerance,CFT)。CFT 解决的是分布式的系统中存在故障,但不存在恶意节点的场景下的共识问题。 这个场景可能会丢失消息,或者有消息重复,但不存在错误消息,或者伪造消息的情况。常见的算法有 Paxos 算法、Raft 算法、ZAB 协议(这些内容我同样会在后面讲解)。
虽然口信消息和签名消息能够在理论上解决这个问题,但是这两个解决方案也都是有局限性的。如果将军书为n,叛将数f,那么算法需要递归协商f+1轮,消息的复杂度为O(n^(f+1)),消息数量指数级暴增。所以这两种算法在实际中难以落地。
CAP 不可能三角说的是对于一个分布式系统而言,一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)3 个指标不可兼得,只能在 3 个指标中选择 2 个。关于原因可以参考上面的理论说明。正是由于每个特性的要求导致这三个特性永远无法得到同时满足。
在网络交互中就一定会有延迟和数据丢失,但这种状况我们必须接受,还必须保证系统不能挂掉。节点间的分区故障肯定是有可能发生的。也就是说,分区容错性(P)是前提,是必须要保证的。
因为CAP只能同时实现其中的两个:
CP:当选择了一致性(C)的时候,如果因为消息丢失、延迟过高发生了网络分区,部分节点无法保证特定信息是最新的,那么这个时候,当集群节点接收到来自客户端的写请求时,因为无法保证所有节点都是最新信息,所以系统将返回写失败错误,也就是说集群拒绝新数据写入。典型的应用是 ZooKeeper,Etcd 和 HBase, 适合 事务、元数据信息存储。
AP:当选择了可用性(A)的时候,系统将始终处理客户端的查询,返回特定信息,如果发生了网络分区,一些节点将无法返回最新的特定信息,它们将返回自己当前的相对新的信息。典型应用就比如 Cassandra 和 DynamoDB。适合 一致性要求不高的业务存储。
CA 模型,在分布式系统中不存在。因为舍弃 P,意味着舍弃分布式系统,就比如单机版关系型数据库 MySQL,如果 MySQL 要考虑主备或集群部署时,它必须考虑 P。
其实,在不存在网络分区的情况下,也就是分布式系统正常运行时(这也是系统在绝大部分时候所处的状态),就是说在不需要 P 时,C 和 A 能够同时保证。只有当发生分区故障的时候,也就是说需要 P 时,才会在 C 和 A 之间做出选择。而且如果各节点数据不一致,影响到了系统运行或业务运行(也就是说会有负面的影响),推荐选择 C,否则选 A。
一个分布式事务,要么全部执行,要么全部不执行。
当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交。
二阶段提交的算法思路可以概括为: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
两个阶段是指:
二阶段提交协议,不仅仅是协议,也是一种非常经典的思想。二阶段提交在达成提交操作共识的算法中应用广泛,比如 XA 协议、TCC、Paxos、Raft 等。
幂等性,是指同一操作对同一系统的任意多次执行,所产生的影响均与一次执行的影响相同,不会因为多次执行而产生副作用。常见的实现方法有 Token、索引等。它的本质是通过唯一标识,标记同一操作的方式,来消除多次执行的副作用。
1、同步阻塞问题。
执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。也就是说从投票阶段到提交阶段完成这段时间,资源是被锁住的。
2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。
尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
【协调者发出Commmit消息之前宕机的情况】
(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
4、二阶段无法解决的问题:------ 极限情况下,对某一事务的不确定性!
【协调者发出Commmit消息之后宕机的情况】
协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
三阶段提交协议是二阶段提交的改进版:
1、引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点状态的一致。
3PC把2PC的投票阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
为什么要把投票阶段一分为二?
假设有1个协调者,9个参与者。其中有一个参与者不具备执行该事务的能力。协调者发出prepare消息之后,其余参与者都将资源锁住,执行事务,写入undo和redo日志。协调者收到相应之后,发现有一个参与者不能参与。所以,又出一个roolback消息。其余8个参与者,又对消息进行回滚。这样子,是不是做了很多无用功?所以,引入can-Commit阶段,主要是为了在预执行之前,保证所有参与者都具备可执行条件,从而减少资源浪费。
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
BASE的核心就是基本可用(Basically Available)和最终一致性(Eventually consistent);
软状态(Soft state),软状态描述的是实现服务可用性的时候系统数据的一种过渡状态,也就是说不同节点间,数据副本存在短暂的不一致。
BASE 理论是 CAP 理论中的 AP 的延伸,是对互联网大规模分布式系统的实践总结,强调可用性。几乎所有的互联网后台分布式系统都有 BASE 的支持,这个理论很重要,地位也很高。
基本可用是当分布式系统在出现不可预知的故障时,允许损失部分功能的可用性,保障核心功能的可用性。主要有流量削峰、延迟响应、体验降级、过载保护等几种解决方案。
如12306在不同的时间,出售不同区域的票,将访问请求错开,削弱请求峰值。在春运期间,自己提交的购票请求,往往会在队列中排队等待处理,可能几分钟或十几分钟后,系统才开始处理,然后响应处理结果,这就是你熟悉的延迟响应。 体验降级, 比如用小图片来替代原始图片,通过降低图片的清晰度和大小,提升系统的处理能力。过载保护, 比如把接收到的请求放在指定的队列中排队处理,如果请求等待时间超时了(假设是 100ms),这个时候直接拒绝超时请求;比如队列满了之后,就清除队列中一定数量的排队请求,保护系统不过载,实现系统的基本可用。
最终一致性:最终一致性是说,系统中所有的数据副本在经过一段时间的同步后,最终能够达到一个一致的状态。也就是说,在数据一致性上,存在一个短暂的延迟。
在实现最终一致性时有以下几种思路:
读时修复:在读取数据时,检测数据的不一致,进行修复。比如 Cassandra 的 ReadRepair 实现,具体来说,在向 Cassandra 系统查询数据的时候,如果检测到不同节点的副本数据不一致,系统就自动修复数据。
写时修复:在写入数据,检测数据的不一致时,进行修复。比如 Cassandra 的 Hinted Handoff 实现。具体来说,Cassandra 集群的节点之间远程写数据的时候,如果写失败就将数据缓存下来,然后定时重传,修复数据的不一致性。
异步修复:这个是最常用的方式,通过定时对账检测副本数据的一致性,并修复。
Reference: