飞道的博客

漫谈分布式系统(10) -- 初探分布式事务

475人阅读  评论(0)

这是《漫谈分布式系统》系列的第 10 篇,预计会写 30 篇左右。每篇文末有为懒人准备的 TL;DR,还有给勤奋者的关联阅读。扫描文末二维码,关注公众号,听我娓娓道来。也欢迎转发朋友圈分享给更多人。  

无心插柳,用分布式事务来解决数据一致性

上一篇,我们对分布式系统的一致性问题有了基本的了解。详细介绍了单主同步的数据复制机制,虽然已经 best effort guarantee,但仍然不能保证想要的强一致性。

然后通过对 data replication 的本质分析,发现了用事务解决数据一致性问题的可能。

虽然事务可以用来解决数据复制过程中的一致性问题,但事务的初衷却不是这个,至少不只是这样。所以,我们有必要好好了解下分布式事务。

不一致的根源

副本上的数据会不一致,是因为机器、网络故障等原因,导致要么副本间不知道彼此不一样,要么知道不一样但是解决不了。

本质上,是因为每个副本都只掌握了局部信息,无法做出正确的决策。

对症下药,如果每个副本都能知道全部的信息,就能做出正确的决策了。

但这样带来的信息同步消耗,以及更多更复杂的流程带来更高的出错可能,事实上就不可行了。

折中下,不用所有副本,只要有一个副本(把这部分功能抽离出来并赋予新的角色)掌握所有信息,再让它去协调其他副本。

这个思想,其实在单 master 多 slave 的架构上已经有体现,master 作为特殊的副本,就已经充当了协调者的功能,只不过是分别独立地向其他副本做数据复制。而现在需要把这些各自独立的协调工作合并起来考虑。

解决故障的另一种思路

一个复杂的分布式系统,可能出故障的地方实在太多了:

  • 服务器可能宕机,还要分成重启就能恢复和永远恢复不了。

  • 网络故障,还要分成偶然抖动和长期故障。

  • 服务故障,还要分成不同角色是否同时故障。

  • ......

我们还需要为每种可能的故障,去设计应对机制,以副本问题为例:

  • 为了应对故障,必须要有副本。

  • 为了应对 IDC 级别的故障,副本必须分布在不同的机房。

  • 为了应对交换机级别的故障,不能把所有副本都放在同一个交换机。

  • ...

这样不仅很难覆盖所有可能的故障,势必还会导致系统设计和实现越来越复杂,反过来影响系统的可靠性。

能不能换个思路,我知道故障可能发生,并且会有各种不同的故障,甚至有时候都没法判断当前发生的是什么故障。

但是我不想再这么细粒度的去处理这个问题,我就大老粗一点,先去操作着,能成功最好;如果失败了,不管因为什么原因失败,恢复现场,待会再重新操作一遍。

2PC

上面两个思想结合起来,就有了所谓 2PC(Two Phase Commit)。2PC 也是分布式事务的典型实现方式之一。

给协调的角色一个新的名字叫协调者(Coordinator),其他参与角色就叫参与者( Participant)。

协调者作为核心,掌握全局信息,拥有决策的能力和权利。参与者只用关注自己,安心的干活。

之所以叫 2PC,正是因为整个事务提交被分成了两个阶段:准备(Prepare)和提交(Commit)。

主要执行流程如下:

  1. 协调者收到应用端提过来的事务请求后,向所有参与者发送 Prepare 指令。

  2. 参与者在本地做好保证事务一定能成功的准备工作,如获取锁等,并记录 redo log 和 undo log,以便重做或回滚(类似单机事务)。如果能满足,则返回 Yes 给协调者,否则返回 No。

  3. 协调者收到所有参与者的回复后,汇总检查并记录在本地事务日志中,如果所有回复都是 Yes,则向所有参与者发送 Commit 指令,否则发送 Abort 指令。

  4. 参与者接收到协调者的指令,如果是 Commit 指令,就正式提交事务;如果是 Abort 指令,则依据 undo log 执行回滚操作。

看起来很好地满足了需求,但2PC 是否足够完美,我们还要仔细分析下执行过程。可以从几个维度来分析:

  • 过程:两个阶段一共有 4 次消息传递,还可以细分为消息传输前中后。

  • 故障点:可能发生故障的有服务器(参与者、协调者)和网络,还可以细分为单个故障和同时多个故障。

  • 故障类型:可恢复的机器故障(fail-recover)、不可恢复的机器故障(fail-dead)、网络抖动(偶然丢包)、网络分区(较长时间的网络不通)。

  • 影响:短期阻塞影响性能、永久阻塞影响可用性、数据一致性问题。

我尝试过把以上这些维度全部组合考虑,但实在太复杂了,我们先找一些规律和共性,排除掉一些组合,只关注重点情况。

对于过程:

  • 第一阶段由于不会实际提交数据,所以可以在发生故障后取消整个事务,不会有副作用,只是过程中可能会阻塞。

  • 第二个阶段就会实际提交数据了,一旦发生部分提交,就可能导致数据一致性问题。更具体地,由于参与者的回复消息丢失时,事务已经实际执行完毕,不会产生副作用,因此只关注协调者发送的消息部分送达的情况。

对于故障点和故障类型:

  • fail-recover 类的故障,由于协调和和参与者都会在本地持久化事务状态,再加上消息重试机制,都只会阻塞当前事务,或者由于当前事务占用了资源(如获取锁)导致其他事务阻塞,但不会导致数据一致性问题。

  • fail-dead 类的故障,由于本地事务状态丢失,就有数据不一致的可能。参与者 fail-dead 可以从其他参与者复制数据,而协调者的 fail-dead 就没地方可以复制了,需要重点关注。

  • 网络抖动类的故障,可以通过消息重试解决,只会导致阻塞,不会导致一致性问题(严格来讲,重试成功前,数据也是不一致的)。

  • 网络分区类的故障,通过上篇文章对 CAP 的分析,是导致数据一致性问题的重要原因,需要重点关注。

(BTW,看起来好多问题都是消息部分送达导致的,看起来协调者需要把给多个参与者发送 commit 做成一个事务啊。但是这不还在设计事务的过程中吗,禁止套娃!)

从上面的分析,可以先有第一个结论,短时阻塞是随时随地都可能发生的,这是同步操作的天性,也是 2PC 无法回避的缺点。

然后,我们重点关注以下可能导致系统永久阻塞数据一致性问题的维度:

  • 对于过程,我们关注第二阶段,并且重点关注第二阶段部分消息传递成功的情况,这种情况才会造成实际影响。

  • 对于故障点和故障类型,我们关注协调者 fail-dead 和网络分区这两类。

编号 过程 协调者 参与者 一致性隐患 可能永久阻塞
1 commit/abort fail-dead ok no no
2 commit/abort fail-dead fail-dead yes yes
3 commit/abort fail-dead fail-recover no no

逐个按编号解释下:

  • 编号 1,协调者发出 commit/abort 消息后死掉,部分参与者接收到了,部分没有。新的协调者被选出后,只能去询问所有参与者相关事务的状态,得到部分有指令部分无指令的回复。足以判断这个事务是已经决定要 commit 还是 abort 的,于是只需要向没有收到指令的参与者再次发送指令即可。

  • 编号 2 和 3,协调者发出 commit/abort 消息后死掉,部分参与者接收到了,部分没有。新的协调者被选出后,照例去询问所有参与者相关事务的状态。假设只有一个参与者没有回复,其他参与者都给出了自己的回复,要么全是 commit 或 abort,要么没有收到任何指令。收到回复的情况下,参与者就能确定之前的决策;但如果没有回复,参与者就无法确定之前的决策了。如果发生故障的参与者 fail-recover 了,自然就能从它那里知道状态,只是会阻塞而已。但如果发生故障的参与者 fail-dead 了,决策结果就永远丢失了,事务会永远阻塞下去,并且这个参与者可能在死前已经完成了 commit 操作,就会导致了不一致问题的产生。

过程麻烦,结论倒挺简单,当协调者和部分参与者同时 fail-dead 时,有可能导致永久阻塞,并出现数据一致性问题。

而对于网络分区,当协调者发出 commit/abort 消息后发生网络分区,部分参与者接收到了,部分没有。没有协调者的分区会选举出新的协调者。如果收到和没收到消息的参与者正好全部分散在不同的网络分区,各个协调者就会做出不同的判断,导致分区间数据不一致。

编号 过程 网络分区 一致性隐患 可能永久阻塞
1 commit/abort yes yes no

上面把 fail-dead 和网络分区两类故障分开来分析,当两者组合产生时,类似按位或的效果。

总结下,2PC 主要会产生两类三种问题:

  1. 短时阻塞问题,影响性能或短时可用性(协调者为了避免单点故障导致的长时间阻塞,通常会 standby 备节点,也可以归为此类)。

  2. 协调者和部分参与者同时 fail-dead 后,可能导致系统永久阻塞,以及产生一致性问题。

  3. 网络分区后的一致性问题。

3PC

上篇文章,正是为了解决数据一致性问题,才引出了这篇的分布式事务。好不容易设计出了 2PC,想不到又搞出了一致性问题,还可能有无法恢复的阻塞,不能被自己想解决的问题给解决掉了啊,得想办法。

仔细想想,协调者和参与者同时 fail-dead,新的协调者被选举出来后,为什么无法判断当前事务到底应该 commit 还是 abort 呢?我们定义协调者这个角色,目的就是让它有决策的能力,为什么这种情况下,却没有判断的能力了?

关键就在于上面我们给问题 「降维」时提到的这句话:协调和和参与者都会在本地持久化事务状态

正是因为事务状态在每台机器都做了本地持久化,才使得我们能保证 fail-recover 类的故障不会导致无法决策。

但在 fail-dead 的情况下,事务状态就丢失了。如果所有已经本地持久化好事务状态的机器都死了,那状态就彻底丢失了。比如上面提到的例子,协调者在发出第一个提交指令后就 fail-dead,而收到指令的那个参与者正好也 fail-dead 了,剩下再多参与者也是徒劳。

问题的源头找到了,既然是事务状态 -- 主要是由第一阶段投票结果产生的决策结果 -- 的丢失导致了这个问题,那我们就把决策结果发给所有参与者,然后才去执行真正的提交动作。这样,只要有一台机器还活着(全挂的情况需要通过多机架等节点分布方案来避免),决策结果就还在。

这就是所谓 3PC(Three Phase Commit)的思路。

在 2PC 的两个阶段中间,插入一个专门用来同步决策结果的步骤。只有这个步骤成功了,才会进入下一阶段,否则重试或 abort。

  • Can-Commit,类似 2PC 里的 Prepare 阶段。

  • Pre-Commit,新增的阶段,决策者向参与者同步决策结果。

  • Do-Commit,类似 2PC 里的 Commit 阶段。

3PC 很好地解决了 2PC 的第 2 个问题。但对第 3 个问题 -- 网络分区后的数据一致性问题依然没有办法。而 2PC 的第一个问题 -- 短时阻塞导致的性能损耗,更是同步类的方案的通病,3PC 也无能为力。

另外,2PC 在没有 standby 协调者的情况下,只要协调者故障,就会导致整个系统长时间阻塞,也因此被算作 blocking 算法。

3PC 多加的一个阶段除了解决可能的一致性问题外,也解决了阻塞问题。为了进一步缓解阻塞,参考协调者的做法,在参与者这端也引入了超时机制。在 Pre-Commit 后,如果没有收到 Do-Commit 指令,超时后会自动 Commit。

这样,3PC 虽然可以勉强称为非阻塞(noblocking,这里的阻塞指永久阻塞,不包括由于同步操作导致的短时阻塞)算法,但又增加了数据不一致的可能性。后面文章,我们会专门探讨,超时机制看似可靠实际却充满不确定性。

虽然 3PC 在算法层面比 2PC 更好,但多加的一轮消息同步,让本就不佳的性能雪上加霜;对非阻塞的追求又引入了新的不一致可能;而对网络分区也没有很好的办法。所以在现实中,并没有如预期得到比 2PC 更多的应用。

而反倒是 2PC,由于基本实现了分布式事务的目标,形成了一个叫做 XA(eXtended Architecture)的标准,被 PostgreSQL、MySQL 和 Oracle 等数据库广泛采用,并得到了各种语言和 API 的支持。

这也是理论和实践不同取舍的体现。

除了 2PC 和 3PC,还有所谓 TCC(Try-Comfirm-Cancel) 的分布式事务实现方式。

TCC 和 2PC/3PC 思想类似,只是把应用层耦合进了整个流程,这里不再赘述。

TL;DR

  • 不一致的根本,是每个副本都只掌握了局部信息,无法做出正确的决策。需要有角色能掌握全部信息。

  • 与其被动逐个解决问题,不如考虑先尝试,失败再回滚的方式,也就是事务。

  • 2PC 是分布式事务的典型实现,分为 Prepare-Commit 两个阶段,协调好了再操作。

  • 2PC 在一些情况下会有阻塞和数据一致性问题。

  • 3PC 通过多插入一轮消息来同步决策结果,解决了协调者和参与者同时挂掉时的阻塞问题。

  • 3PC 虽然缓解了阻塞,解决了一些数据不一致问题,但也牺牲了性能,并引入了新的数据不一致的可能。

  • 2PC 和 3PC 都不能提供分区容忍性。


上一篇,我们把数据一致性问题的解决办法分为了预防类和先污染后治理类。然后介绍了一种预防类的一致性解法 -- 单主同步。

这一篇,粗略介绍了分布式事务的几种实现方式,作为第二种预防类的一致性解法。

然而,这两种解法,碰到网络分区都无能为力。

而我们在介绍 CAP 时说过,网络分区是无法回避的问题。所以,下一篇,我们就一起看下,有没有什么预防类的一致性算法,是能够提供分区容忍性的。

关联阅读

漫谈分布式系统(9) -- 初探数据一致性

原创不易

关注/分享/赞赏

给我坚持的动力


转载:https://blog.csdn.net/ShadyXu/article/details/105780976
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场