17

漫谈分布式系统(十):初探分布式事务

 4 years ago
source link: https://mp.weixin.qq.com/s/kNZD0wH72_RHB5yXA9-49w
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

这是《漫谈分布式系统》系列的第 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) -- 初探数据一致性

原创不易

关注/分享/赞赏

给我坚持的动力

yEfMje3.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK