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

这是《漫谈分布式系统》系列的第 10 篇,预计会写 30 篇左右。欢迎订阅,听我娓娓道来。也欢迎转发朋友圈分享给更多人。

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

上一篇,我们对分布式系统的一致性问题有了基本的了解。详细介绍了单主同步的数据复制机制,虽然已经 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 和网络分区这两类。
top Created with Sketch.