内容简介:这是《漫谈分布式系统》系列的第 10 篇,预计会写 30 篇左右。每篇文末有为懒人准备的 TL;DR,还有给勤奋者的关联阅读。扫描文末二维码,关注公众号,听我娓娓道来。也欢迎转发朋友圈分享给更多人。上一篇,我们对分布式系统的一致性问题有了基本的了解。详细介绍了单主同步的数据复制机制,虽然已经 best effort guarantee,但仍然不能保证想要的强一致性。
这是《漫谈分布式系统》系列的第 10 篇,预计会写 30 篇左右。每篇文末有为懒人准备的 TL;DR,还有给勤奋者的关联阅读。扫描文末二维码,关注公众号,听我娓娓道来。也欢迎转发朋友圈分享给更多人。
无心插柳,用分布式事务来解决数据一致性
上一篇,我们对分布式系统的一致性问题有了基本的了解。详细介绍了单主同步的数据复制机制,虽然已经 best effort guarantee,但仍然不能保证想要的强一致性。
然后通过对 data replication 的本质分析,发现了用事务解决数据一致性问题的可能。
虽然事务可以用来解决数据复制过程中的一致性问题,但事务的初衷却不是这个,至少不只是这样。所以,我们有必要好好了解下分布式事务。
不一致的根源
副本上的数据会不一致,是因为机器、网络故障等原因,导致要么副本间不知道彼此不一样,要么知道不一样但是解决不了。
本质上,是因为 每个副本都只掌握了局部信息,无法做出正确的决策。
对症下药,如果每个副本都能知道全部的信息,就能做出正确的决策了。
但这样带来的信息同步消耗,以及更多更复杂的流程带来更高的出错可能,事实上就不可行了。
折中下,不用所有副本,只要有一个副本(把这部分功能抽离出来并赋予新的角色)掌握所有信息,再让它去协调其他副本。
这个思想,其实在单 master 多 slave 的架构上已经有体现,master 作为特殊的副本,就已经充当了协调者的功能,只不过是分别独立地向其他副本做数据复制。而现在需要把这些各自独立的协调工作合并起来考虑。
解决故障的另一种思路
一个复杂的分布式系统,可能出故障的地方实在太多了:
-
服务器可能宕机,还要分成重启就能恢复和永远恢复不了。
-
网络故障,还要分成偶然抖动和长期故障。
-
服务故障,还要分成不同角色是否同时故障。
-
......
我们还需要为每种可能的故障,去设计应对机制,以副本问题为例:
-
为了应对故障,必须要有副本。
-
为了应对 IDC 级别的故障,副本必须分布在不同的机房。
-
为了应对交换机级别的故障,不能把所有副本都放在同一个交换机。
-
...
这样不仅很难覆盖所有可能的故障,势必还会导致系统设计和实现越来越复杂,反过来影响系统的可靠性。
能不能换个思路,我知道故障可能发生,并且会有各种不同的故障,甚至有时候都没法判断当前发生的是什么故障。
但是我不想再这么细粒度的去处理这个问题,我就大老粗一点,先去操作着,能成功最好;如果失败了,不管因为什么原因失败,恢复现场,待会再重新操作一遍。
2PC
上面两个思想结合起来,就有了所谓 2PC(Two Phase Commit)。2PC 也是分布式事务的典型实现方式之一。
给协调的角色一个新的名字叫协调者(Coordinator),其他参与角色就叫参与者( Participant)。
协调者作为核心,掌握全局信息,拥有决策的能力和权利。参与者只用关注自己,安心的干活。
之所以叫 2PC,正是因为整个事务提交被分成了两个阶段:准备(Prepare)和提交(Commit)。
主要执行流程如下:
-
协调者收到应用端提过来的事务请求后,向所有参与者发送 Prepare 指令。
-
参与者在本地做好保证事务一定能成功的准备工作,如获取锁等,并记录 redo log 和 undo log,以便重做或回滚(类似单机事务)。如果能满足,则返回 Yes 给协调者,否则返回 No。
-
协调者收到所有参与者的回复后,汇总检查并记录在本地事务日志中,如果所有回复都是 Yes,则向所有参与者发送 Commit 指令,否则发送 Abort 指令。
-
参与者接收到协调者的指令,如果是 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 主要会产生两类三种问题:
-
短时阻塞问题,影响性能或短时可用性(协调者为了避免单点故障导致的长时间阻塞,通常会 standby 备节点,也可以归为此类)。
-
协调者和部分参与者同时 fail-dead 后,可能导致系统永久阻塞,以及产生一致性问题。
-
网络分区后的一致性问题。
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 时说过,网络分区是无法回避的问题。所以,下一篇,我们就一起看下,有没有什么预防类的一致性算法,是能够提供分区容忍性的。
关联阅读
原创不易
关注/分享/赞赏
给我坚持的动力
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 漫谈分布式系统(十):初探分布式事务
- 漫谈分布式系统(二十三):分布式数据仓库
- 漫谈分布式系统(十一):达成共识就是一致
- 漫谈分布式系统(十一):达成共识就是一致
- 漫谈spring cloud分布式服务架构
- 漫谈分布式系统 (20):基于规则的优化
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Effective JavaScript
赫尔曼 (David Herman) / 黄博文、喻杨 / 机械工业出版社 / 2014-1-1 / CNY 49.00
Effective 系列丛书经典著作,亚马逊五星级畅销书,Ecma 的JavaScript 标准化委员会著名专家撰写,JavaScript 语言之父、Mozilla CTO —— Brendan Eich 作序鼎力推荐!作者凭借多年标准化委员会工作和实践经验,深刻辨析JavaScript 的内部运作机制、特性、陷阱和编程最佳实践,将它们高度浓缩为极具实践指导意义的 68 条精华建议。 本书共......一起来看看 《Effective JavaScript》 这本书的介绍吧!