CockroachDB-两阶段提交

我对percolator的了解并不多,我对它的印象是它可以在一个常见的分布式KV存储上实现交易能力,但是2PC + raft进程的开销是相当大的。前几天,我听说cockroachdb与percolator相比做了一些工程优化,所以我想学习如何实现这一部分。

与percolator一样,crdb也根据去中心化的事务管理器实现了多行事务管理。难点在于每个行操作都可能有并发的竞争条件。事务提交协议使用一台机器的原子事务能力来仲裁并发操作,以实现原子的多行事务提交,并且还使用一台机器的原子事务来促进不同阶段的异常恢复。

与percolator的不同之处在于percolator基于现有的低级通用KV存储,允许事务元信息嵌入到公共KV中,而crdb可以设计得更极端,为事务元信息做一些自己的存储形式。

本文大致分为以下几部分。

1、事务元信息和常规事务流程
2、并发控制
3、并行提交优化
4、TransactionRecord和WriteIntent
CRDB将数据分割成多个范围,类似于tidb中的区域,每个范围大小约为64mb,每个范围可以在内部执行一个事务。

在每个范围内,crdb分配单独的区域来保存TransactionRecord和WriteIntent,它们是与事务相关的主要元信息。

TransactionRecord将用于保持事务的状态(PENDING、COMMITTED、ABORTED),并有一个超时。
WriteIntent相当于一个写锁和一个新数据的暂存区。同一个Key最多只能有一个WriteIntent同时存在,用于悲观互斥,事务中的每个WriteIntent都指向同一个TransactionRecord。
回想一下,percolator选择事务中写入的第一行作为Primary row,然后使其他Rows指向Primary row,其他key的并发操作与Primary row对齐,事务的整体状态由保存在Primary row中的提交状态决定。

事务的第一行也被选择在crdb中扮演一个特殊的角色,TransactionRecord被保存在与第一行相同的范围内,这样事务中的所有WriteIntent都指向TransactionRecord。乍一看,这就是它与过滤器的不同之处。

协调器节点是用户访问crdb集群时连接到的节点,它使自己成为推动事务处理向前的协调器。

一旦所有的键都在一个阶段中被写入,协调器驱动提交阶段,这是一个两阶段的提交:事务记录被标记为COMMITTED,它作为提交点向用户返回一个成功的提交,协调器驱动写意图的异步清理。如果在此期间有用户访问WriteIntent,则查询Transaction Record的状态,并执行清理收敛操作。

最终WriteIntent和TransactionRecord都被清除,事务完成。

与percolator不同,crdb作为一个Serializable隔离级别,对于整个事务只有一个提交时间戳,而不是两个时间戳,即开始和提交。这里的时间戳应该只用于MVCC,并发控制级别依赖于锁定的悲观并发控制。这个单一的时间戳也是Serializable的逻辑体现:Serializable等价于所有按顺序执行的事务,事务以原子量的时间开始和结束。

如果协调器挂起会发生什么?

每个范围都有一个筏,上层通常可以认为他们是可靠的。但是协调器是一个单点,它挂在中间的概率还不够低,不能依靠它来保证事务的可靠性。按照设计,crdb安排协调器驱动事务的整个生命周期,但是当协调器挂起时,如果遇到剩余的WriteIntent,其他访问者仍然可以让事务经过访问者驱动的清理过程。这和percolator是一样的。

如果事务执行在提交点之前中断,则键访问者驱动回滚进程,将事务状态标记为中断,并清理键上的事务元信息;
如果在提交点之后出现中断,则访问者将驱动前滚进程,使事务写入的最新值生效,并清理事务元信息。
与每个锁都有超时的percolator不同,crdb中的超时保存在TransactionRecord中,而WriteIntent没有超时。如果其他访问者发现TransactionRecord已经超时,他们将认为协调器已经死亡,并启动清理过程,将TransactionRecord置于ABORTED状态。

当一个Key被访问并且发现一个写意图存在时,TransactionRecord和写意图的引用一起被发现,并根据其状态进行不同的处理。

如果发现它处于COMMITTED状态,则直接读取WriteIntent中的值,同时启动WriteIntent清理。
如果它处于ABORTED状态,这个WriteIntent中的值将被忽略,并开始清理WriteIntent。
如果遇到PENDING状态,则认为遇到了正在进行的事务。
如果发现事务已经过期,则将其标记为ABORTED状态。
如果事务没有过期,则冲突检测将与时间戳一起执行。
后面将详细介绍冲突检测的逻辑。

时间戳缓存和读刷新
写意图可以用来跟踪写冲突,但是crdb是Serializable,也需要跟踪读记录,检测读操作和写操作的冲突。

crdb方法是在每个范围内保留一个时间戳缓存,用于存储该范围内Key最后一次读取操作的时间戳。顾名思义,时间戳缓存是存储在范围内的内存数据,不是筏复制的。

无论何时发生写操作,都会检查当前事务的时间戳,以确定它是否小于时间戳缓存中Key的最新值。如果是,当前事务的写操作将使另一个事务最近的读操作无效。在正常的Serializable事务流程中,此时应该将事务冲突声明为失败。

然而,crdb在这里并没有直接退出事务冲突,但仍然保留了一个手。其思想是:当前事务与其他事务存在读后写冲突,我是否可以将事务的时间戳推回当前时间,然后运行读后写冲突检查。

但是,Push Timestamp必须满足某个谓词,即当前事务读取的键在[原始时间戳,新时间戳]范围内是否有新的写操作。不幸的是,如果有新的写入,则无法保存它,事务冲突将被认为失败。这种检查称为“读刷新”。

与直接报告事务冲突,然后在用户的上层重新尝试整个事务相比,推送时间戳更轻,更用户友好。我认为这也是悲观并发控制比乐观并发控制更好的地方。

事务冲突有几种情况。

写后读:读取未提交事务的写意图时,其时间戳小于当前事务。crdb将当前事务添加到TxnWaitQueue队列中,以等待相关事务的完成。如果写入意图读取的时间戳大于当前事务,则不需要等待,直接由MVCC读取密钥,这相当于读取当前事务时刻的快照。

读后写:写入时,当前事务的时间戳必须大于或等于最后一次读取Key的时间戳,如果有冲突,则尝试使用Push timestamp继续事务。

写后写:在写时,如果遇到一个之前未完成的写意图,则等待该事务先完成。如果遇到更新的时间戳,则该时间戳将由Push时间戳本身回推。

 

 

总而言之,事务遇到冲突有两种策略。

等待:主要用于其他事务的开始时间早于当前事务,当遇到依赖关系时,当前事务等待其他事务完成后再执行。
Push Timestamp:主要用于当前事务开始时间早于其他事务时,遇到依赖时,当前事务首先尝试回推时间戳,使其变得晚于其他事务,但回推时间戳需要经过Read refresh预检查,以确保当前事务读集未被修改,否则事务也应中断。

并行提交

原来的percolator性能很差:每一行数据的Prewrite都需要被复制,然后每一行数据再次被Commit复制。如果下层使用Raft做复制,N行数据,相当于执行2 * N个Raft共识,每个Raft共识至少是一次fsync()行程。相比之下,一个独立的DB只fsync()一次,不管有多少行事务。

为了优化提交的性能,crdb首先做了Write Pipelining,在一个事务中写入的多行数据被调度到一个管道中并行启动共识过程,这将等待时间从O(N)减少到O(2), Prewrite和commit分别经历了两轮共识过程。

是否有可能进一步优化提交过程?crdb引入了一种新的提交协议——并行提交(Parallel commit),它允许一轮协商一致的过程来完成提交。一般的想法是,在两阶段提交中,只要所有的预写都完成了,提交就不会失败,用户可以安全地返回成功的提交。

那么,如何确定所有写操作都是完整的呢?对事务记录做了两个更改:引入暂存状态,表示事务已经进入提交状态;2. 添加inflightwites字段,它记录当前事务写入的键的列表。此外,事务记录不再是在事务开始时创建的,而是在用户Commit()时创建的,这样我们就可以知道事务修改了哪些键。

根据官方文档中的示例,交易的过程大致如下。

客户端与事务协调器联系以创建事务。
客户端尝试写一个键K1值为“Apple”的写意图,它会生成交易记录的ID,并使写意图指向它,但实际上并不创建交易记录。
客户端尝试写一个K2键值为“Berry”的写意图,再次使其指向事务记录的ID,该事务记录也不存在。
客户端启动一个Commit(),它创建了一个状态为STAGING的事务记录,并使其inflightwites指向[" Berry ", " Apple "] WriteIntents。
等待“写意图”和“事务记录”的并发写入完成并返回成功。
协调器启动提交阶段,使事务记录提交,并使写意图刷新到主存储。

如果协调器在提交后挂起,读取写意图键的访问者首先读取相应的事务记录,然后通过inflightwrite访问每个键来确定写是否成功。

如果不成功并且事务记录已超时,则访问者将事务记录驱动到ABORTED状态。
如果所有key都已成功写入,则认为它们处于隐式提交状态,访问器将事务记录驱动到显式提交状态,并将相关的写意图刷新到主存储。
可以看出,虽然Parallel显著减少了提交过程的等待时间,但访问者驱动的异常恢复过程变得更加昂贵。crdb仍然希望在正常进程中尽快通过协调器驱动提交进程,以便访问者驱动的恢复进程只能作为最后的手段。

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值