Design Data-Intensive Applications 读书笔记二十九 第九章:分布式事务,2PC

分布式事务和一致性

一致性是分布式计算中最重要的一个问题。通俗来说,它的目标就是让多个节点对于某件事达成共识。这并不容易。

有几个重要场景,需要节点达成一致:

主节点选举:在单主节点备份中,所有的节点需要关于谁是主节点达成一致。节点的领导权可能因为网络问题而产生竞争。这种情况下,一致性对于避免错误的故障转移导致脑裂(两个节点都认为自己是主节点)的场景很重要。如果有两个主节点,它们都会接受写入,它们的数据会产生分歧,导致不一致和数据丢失。

原子提交:在支持事务的多节点或者多分区的数据库中,我们要面对一个问题:一个事务可能在一些节点上成功,但是在其他节点上失败。如果我们想维护事务的原子性,我们需要所有节点对于事务的结果达成一致:要么都丢弃,回滚,要么都提交了。这就是原子提交问题。

 

原子提交和两步提交(2PC)

第7章讨论了事务的原子性,它能保证在做多个写入的时候结果不会是中间状态,事务的结果要么是全部成功,要么是全部失败,不会有几个写入成功而其他写入失败这种中间状态。

 

从单一节点到分布式原子提交

在单节点数据库上执行事务,原子性是由存储引擎实现的。当客户端向数据库提交事务时,数据库首先将事务持久化(一般是提前写入日志),然后是将提交记录插入到日志中。如果数据库中途故障了,那么节点重启时从日志中恢复事务:如果提交记录显示在故障前已经写入成功,那么事务被认为是已经提交;如果不是,事务的任何写入都会被回滚。

因此,在单一节点上,事务提交严格依赖磁盘上数据的写入顺序:首先是数据,然后是提交记录。决定事务提交成功与否的关键瞬间就是磁盘完成写入提交记录的瞬间:在那个时间点之前,有可能丢弃,但是在那个时间点之后,事务就是提交完成了(即便是数据库故障)。因此,单一设备保证事务的原子性。

但是,如果事务涉及到多个节点?例如,在一个分区过的数据库上执行多对象事务,或者是术语分区第二索引(索引项可能在不同节点上)。绝大多数“NoSQL”数据存储都不支持分布式索引,但是很多聚簇关系系统支持。这种情况下,只是简单地将请求发送到所有的节点上让各节点独立执行是不够的。如果这样做,很容易出现一些节点提交成功但是其他节点提交失败的情况:

1、一些节点可能检测到不满足某些约束条件或者冲突,因而丢弃事务;其他节点可能成功提交。

2、一些请求可能因为网络问题而丢失,最终因为超时而丢弃事务,其他提交请求则能成功。

3、一些节点可能在写入提交记录前发生故障,因而发生回滚,其他节点能成功提交。

如果一些节点提交事务而其他节点废弃了,那么节点间无法保持一致。而且一旦事务提交在一个节点上提交成功,也不能因为事务在其他节点上丢弃而一再回退。因此一个节点必须确保其他所有节点都将提交事务的时候才能提交。

一个事务的提交必须是不可改变的,不允许提交后又反过来丢弃事务。因为一旦事务提交后,它对于其他事务就是可见的,其他客户端可能依赖之前提交的数据。如果允许事务提交后丢弃,那么任何读取过提交的数据的事务都要回退。

 

两步提交

两步提交(2PC)的基本流程如图9-9

Design Data-Intensive Applications 读书笔记二十九 第九章:分布式事务,2PC

2PC使用了一个在单节点事务中没有的组件:协调者(事务管理员)。协调者可以是一个依赖库内置的进程,也可以是一个独立的进程或者服务。一个2PC事务以应用在多个节点上读取和写入数据开始。我们称数据库节点参与进事务中。当应用准备提交,协调者开始第一步:给每个节点发送准备请求,询问他们是否准备提交事务。然后协调者追踪参与者的回复:

1、如果所有的参与者回复“yes”,表明他们准备提交,然后协调者第二步发出提交请求,然后才进行提交。

2、如果有一个参与者回复“no”,协调者第二步发出丢弃请求。

 

承诺系统

现在仔细来看看2PC是怎么工作的:

1、当应用想开始分布式事务时,它需要向协调者请求全局唯一的事务id。

2、每个参与者开始各自的,附加事务id的单节点事务。所有的读写都在这一个事务内完成。如果这步里有任何事情出错,协调者或者任意参与者可以丢弃事务。

3、当应用准备提交,协调者给所有参与者发送准备请求,并附上事务id。如果有一个请求失败或者超时,协调者给所有参与者发送附加事务id的丢弃请求。

4、当一个参与者就收到准备请求,它必须确保它能确实在任何环境下提交事务。这包括将所有的事务数据写进磁盘(故障,停电,磁盘空间不足都不是后续拒绝提交事务的借口),然后检测任何冲突或者限制。向协调者回复“yes”表示节点承诺如果有请求,就会提交事务。换句话说,参与者交出了丢弃事务的权利,并没有实际提交事务。

5、当参与者收到所有参与者的准备请求的回复,它要做出判断是否提交或者丢弃事务(只有所有参与者回复“yes”时才提交)。协调者必须将决定写进磁盘上的事务日志,以防后续发生故障,这称为提交点。

6、一旦协调者的决定写进磁盘,提交或者丢弃请求就会被发送至所有参与者。如果请求失败或者超时,协调者必须不断重试直至成功。这没有回头路:如果决定提交,那么必须强制执行决定,不论要重试多少次。如果此时参与者发送故障,事务必须在它恢复后提交,因为参与者投票“yes”,在恢复后也不能拒绝。

这个机制存在两个无法回头的关键时间点:当一个参与者投票“yes”,它承诺后续一定会提交(尽管协调者可能选择丢弃);一旦协调者做出决定,决定就是不可逆的。这些承诺确保2PC的原子性。(单节点原子性提交将两个事件合并了:将提交记录写入到事务日志)。

 

协调者故障

我们已经讨论了如果参与者发生网络故障会怎么处理:如果是在准备阶段请求失败或者超时,协调者就会丢弃事务;如果是提交请求或者丢弃请求失败,协调者就会不断重试。那么如果协调者发生网络问题会怎么办?

如果协调者在准备阶段前发生网络故障,那么参与者可以丢弃事务。但是一旦参与者接收到准备请求并且投票“yes”,那么就不能单方面地丢弃,必须等待协调者决定提交或者丢弃。如果这时协调者故障或者发生网络问题,参与者只能等待,不能做其他事。这时,参与者的状态称为存疑或者未定。

如图9-10,协调者决定提交,数据库2收到了提交请求。但是其在向数据库1发送提交请求是发送故障,数据库1不知道是否提交或者丢弃。超时机制在这里也无用:如果数据库1单方面因为超时而废弃,它就会与数据库2不一致,类似的,也不能提交,因为不知道其他参与者是否丢弃。

 

如果没法从协调者收到回复,参与者就没法知道是否要提交或者丢弃。理论上参与者可以从其他参与者那里获得投票结果,但这超处理2PC的范围。完成2PC的唯一方法就是等待协调者恢复。这也就是为什么协调者在发送提交或者丢弃请求前需要将决定写入事务日志,在回复后可以决定所有存疑事务的状态。任何没有提交或者丢弃的事务在日志中都有记录。所以,协调者决定了单节点提交的原子性。

Design Data-Intensive Applications 读书笔记二十九 第九章:分布式事务,2PC

 

三步提交

两步提交称为阻塞式原子提交协议,因为如果协调者故障,这个流程就停滞了。理论上有非阻塞式的原子提交协议,但是并不实用。

作为替代,有一个3步提交算法(3PC)。但是3PC认为网络延迟是有界的,但是实际中绝大部分系统都面临*延迟和进程停顿,所以3PC不能保证原子性。一般而言,非阻塞式原子提交需要一个完美的故障检测者来检测节点是否故障。但是因为网络存在*延迟,使用超时机制,可能会将一个实际上并未故障的节点判定为故障,所以不存在一个可靠的检测者。因此尽管2PC存在问题,还是继续使用2PC。