探究分布式架构的理论基石


概述

现如今的服务已经很少是仅由单机提供的了,这些系统为了同时服务大量的客户,都需要依靠分布式系统以突破单机的瓶颈。CAP 定理决定了分布式系统天生上的限制,通过 CAP 定理我们可以理解为什么共识机制如此复杂,以及系统如何在强一致性与最终一致性间做取舍。

青蛙图

分布式有多难?

不管设计什么架构,我们都希望满足 高性能高可用 的系统,为了突破性能的瓶颈,我们从单核走向了多核,从单机走向了多机;为了防止意外造成系统无法运作,我们通过「备份」来对抗各种天灾人祸。分布式的目的就在于 突破单机的性能瓶颈建立不间断的服务 ,而分布式设计带来的好处,恰恰也是分布式设计的困难点。拓展阅读:分布式事务最经典的七种解决方案

CAP 定理

CAP

一致性(C:Consistency)

一致性意思就是写操作之后进行读操作无论在哪个节点都需要返回写操作的值。

对于一个将数据副本分布在不同分布式节点上的系统来说,如果对第一个节点的数据更新后,却没有使得第二个节点上的数据得到更新,于是在对第二个节点的数据进行读取时,获取的依然是老数据,这就是典型的分布式数据不一致的情况。

可用性(A:Availability)

指系统提供的服务一直处于可用的状态,每次请求都能获取到非错的响应(不保证获取的数据为最新数据)。

分区容错性(P:Partition tolerance)

分布式系统在遇到任何网络分区(个别节点通信异常)故障的时候,仍然能够对外提供服务。

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

对于分布式系统而言,网络问题是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然要解决的问题。因此往往需要根据业务特点在一致性和可用性之间寻求平衡,也就是说理论上不可能选择 CA 架构,而必须选择 CP 或 AP 架构。

分布式的不可能三角 — CAP 三元取舍

CAP 定理乍看之下,三个任取两个都可以,但在现实世界中,网络是最无法被保证的,所以分区容错性(P)是一定要被保障的,所以实际设计系统时,我们要在一致性(C)跟可用性(A)之间做取舍。

强一致性 — CP 与 ACID

ACID 是关系型数据库的基石,代表每次事务(Transaction)需要满足原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)及持久性(Durability)。CAP 定理是对于系统的描述,而 ACID 是对于事务(Transaction)的描述,虽然两种对于不同层面描述的理论不应该被放在一起讨论,但这里想强调的是 ACID 是强一致性的描述,如果在分布式情况下满足了,也等于满足了 CP 模型。

分布式要在系统层面满足强一致性,通常会使用两阶段提交(Two-Phase Commit, 2PC),通常步骤如下:

  1. 使用者向协调者(Coordinator)发起一个写操作
  2. 准备阶段(Prepare Phase): 协调者向其他所有节点询问是否可以进行此操作
  3. 执行阶段(Commit Phase): 若所有节点都回复可以执行操作,则协调者向所有节点发送执行此操作

设计两阶段提交时要注意,在准备阶段(Prepare Phase),节点如果回复「允许」进行操作,那么不管发生什么意外,节点都要能保证在执行阶段(Commit Phase)进行此操作,即使准备阶段后,节点因意外关机,节点也要在意外恢复后,也需要依据协调者的指令完成准备阶段答应的操作。另外因为是强一致性的关系,所以协调者会在执行阶段(Commit Phase)结束才回复使用者,因为如果协调者在准备阶段(Prepare Phase)结束就回复给使用者,那可能因为协调者还没发送执行信息给其他节点前,就意外关机,造成使用者接收到的信息与整个集群不一致。

Prepare Phase

Commit Phase

两阶段操作被应用在许多分布式算法及系统中,比较出名的像是 MySQL XA 、 Raft 等。在更复杂的情况,使用三阶段提交(Three-Phase Commit, 3PC)来解决。

高可用性 — AP 与 BASE

BASE 是 Basically Available(基本可用)、Soft State(软状态)和 Eventually Consistent(最终一致性)三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结。BASE 强调牺牲高一致性,从而获取可用性,数据允许在一段时间内不一致,只要保证最终一致性就可以了。

对于互联网的用户来说,如果一个系统不可用,那即使应用内部的状态有多么的一致,对用户来说都是坏掉的,所以我们很常在互联网应用中看到基础可用(Basically Available)的手法,以 微博点赞数为例,热门的贴文刚发表时,就会涌入一堆人按赞,每一次按赞对系统来说都是一次写入,如果系统为了强一致性让一部份人暂时无法写入的话,那大家一定会觉得是不是系统坏掉了,所以微博并不需要急著统计出一个精确的数字,因为对大多数人来说 8 个赞跟 10 个赞没什么区别,只要系统可以在最后保持最终一致性即可。

不过只满足了基础可用,系统还是要有方法可以恢复最终一致性(Eventually Consistent),常见的做法有读时修复(Read Repair)、写时修复(Write Repair)以及反熵(Anti-Entropy)。读时修复在读取时同时到多个节点读取,并以最新的节点为主;写时修复,同时写入多个节点,若发现有写入失败则记录下来,定时重传,直到写入成功,或是有新的写入为止;最后反熵则是定期检查状态是否一致,如果不一致则通过特定的修复顺序,修正每个节点的数据。

NPC 挑战

NPC是Network Delay, Process Pause, Clock Drift的首字母缩写。我们先看看具体的NPC问题是什么:

  • Network Delay,网络延迟。虽然网络在多数情况下工作的还可以,虽然TCP保证传输顺序和不会丢失,但它无法消除网络延迟问题。
  • Process Pause,进程暂停。有很多种原因可以导致进程暂停:比如编程语言中的GC(垃圾回收机制)会暂停所有正在运行的线程;再比如,我们有时会暂停云服务器,从而可以在不重启的情况下将云服务器从一台主机迁移到另一台主机。我们无法确定性预测进程暂停的时长,你以为持续几百毫秒已经很长了,但实际上持续数分钟之久进程暂停并不罕见。
  • Clock Drift,时钟漂移。现实生活中我们通常认为时间是平稳流逝,单调递增的,但在计算机中不是。计算机使用时钟硬件计时,通常是石英钟,计时精度有限,同时受机器温度影响。为了在一定程度上同步网络上多个机器之间的时间,通常使用NTP协议将本地设备的时间与专门的时间服务器对齐,这样做的一个直接结果是设备的本地时间可能会突然向前或向后跳跃。

TCC的空补偿与悬挂

以分布式事务中的TCC作为例子,看看NP带来的影响。一般情况下,一个TCC回滚时的执行顺序是,先执行完Try,再执行Cancel,但是由于N,则有可能Try的网络延迟大,导致先执行Cancel,再执行Try。这种情况就引入了分布式事务中的两个难题:

  1. 空补偿:Cancel执行时,Try未执行,事务分支的Cancel操作需要判断出Try未执行,这时需要忽略Cancel中的业务数据更新,直接返回
  2. 悬挂:Try执行时,Cancel已执行完成,事务分支的Try操作需要判断出Cancel一致性,这时需要忽略Try中的业务数据更新,直接返回

分布式事务还有一类需要处理的常见问题,就是重复请求,业务需要做幂等处理。因为空补偿、悬挂、重复请求都跟NP有关,我们把他们统称为子事务乱序问题。在业务处理中,需要小心处理好这三种问题,否则会出现错误数据。

现有方案的问题

  • 空补偿:“针对该问题,在服务设计时,需要允许空补偿,即在没有找到要补偿的业务主键时,返回补偿成功,并将原业务主键记录下来,标记该业务流水已补偿成功。”
  • 防悬挂:“需要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝执行该笔服务,以免造成数据不一致。”

上述的这种实现,能够在大部分情况下正常运行,但是上述做法中的“先查后改”在并发情况下是容易掉坑里的,我们分析一下如下场景:

  • 正常执行顺序下,Try执行时,在查完没有空补偿记录的业务主键之后,事务提交之前,如果发生了进程暂停P,或者事务内部进行网络请求出现了拥塞,导致本地事务等待较久
  • 全局事务超时后,Cancel执行,因为没有查到要补偿的业务主键,因此判断是空补偿,直接返回
  • Try的进程暂停结束,最后提交本地事务
  • 全局事务回滚完成后,Try分支的业务操作没有被回滚,产生了悬挂

事实上,NPC里的P和C,以及P和C的组合,有很多种的场景,都可以导致上述竞态情况,情况发生的概率不高,但是在分布式只要有概率发生的事情那就一定会发生。当然现在也有一些更优的解决方案,如阿里开源的seata。


文章作者: JoyTsing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 JoyTsing !
评论
  目录