本文作者:陈奕霖(sticnarf),PingCAP 研发工程师,TiKV Committer,热衷于开源技术,在分布式事务畛域有丰盛教训,目前致力于优化 TiDB 的分布式事务性能。
TiDB 提供了原生的分布式事务反对,实现低提早的分布式事务是继续的优化方向。TiDB 5.0 引入的 Async Commit 个性大大改善了事务提交的提早,这一个性次要由本文作者陈奕霖 (sticnarf),以及赵磊(youjiali1995),Nick Cameron(nrc) 和周振靖(MyonKeminta) 实现。
本文将向大家介绍 Async Commit 的设计思路、原理以及要害的实现细节。
Percolator 的额定提早
TiDB 事务基于 Percolator 事务模型。读者能够参考咱们之前的博客具体理解 Percolator 事务模型的提交过程。
上图是引入 Async Commit 之前的提交流程示意图。用户向 TiDB 发送 COMMIT 语句之后,TiDB 至多要经验以下的步骤能力向用户返回提交后果:
- 并发地 prewrite 所有的 keys;
- 从 PD 获取工夫戳作为 Commit TS;
- 提交 primary key。
整个提交过程的要害门路包含了至多两次 TiDB 和 TiKV 之间的网络交互。只有提交 secondary keys 的操作在 TiDB 后盾异步实现。
在 TiDB 的事务模型中,咱们能够大抵将 TiDB 节点认为是事务的协调者,而 TiKV 节点是事务的参与者。传统二阶段提交中咱们个别默认协调者的数据存储在节点本地,但 TiDB 事务模型中,所有的事务相干数据都在 TiKV 上。也正是因而,传统二阶段提交中,第一阶段完结后事务状态即可确定;而 TiDB 须要实现第二阶段的一部分,将事务状态存储到 TiKV 上,能力向用户响应。
不过这也意味着,TiDB 之前的事务模型是有晋升空间的。是否改良 Percolator 事务模型,让事务的状态在实现第一阶段后,无需额定的网络交互即可确定呢?
改良事务实现条件
引入 Async Commit 之前,事务的 primary key 被提交才意味着这个事务被提交。Async Commit 力求实现的,就是把确定事务状态的工夫提前到实现 prewrite 的时候,让整个提交的第二阶段都异步化进行。也就是说,对于 Async Commit 事务,只有事务所有的 keys 都被胜利 prewrite,就意味着事务提交胜利。
下图是 Async Commit 事务的提交流程(你可能发现原来获取 Commit TS 的环节没有了,在 prewrite 前多了从 PD 获取工夫戳作为 Min Commit TS 的操作,这里的起因会在后文中介绍):
为了达到这个指标,咱们有两个次要问题要解决:
- 如何确定所有 keys 已被 prewrite。
- 如何确定事务的 Commit TS。
如何找到事务所有的 keys
引入 Async Commit 之前,事务的状态只由 primary key 决定,所以只须要在所有 secondary key 上存储到 primary key 的指针。如果遇到未提交的 secondary key,查问 primary key 的状态即可晓得以后事务的状态:
判断 Async Commit 事务则须要晓得所有 keys 的状态,所以咱们须要能从事务的任意一个 key 登程,查问到事务的每一个 key。于是咱们做了一点小的批改,保留从 secondary key 到 primary key 指针的同时,在 primary key 的 value 外面存储到到每一个 secondary key 的指针:
Primary key 上存储了所有 secondary keys 的列表,但显然,如果一个事务蕴含的 keys 的数量特地多,咱们不可能把它们全副存到 primary key 上。所以 Async Commit 事务不能太大,以后咱们只对蕴含不超过 256 个 keys 且所有 keys 的大小总和不超过 4096 字节的事务应用 Async Commit。
过大的事务的提交时长自身较长,缩小一次网络交互带来的提早晋升不显著,所以咱们也不思考用相似多级构造的形式让 Async Commit 反对更大的事务。
如何确定事务的 Commit TS
Async Commit 事务的状态在 prewrite 实现时就必须确定了,Commit TS 作为事务状态的一部分也不例外。
默认状况下,TiDB 事务满足快照隔离的隔离级别和线性一致性。咱们心愿这些性质对于 Async Commit 事务同样可能成立,那么确定适合的 Commit TS 十分要害。
对于 Async Commit 事务的每一个 key,prewrite 时会计算并在 TiKV 记录这个 key 的 Min Commit TS,事务所有 keys 的 Min Commit TS 的最大值即为这个事务的 Commit TS。
下文会介绍 Min Commit TS 的计算形式,以及它们是如何使 Async Commit 事务满足快照隔离和线性一致性的。
保障快照隔离
TiDB 通过 MVCC 实现快照隔离。事务在开始时会向 TSO 获取 Start TS,为实现快照隔离,咱们要保障以 Start TS 作为快照工夫戳始终能读取到一个统一的快照。
为此,TiDB 的每一次快照读都会更新 TiKV 上的 Max TS1。Prewrite 时,Min Commit TS 会被要求至多比以后的 Max TS 大2,也就是比所有先前的快照读的工夫戳大,所以能够取 Max TS + 1 作为 Min Commit TS。在这个 Async Commit 事务提交胜利后,因为其 Commit TS 比之前的快照读的工夫戳大,所以不会毁坏快照隔离。
上面的例子中,事务 T1 要写 x 和 y 两个 keys。T2 读取 y 将 Max TS 更新到 5,所以接下来 T1 prewrite y 时,Min Commit TS 至多为 6。T1 prewrite y 胜利即意味着 T1 提交胜利,而 T1 的 Commit TS 至多为 6。所以之后 T2 再读取 y 时,不会读取到 T1 更新的值,事务 T2 的快照放弃了统一。
T1: Begin (Start TS = 1) | |
---|---|
T1: Prewrite(x) | T2: Begin (Start TS = 5) |
T2: Read(y) => Max TS = 5 | |
T1: Prewrite(y) => Min Commit TS = 6 | |
T2: Read(y) |
保障线性一致性
线性一致性实际上有两方面的要求:
- 循序性(sequential)
- 实时性(real-time)
实时性要求在事务提交胜利后,事务的批改立即就能被新事务读取到。新事务的快照工夫戳是向 PD 上的 TSO 获取的,这要求 Commit TS 不能太大,最大不能超过 TSO 调配的最大工夫戳 + 1。
在快照隔离一节提到,Min Commit TS 的一个可能的取值是 Max TS + 1。用于更新 Max TS 的工夫戳都来自于 TSO,所以 Max TS + 1 必然小于等于 TSO 上未调配的最小工夫戳。除了 TiKV 上的 Max TS 之外,协调者 TiDB 也会提供 Min Commit TS 的束缚(前面会提到),但也不会使其超过 TSO 上未调配的最小工夫戳。
循序性要求逻辑上产生的程序不能违反物理上的先后顺序。具体地说,有两个事务 T1 和 T2,如果在 T1 提交后,T2 才开始提交,那么逻辑上 T1 的提交就应该产生在 T2 之前,也就是说 T1 的 Commit TS 应该小于 T2 的 Commit TS。3
为了保障这个个性,TiDB 会在 prewrite 之前向 PD TSO 获取一个工夫戳作为 Min Commit TS 的最小束缚。因为后面实时性的保障,T2 在 prewrite 前获取的这个工夫戳必然大于等于 T1 的 Commit TS,而这个工夫戳也不会用于更新 Max TS,所以也不可能产生等于的状况。综上咱们能够保障 T2 的 Commit TS 大于 T1 的 Commit TS,即满足了循序性的要求。
综上所述,每个 key 的 Min Commit TS 取 prewrite 时的 Max TS + 1 和 prewrite 前从 PD 获取的工夫戳的最大值,事务的 Commit TS 取所有 key 的 Min Commit TS 的最大值,就可能同时保障快照隔离和线性一致性。
一阶段提交 (1PC)
如果一个事务只更新一条记录的非惟一索引,或是只插入一条没有二级索引的记录,它只会波及到单个 Region。在这种只波及一个 Region 的场景下,是不是能够不应用分布式事务提交协定,只用一个阶段实现事务的提交?这当然是可行的,但艰难就在于一阶段提交的事务的 Commit TS 如何确定。
有了 Async Commit 计算 Commit TS 的根底,一阶段提交实现的艰难点也解决了。咱们用和 Async Commit 雷同的形式去计算出一阶段提交事务的 Commit TS,通过一次和 TiKV 的交互间接将事务提交即可:
一阶段提交没有应用分布式提交协定,缩小了写 TiKV 的次数。所以如果事务只波及一个 Region,应用一阶段提交不仅能够升高事务提早,还能够晋升吞吐。4
一阶段提交个性在 TiDB 5.0 中作为 Async Commit 的一部分被引入。
因果一致性
上文提到向 TSO 获取 Min Commit TS 能够保障循序性。那么如果把这一步省去会怎么?这样不就又省了一次 PD 和 TiDB 的网络交互延时吗?
然而在这种状况下,咱们能够找到违反循序性的例子。假如 x 和 y 位于不同的 TiKV 节点上,事务 T1 会批改 x,事务 T2 会批改 y。T1 比 T2 开始得早,但用户在 T2 在提交胜利后才告诉 T1 提交。这样,对于用户来说,事务 T1 的提交产生在事务 T2 提交实现之后,如果满足循序性,逻辑上 T1 应该晚于 T2 提交。
如果省去了 prewrite 前获取 Min Commit TS 的操作,T1 的 Commit TS 可能为 2,小于 T2 的 Commit TS = 6。如果有一个 Start TS 为 3 的事务 T3,它能够察看到 T2 在逻辑上晚于 T1 的事实。所以此时是没有线性一致性的。
T1: Begin (Start TS = 1) | ||
---|---|---|
T3: Begin (Start TS = 3) | ||
T2: Begin (Start TS = 5) | ||
T2: Prewrite(y)Min Commit TS = 6 | ||
告诉 T1 提交 | ||
T1: Prewrite(x)Min Commit TS = 2 | ||
T3: Read(x, y) |
此时,快照的概念也可能和预期的不太一样。上面的例子中,晚开始的 T2 告诉事务 T1 提交,T1 的 Commit TS 可能会小于 T2 的 Start TS。
对于用户来说,T2 在后续读取到 T1 对 x 的批改是不合乎预期的。这种情景下,可反复读的性质没有被毁坏,但是否还合乎快照隔离就存在争议了5。
T1: Begin (Start TS = 1) | |
---|---|
T2: Begin (Start TS = 5) | |
T2: Read(y) | |
告诉 T1 提交 | |
T1: Prewrite(x)Min Commit TS = 2 | |
T2: Read(x) |
咱们将这样更弱的一致性称为因果一致性:有因果关系的事务的程序和它们物理上提交的程序统一,但没有因果关系的事务之间的提交程序则是不确定的。当且仅当两个事务加锁或写入的数据有交加时,咱们认为它们有因果关系。事实上,这里的因果关系只蕴含数据库可知的因果关系,不波及下面例子中“应用层告诉”这种内部的因果关系。
产生这样的异样场景的条件比拟刻薄,所以咱们给用户提供了省去获取 Min Commit TS 的形式:应用 START TRANSACTION WITH CAUSAL CONSISTENCY ONLY 开启的事务,在提交时不获取 Min Commit TS。如果你的应用场景里,不波及下面这种在数据库内部管制两个同时运行的事务提交程序的状况,能够尝试将一致性级别升高,缩小一次 TiDB 从 PD TSO 获取工夫戳的耗时。
性能晋升
Async Commit 使事务实现的工夫点提前到 prewrite 完结时,使提交 primary key 的操作异步化。提交 primary key 这一步操作在整个事务中耗时的占比越大,那 Async Commit 的晋升就越显著。交互少的小事务通常能依附 Async Commit 失去较大的晋升。
反之,也有一些 Async Commit 晋升不显著的场景:
- 蕴含很多条语句,有较长的交互逻辑的事务,事务提交的耗时占比拟低,Async Commit 的晋升则不会很显著。
- 蕴含 keys 较多,写入数据量较大的事务,prewrite 的耗时显著长于提交 primary key 的耗时,Async Commit 的晋升也不会很显著。
- Async Commit 没有缩小对 TiKV 的读写量,所以不能晋升极限吞吐。所以如果零碎自身已靠近吞吐极限,Async Commit 不会带来显著晋升。
Sysbench oltp_update_index
场景下,一个事务只写入行记录和索引两个 keys,同时也是没有额定交互的 auto commit 事务,所以实践上 Async Commit 能大幅升高其延时。
理论测试也能证实这一点。如上图所示,在固定 2000 TPS 的条件下测试 sysbench oltp_update_index
,开启 Async Commit 后,均匀延时升高了 42%,p99 延时升高了 32%。
如果事务只波及一个 Region,一阶段提交的优化可能更加显著地升高事务提交的提早。因为缩小了 TiKV 的写入量,所以也能够晋升极限吞吐。
如上图所示,在固定 2000 TPS 的条件下测试 sysbench oltp_update_non_index
。这是一个一个事务只写入一个 Region 的场景,开启一阶段提交后,均匀延时升高了 46%,p99 延时升高了 35%。
总结
Async Commit 让 TiDB 事务提交缩小了一次写 TiKV 的延时,是对原先 Percolator 事务模型的一个较大的改良。新创建的 TiDB 5.0 集群默认启用 Async Commit 和一阶段提交。从旧版本升级到 5.0 的集群,则须要用户手动将全局零碎变量 tidb_enable_async_commit
和 tidb_enable_1pc
设为 ON 来开启 Async Commit 和一阶段提交个性。
限于篇幅,本文只波及到了 Async Commit 中的要害设计,感兴趣的读者能够浏览 Async Commit 的设计文档理解更多的细节。将来咱们也会继续改良 TiDB 事务的性能,改善大家的 TiDB 应用体验,让更多人能从 TiDB 中收益。
欢送分割咱们:
Transaction SIG 的主要职责是对 TiKV 分布式事务的将来倒退进行探讨和布局,并组织社区成员进行相干开发和保护。
当初你们能够在 TiKV 社区 Slack 的 #sig-transaction channel 找到咱们。
从 TiDB 4.0 公布以来总计有 538 位 Contributor 提交了 12513 个 PR 帮忙咱们一起实现企业级外围场景的里程碑版本的开发,Async Commit 只是这些 PR 的代表。为感激所有参加 5.0 版本的 Contributor 们,TiDB 社区精心筹备了一份 5.0 定制周边。如果你也是 5.0 的 Contributor,请在 5 月 5 日前填写表单,通知咱们你的地址。
正文
1 为了保障在 Region Leader 迁徙后,新 Leader 的 Max TS 足够大,在 Region Leader 迁徙和 Region Merge 后,TiKV 也会从 PD 获取最新的工夫戳更新 Max TS。. ↩
2 在 prewrite 过程中,为了避免更新的快照读毁坏这个束缚,TiKV 会对 prewrite 的 key 加上内存锁,短暂地阻塞住 Start TS ≥ Min Commit TS 的读申请。. ↩
3 如果 T1 和 T2 的提交过程在工夫上有重叠,那么它们逻辑上的提交的先后顺序则是无奈确定的。. ↩
4 精确地说,一阶段提交只利用于通过单次写 TiKV 申请就能实现事务的状况。为了晋升提交效率,较大的事务会被切分成很多个申请,此时就算它们波及的都是同一个 Region,目前也不会应用一阶段提交。. ↩
5 如果咱们容许认为 T1 在逻辑上的提交工夫早于 T2 开始的工夫(因为不满足线性一致性),那么这种状况仍然能够认为是满足快照隔离的。. ↩