使用2PC可保证分布式事务的原子性,但2PC性能究竟如何呢?

1 到底多慢?

看一组数据。2013年MySQL技术大会(Percona Live MySQL C&E 2013),Randy Wigginton等在“Distributed Transactions in MySQL”演讲中公布一组XA事务与单机事务对比数据的折线图:

想要瞬间完成任务?原子性来帮你打破高延迟魔咒!_SQL

  • 横坐标:并发线程数量
  • 纵坐标:事务延迟,ms
  • 蓝色折线:单机事务
  • 红色的折线式:跨两个节点的XA事务

无论并发数量,XA事务延迟时间总在单机事务10倍以上,真是巨大性能差距,所以演讲最终建议“不使用分布式事务”。

如今任何计划使用分布式数据库的企业,都不可能接受10倍单体数据库的事务延迟。若仍存在这样大差距,分布式数据库必然无法生存,所以它们一定得做优化。

本文2PC都指基于Percolator优化的改进型。

2 事务延迟估算

整个2PC事务延迟由两阶段组成,公式表达:

(1)Ltxn=Lprep+Lcommit

L即各阶段的延迟。

准备阶段是事务操作的主体,包含若干读/写操作。

读操作次数(2)R=读操作次数

读操作平均延迟(3)Lr=读操作平均延迟

写操作次数(4)W=写操作次数

写操作平均延迟(5)Lw=写操作平均延迟

整个准备阶段的延迟(6)整个准备阶段的延迟=Lprep=R∗Lr+W∗Lw

不同产品架构,读操作成本不一。最乐观的情况CockroachDB,因采用P2P架构,每个节点:

  • 既承担客户端服务接入的工作
  • 也有请求处理和数据存储的职能

最理想情况,读操作的客户端接入节点,同时是当前事务所访问数据的Leader节点,所有读取都是本地操作。

磁盘操作相对网络延迟来说极短,所以可忽略读取时间。准备阶段的延迟主要由写操作决定:

(7)Lprep=W∗Lw

分布式数据库的写不是简单的本地操作,而是使用共识算法同时在多个节点写数据。所以,一次写操作延迟等于一轮共识算法开销,

一轮共识算法的用时(8)Lc=一轮共识算法的用时

可得:

(9)Lprep=W∗Lc

再看第二阶段,提交阶段,Percolator模型的提交阶段只需写入一次数据,修改整个事务的状态。对于CockroachDB,这个事务标识可保存在本地。提交操作的延迟也是一轮共识算法,即:

(10)Lcommit=Lc

分别得到两个阶段的延迟后,带入最开始的公式,可以得到:

(11)Ltxn=(W+1)∗Lc

小明给小红转500元。

想要瞬间完成任务?原子性来帮你打破高延迟魔咒!_客户端_02

在这个转账事务中,包含两条写操作SQL,分别是扣减小明账户余额和增加小红账户余额,W等于2。再加上提交操作,一共有3个$L_{c}$。我们可以看到,这个公式里事务的延迟是与写操作SQL的数量线性相关的,而真实场景中通常都会包含多个写操作,那事务延迟肯定不能让人满意。

3 优化方法

缓存写提交(Buffering Writes until Commit)

怎么缩短写操作的延迟呢?

第一个办法是将所有写操作缓存起来,直到commit语句时一起执行,这种方式称为Buffering Writes until Commit,我把它翻译为“缓存写提交”。而TiDB的事务处理中就采用这种方式,TiDB官网的交互图。

想要瞬间完成任务?原子性来帮你打破高延迟魔咒!_SQL_03

所有从Client端提交的SQL首先会缓存在TiDB节点,只有当客户端发起Commit时,TiDB节点才会启动两阶段提交,将SQL被转换为TiKV的操作。这样,显然可以压缩第一阶段的延迟,把多个写操作SQL压缩到大约一轮共识算法的时间。那么整个事务延迟就是:

(12)Ltxn=2∗Lc

但缓存写提交存在两个明显的缺点。

首先是在客户端发送Commit前,SQL要被缓存起来,如果某个业务场景同时存在长事务和海量并发的特点,那么这个缓存就可能被撑爆或者成为瓶颈。

其次是客户端看到的SQL交互过程发生了变化,在MySQL中如果出现事务竞争,判断优先级的规则是First Write Win,也就是对同一条记录先执行写操作的事务获胜。而TiDB因为缓存了所有写SQL,所以就变成了First Commit Win,也就是先提交的事务获胜。我们用一个具体的例子来演示这两种情况。

想要瞬间完成任务?原子性来帮你打破高延迟魔咒!_缓存_04

在MySQL中同时执行T1,T2两个事务,T1先执行了update,所以获得优先权成功提交。而T2被阻塞,等待T1提交后才完成提交。

想要瞬间完成任务?原子性来帮你打破高延迟魔咒!_缓存_05

在TiDB中执行同样的T1、T2,虽然T2晚于T1执行update,但却先执行了commit,所以T2获胜,T1失败。

First Write Win与First Commit Win在交互上是显然不同的,这虽然不是大问题,但对于开发者来说,还是有一定影响的。可以说,TiDB的“缓存写提交”方式已经不是完全意义上的交互事务了。

管道(Pipeline)

有没有一种方法,既能缩短延迟,又能保持交互事务的特点呢?还真有。这就是CockroachDB采用的方式,称为Pipeline。具体过程就是在准备阶段是按照顺序将SQL转换为K/V操作并执行,但是并不等待返回结果,直接执行下一个K/V操作。

这样,准备阶段的延迟,等于最慢的一个写操作延迟,也就是一轮共识算法的开销,所以整体延迟同样是:

$$L{prep} = L{c}$$

那么,加上提交阶段的一轮共识算法开销:

$$L{txn} = 2 * L{c}$$

我们再回到小明转账的例子来看一下。

想要瞬间完成任务?原子性来帮你打破高延迟魔咒!_SQL_06

同样的操作,按照Pipeline方式,增加小红账户余额时并不等待小明扣减账户的动作结束,两条SQL的执行时间约等于1个$L{c}$。加上提交阶段的1个$L{c}$,一共是2个$L_{c}$,并且延迟也不再随着SQL数量增加而延长。

2个$L_{c}$是多久呢?我们带入真实场景,来计算一下 。

首先,我们评估一下期望值。对于联机交易来说,延迟通常不超过1秒,如果用户体验良好,则要控制在500毫秒以内。其中留给数据库的处理时间不会超过一半,也就是250-500毫秒。这样推算,$L_{c}$应该控制在125-250毫秒之间。

再来看看真实的网络环境。我们知道人类现有的科技水平是不能超越光速的,这个光速是指光在真空中的传播速度,大约是30万千米每秒。而光纤由于传播介质不同和折线传播的关系,传输速度会降低30%,大致是20万千米每秒。但是,这仍然是一个比较理想的速度,因为还要考虑网络上的各种设备、协议处理、丢包重传等等情况,实际的网络延迟还要长很多。

为了让你有一个更直观的感受。我这里引用了论文“Highly Available Transactions: Virtues and Limitations“中的一些数据,这篇论文发表在VLDB2014上,在部分章节中初步探讨了系统全球化部署面临的延迟问题。论文作者在亚马逊EC2上,使用Ping包的方式进行了实验,并统计了一周时间内7个不同地区机房之间的RTT(Round-Rip Time,往返延迟)数据。

简单来说,RTT就是数据在两个节点之间往返一次的耗时。在讨论网络延迟的时候,为了避免歧义,我们通常使用RTT这个概念。

想要瞬间完成任务?原子性来帮你打破高延迟魔咒!_客户端_07

实验中,地理跨度较大两个机房是巴西圣保罗和新加坡,两地之间的理论RTT是106.7毫秒(使用光速测算),而实际测试的RTT均值为362.8毫秒,P95(95%)RTT均值为649毫秒。将649毫秒代入公式,那$L_{txn}$就是接近1.3秒,这显然太长了。而考虑到共识算法的数据包更大,这个延迟还会更长。

并行提交(Parallel Commits)

但是,像CockroachDB、YugabyteDB这样分布式数据库,它们的目标就是全球化部署,所以还要努力去压缩事务延迟。

可是,还能怎么压缩呢?准备阶段的操作已经压缩到极限了,commit这个动作也不能少呀,那就只有一个办法,让这两个动作并行执行。

在优化前的处理流程中,CockroachDB会记录事务的提交状态:

1TransactionRecord{2    Status: COMMITTED,3    ...4}

并行执行的过程是这样的。

准备阶段的操作,在CockroachDB中被称为意向写。这个并行执行就是在执行意向写的同时,就写入事务标志,当然这个时候不能确定事务是否提交成功的,所以要引入一个新的状态“Staging”,表示事务正在进行。那么这个记录事务状态的落盘操作和意向写大致是同步发生的,所以只有一轮共识算法开销。事务表中写入的内容是类似这样的:

1TransactionRecord{2    Status: STAGING,3    Writes: []Key{"A", "C", ...},4    ...5}

Writes部分是意向写的Key。这是留给异步进程的线索,通过这些Key是否写成功,可以倒推出事务是否提交成功。

而客户端得到所有意向写的成功反馈后,可以直接返回调用方事务提交成功。注意!这个地方就是关键了,客户端只在当前进程内判断事务提交成功后,不维护事务状态,而直接返回调用方;事后由异步线程根据事务表中的线索,再次确认事务的状态,并落盘维护状态记录。这样事务操作中就减少了一轮共识算法开销。

想要瞬间完成任务?原子性来帮你打破高延迟魔咒!_SQL_08

你有没有发现,并行提交的优化思路其实和Percolator很相似,那就是不要纠结于在一次事务中搞定所有事情,可以只做最少的工作,留下必要的线索,就可以达到极致的速度。而后续的异步进程,只要根据线索,完成收尾工作就可以了。

4 总结

  1. 高延迟一直是分布式事务的痛点。在一些测试案例中,MySQL多节点的XA事务延迟甚至达到单机事务的10倍。按照2PC协议的处理过程,分布式事务延迟与事务内写操作SQL语句数量直接相关。延迟时间可以用公式表达为$L{txn} = (W + 1) * L{c}$ 。
  2. 使用缓存写提交方式优化,可以缩短准备阶段的延迟,$L{txn} = 2 * L{c}$。但这种方式与事务并发控制技术直接相关,仅在乐观锁时适用,TiDB使用了这种方式。但是,一旦将并发控制改为悲观协议,事务延迟又会上升。
  3. 通过管道方式优化,整体事务延迟可以降到两轮共识算法开销,并且在悲观协议下也适用。
  4. 使用并行提交,可以进一步将整体延迟压缩到一轮共识算法开销。CockroachDB使用了管道和并行提交这两种优化手段。

今天我们分析了分布式事务高延迟的原因和一些优化的手段,理想的情况下,事务延迟可以缩小到一轮共识算法开销。你看,是不是对分布式数据库更有信心了。当然,在测算事务延迟时我们还是预设了一些前提,比如读操作成本趋近于零,这仅在特定情况下对CockroachDB适用,很多时候是不能忽略的,其他产品则更是不能无视这个成本。那么,在全球化部署下,执行读操作时,如何获得满意延迟呢?