https://zhuanlan.zhihu.com/p/59115828

Percolator 模型

Percolator[1] 是 Google 发表在 OSDI‘2010 上的论文 Large-scale Incremental Processing Using Distributed Transactions and Notifications 中介绍的分布式事务模型。论文的用意是为了帮助 Google Search 提升网页的索引速度,提升网页搜索的 freshness,由于基于 Map-Reduce 的处理框架总是需要遍历整个数据集,对于增量更新的场景是不合适的,所以他们提出了一个增量处理的框架。这篇论文的重点是 Transaction 和 Notification,但是本文仅关注前者。

Percolator 本身是一个依赖 BigTable 作为数据存储引擎的分布式 Key-Value 数据库,是一个典型的通过 2PC 进行分布式事务协调的实现,简单来说是依靠 Timestamp-Order 实现了快照级别的事务。创新点在于,依靠对时间戳的使用和BigTable的单行事务,实现了跨行事务,更进一步说,由于某一行数据可以位于不同的 BigTable 的 TabletServer 之上,所以该事务也是跨节点的。

事务实现时主要参与者有三个:Percolator 客户端、BigTable TabletServer、Timestamp Oracle(TSO)。按照 2PC 的要求分为两个步骤:PreWrite 和 Commit。

代码实现

Percolator 论文中提供了一个详细的伪代码实现,我们将对其进行详细分析。

事务结构

一个事务允许对多个cell进行读写,其中cell可以是跨行的。写请求在客户端仅仅是先放入缓存中,在此阶段中并不会和 BigTable 进行交互,所以这里用一个数组作为写操作缓存。读操作较为复杂,我们将在介绍完整个写流程之后再回到这一部分。

在一个事务初始化时将会从 TSO 中获取一个时间戳,该时间戳实际上就是 MVCC 的版本号,所有在本事务中修改的数据都会tag上这个版本号,这也是通常的做法,唯一需要保证的就是 TSO 分配的时间戳必须是不可重复并且是递增的,即具备 time order,毕竟事务的冲突判断依赖时间序。

1 class Transaction {
2 struct Write { Row row; Column col; string value; };
3 vector<Write> writes ;
4 int start ts ;
5
6 Transaction() : start ts (oracle.GetTimestamp()) {}
7 void Set(Write w) { writes .push back(w); }
8 bool Get(Row row, Column c, string* value) { ... }

PreWrite 操作

在客户端完成一系列的读写请求的提交后,Percolator准备提交操作,将这些请求提交到 BigTable 去,整个过程分为两个阶段,第一阶段即这里的 PreWrite。PreWrite 将整个事务里涉及的写操作分为两类,一类是 primary记录,一类是 secondary 记录。其中 primary 记录只有一条,而后者可以是多条的,实际上 primary 的选择可以是随机的,论文中固定用第一条操作的记录作为 primary 记录。

有必要提前介绍下每行涉及的几个重要列(Percolator还会有notify、acknowledge等列用于消息通知,但由于分布式事务功能用不上,这里不作介绍):

  • write:数据列,一个写事务最终被成功提交后,相应的数据部分是存储于这个列中的
  • data:临时数据列,MVCC 写过程中会将被修改的数据写入该列,视最终事务的结果是成功commit还是roll-backward、roll-forward来决定如何解释 data 列数据
  • lock:锁所在的列,某个事务在进行修改时会通过写入该列来锁住该行,在目前的实现下只要有发现某行上存在锁(任意时刻的锁)即需要终止本事务(对于残留的锁的处理后文会详细描述)
26 // Prewrite tries to lock cell w, returning false in case of conflict.
27 bool Prewrite(Write w, Write primary) {
28 Column c = w.col;
29 bigtable::Txn T = bigtable::StartRowTransaction(w.row);
30
31 // Abort on writes after our start timestamp . . .
32 if (T.Read(w.row, c+"write", [start ts , ∞])) return false;
33 // . . . or locks at any timestamp.
34 if (T.Read(w.row, c+"lock", [0, ∞])) return false;
35
36 T.Write(w.row, c+"data", start ts , w.value);
37 T.Write(w.row, c+"lock", start ts ,
38 {primary.row, primary.col}); // The primary’s location.
39 return T.Commit();
40}

每次被 PreWrite 操作的行都需要带上 primary 行,这是为了在后续处理遗留的 lock 列时便于确定事务是否进行 roll-backward 还是 roll-forward。

第29行启动一个单行事务,注意这部分逻辑都是在客户端执行的,为了在 commit 阶段能够判断是否有冲突的修改,这里很可能获取了该行在 BigTable 服务端记录的一个内部数据版本号。

第32行进入事务后先读取被修改行的 write 列,查看该列上是否已经有一个在事务开始之后被commit的数据,如果有就需要终止本事务,因为本事务中观察到的数据在事务过程中可能被外部其他事务给修改了,所以本事务未观察到新写入的值,Percolator认为这是不安全的所以进行回滚。另外,Percolator比较激进地认为一行上一旦出现一把锁都是不允许本事务继续进行执行的,后面我们会看到 TiKV 在实现时也有类似的要求,这一定程度上可以简化处理逻辑。

当发现冲突并不存在之后,事务开始真正更新这行数据,时间戳即为事务开始时从 TSO 获取到的 start_ts,写完 data 列之后将该行对应的 lock 列也更新,注意 lock 列中的值填写的是 primary 记录,后续还需要依赖这个重定位来确定 secondary 记录是应该 roll-backword 还是 roll-forward。

待所有记录的 PreWrite 操作完成后,事务进入到 Commit 阶段。

Commit 操作

Commit 操作分为两个阶段,第一个阶段是提交 primary 记录,第二个阶段则是提交 secondary 记录,一旦 primary 记录被提交完成,整个事务就完成了,所有该事务内修改的数据也具有可见性。如果 secondary 没有提交完成前客户端或者服务端就发生了crash,Percolator保证在读操作的实现时依然可以看到整个事务的完整性,并执行 roll-forward 将所有记录恢复以得到数据 integrity。

41 bool Commit() {
42 // PreWrite logic...
47
48 int commit ts = oracle .GetTimestamp();
49
50 // Commit primary first.
51 Write p = primary;
52 bigtable::Txn T = bigtable::StartRowTransaction(p.row);
53 if (!T.Read(p.row, p.col+"lock", [start ts , start ts ]))
54 return false; // aborted while working
55 T.Write(p.row, p.col+"write", commit ts,
56 start ts ); // Pointer to data written at start ts .
57 T.Erase(p.row, p.col+"lock", commit ts);
58 if (!T.Commit()) return false; // commit point
59
60 // Second phase: write out write records for secondary cells.
61 for (Write w : secondaries) {
62 bigtable::Write(w.row, w.col+"write", commit ts, start ts );
63 bigtable::Erase(w.row, w.col+"lock", commit ts);
64 }
65 return true;
66 }
67 } // class Transaction

第48行向 TSO 申请一个时间戳用于进行 commit 时使用,这是 MVCC 的经典套路。

第52行针对 primary 记录开启一个单行事务,第53行检查 primary 记录上的锁是否还在(时间戳是精确的start_ts),这是因为在之前的 PreWrite 阶段和这里的 Commit 第一阶段并不是原子的,primary 记录设置的锁可能会在 PreWrite 完成后被清除掉。这个清除操作主要是因为Percolator对于残留的lock进行lazy cleanup时导致的,比如如下的场景:

  • Txn0 启动,打算修改row(“AAA”),row(“BBB”)
  • Txn0 执行完对“AAA”和“BBB” 的PreWrite,还未执行Commit第一阶段
  • Txn1 启动,打算修改 row(“BBB”),对“BBB”执行lock检查发现有锁,此时启动lazy cleanup查看“BBB”中记录的primary的lock记录是否存在,发现是存在的说明这个primary记录对应的事务还没有Commit,可以对其进行回滚,于是删除其lock
  • Txn1 打算执行 Commit,发现primary记录的lock被人清除了,于是事务失败

为了应对这种情况(在RocksDB中可以依靠GetForUpdate[2]来实现),Commit 第一阶段的事务提交前会先检查这个lock是否存在,只有存在时才可以继续提交,由于这个保证是由 BigTable 单行事务提供的,只要第一阶段顺利提交完成,就说明这个 lock 之前还没被人清除,是安全的。确认primary记录安全之后,就可以更新write列了,其value就是对应修改的data列,并清除掉对应行的lock。

注:Percolator代码中的Erase使用的是commit_ts,按我个人理解应该是start_ts,不知道是否是作者的typo,还请读者一起讨论。

在primary记录提交完成后整个事务事实上已经可以认为完成了,所有数据已经具有对外可见性,这是通过对读操作的实现来保证的,所以后面不再叙述对secondary记录的Commit操作。

Get 操作

8 bool Get(Row row, Column c, string* value) {
9 while (true) {
10 bigtable::Txn T = bigtable::StartRowTransaction(row);
11 // Check for locks that signal concurrent writes.
12 if (T.Read(row, c+"lock", [0, start ts ])) {
13 // There is a pending lock; try to clean it and wait
14 BackoffAndMaybeCleanupLock(row, c);
15 continue;
16 }
17
18 // Find the latest write below our start timestamp.
19 latest write = T.Read(row, c+"write", [0, start ts ]);
20 if (!latest write.found()) return false; // no data
21 int data ts = latest write.start timestamp();
22 *value = T.Read(row, c+"data", [data ts, data ts]);
23 return true;
24 }
25}

Get操作是比较直观的,启动一个事务然后查看要去读的行上是否由锁,需要注意的是,如果存在的锁的时间戳在 start_ts 之后的话,是允许的,因为按照快照隔离级别的要求,当前事务对于一个未来事务修改的数据可以是不可见的,但是如果在 start_ts 之前残留的锁,Get操作就会进行等待和cleanup操作,大致的含义就是如果多次等待之后发现该锁还存在,就可以按照论文中提及的lazy cleanup将这个锁给清除。

确定没有冲突的锁之后,读事务在第19行先看了下在快照隔离事务允许的时间戳之内是否有合法的write记录,如果有这就是我们要读取的最新的数据,从这个write列中读取出写入这个记录的事务修改的数据所在的data时间戳,利用该时间戳获取到相应数据即可。

这里额外提一句,这类读操作对版本号的使用实际上使得一个点查的数据操作在实际实现中会产生大量的Scan操作,在CockrouchDB的博客[3]中有对该问题的阐述。

对BigTable单行事务的要求

Percolator是依赖BigTable的单行事务构建的,它除了保证事务内的所有写操作能够原子执行之外,还需要保证在事务过程中进行判断时依赖的条件在事务被Commit时也是依然成立的。比如在Percolator执行Commit的第一阶段,当主记录写入write列成功时,必须保证整个过程中lock列都是存在的。

对于使用RocksDB的应用来说,RocksDB提供了GetForUpdate接口来做这一保证,保证一个condition在事务整体过程中的一致,一旦该condition被修改事务将无法提交。

Percolator 在 TiKV 中的应用

TiKV是较为知名的一个使用Percolator来实现分布式事务的项目(CockroachDB也从早期的事务模型转向了Percolator,但是因为他们实现的SERIALIZABLE隔离级别,比快照隔离要求更高,还做了一些其他的优化,具体可以参考[5]),所以我大概分析了下代码,也参看了官方源码分析的一些文章[4]。最开始的一个疑惑是TiKV在实现PreWrite阶段做判断时,读操作都是从一个snapshot进行的,这是使用RocksDB的存储引擎的惯用做法,但是按照前面对单行事务的要求,依靠snapshot并不能满足我们的要求。

比如在Commit主记录阶段,从snapshot中看到的lock还存在,此时执行一个写事务修改write列,并不能保证这个lock在外部没有被清理,看得我百思不得其解,后来发现原来 TiKV 在进入到事务之前,还在其 Scheduler 中为每一个行提供了一个latch来防并发,这个latch限制了对于一个行的修改同时只有一个事务可以进行,这样就可以保证我们的要求了。

Percolator模型的其他应用

由于 Percolator 实现了一个快照隔离级别的事务,我们可以利用这个功能做一些特性。比如在一个分布式文件系统中做快照,这个文件系统可以在对索引实现中使用Percolator模型保证索引在多节点上修改的一致性。通知在应用层提交一个快照请求时,只需要从 TSO 获取一个时间戳来表示快照时间点,这个时间点前后都可以保证整个文件系统索引的内部一致性。

Reference

[转帖]Percolator分布式事务模型原理与应用的更多相关文章

  1. X/Open DTP——分布式事务模型

    转载:http://www.cnblogs.com/aigongsi/archive/2012/10/11/2718313.html 这一几天一直在回顾事务相关的知识,也准备把以前了解皮毛的知识进行一 ...

  2. 互动科技 快乐分享 X/Open DTP——分布式事务模型

    这一几天一直在回顾事务相关的知识,也准备把以前了解皮毛的知识进行一些深入总结,虽然这一些知识并没有用到,但是了解其实现原理还是很有必要的,因为知道了原理,你也能把它实现出来. 在上一节事务的编程模型里 ...

  3. Fescar分布式事务实现原理解析探秘

    前言 fescar发布已有时日,分布式事务一直是业界备受关注的领域,fescar发布一个月左右便受到了近5000个star足以说明其热度.当然,在fescar出来之前,已经有比较成熟的分布式事务的解决 ...

  4. 终于有人把“TCC分布式事务”实现原理讲明白了!

    之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下.很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以这篇文章,就用大白话+手工绘图,并结合一 ...

  5. 【转帖】分布式事务之解决方案(XA和2PC)

    分布式事务之解决方案(XA和2PC) https://zhuanlan.zhihu.com/p/93459200 ​ 博彦信息技术有限公司 java工程师 3. 分布式事务解决方案之2PC(两阶段提交 ...

  6. 终于有人把“TCC分布式事务”实现原理讲明白了

    所以这篇文章,就用大白话+手工绘图,并结合一个电商系统的案例实践,来给大家讲清楚到底什么是 TCC 分布式事务. 首先说一下,这里可能会牵扯到一些 Spring Cloud 的原理,如果有不太清楚的同 ...

  7. 巨杉数据库SequoiaDB】巨杉Tech | SequoiaDB 分布式事务实现原理简介

    1 分布式事务背景 随着分布式数据库技术的发展越来越成熟,业内对于分布式数据库的要求也由曾经只用满足解决海量数据的存储和读取这类边缘业务向核心交易业务转变.分布式数据库如果要满足核心账务类交易需求,则 ...

  8. 分布式事务(Seata)原理 详解篇,建议收藏

    前言 在之前的系列中,我们讲解了关于Seata基本介绍和实际应用,今天带来的这篇,就给大家分析一下Seata的源码是如何一步一步实现的.读源码的时候我们需要俯瞰起全貌,不要去扣一个一个的细节,这样我们 ...

  9. 分布式事务(3)---RocketMQ实现分布式事务原理

    分布式事务(3)-RocketMQ实现分布式事务原理 之前讲过有关分布式事务2PC.3PC.TCC的理论知识,博客地址: 1.分布式事务(1)---2PC和3PC原理 2.分布式事务(2)---TCC ...

  10. 分布式事务与Seate框架(1)——分布式事务理论

    前言 虽然在实际工作中,由于公司与项目规模限制,实际上所谓的微服务分布式事务都不会涉及,更别提单独部署构建Seata集群.但是作为需要不断向前看的我,还是有必要记录下相关的分布式事务理论与Seate框 ...

随机推荐

  1. 27、flutter Dialog 弹窗

    AlertDialog //放在State<>之下 void _alertDialog() async { var result = await showDialog( barrierDi ...

  2. 【DevCloud · 敏捷智库】如何利用核心概念解决估算常见问题(内附下载材料)

    摘要:团队用于估算时间过多,留给开发的时间会相应减少,大家工作紧张,状态不佳.团队过度承诺直接造成迭代目标不能完成,士气低落.以上弊端直接伤害敏捷团队,是敏捷团队保持稳定健康节奏的阻力. 背景 敏捷江 ...

  3. 手把手带你通过API创建一个loT边缘应用

    摘要:使用API Arts&API Explorer调用IoT边缘服务接口创建应用,了解边缘计算在物联网行业的应用. 本文分享自华为云社区<使用API Arts&API Expl ...

  4. 人人都在聊的云原生数据库Serverless到底是什么?

    摘要:华为云数据库营销专家Tony Chen和华为云数据库高级产品经理佳恩开展了一场关于云原生数据库与Serverless结合的直播对话. 云计算的迅猛发展推动了数据库的变革,云原生数据库成为当前数据 ...

  5. 一文带你认识MindSpore新一代分子模拟库SPONGE

    [本期推荐专题]物联网从业人员必读:华为云专家为你详细解读LiteOS各模块开发及其实现原理. 摘要:基于MindSpore自动并行.图算融合等特性,SPONGE可高效地完成传统分子模拟过程,利用Mi ...

  6. 带你认识MindSpore量子机器学习库MindQuantum

    摘要:MindSpore在3.28日正式开源了量子机器学习库MindQuantum,本文介绍MindQuantum的关键技术. 本文分享自华为云社区<MindSpore量子机器学习库MindQu ...

  7. max file descriptors [4096] for elasticsearch process is too low, increase to at least [65535]

    elasticsearch安装后启动时候,遇到此问题 问题翻译过来就是:elasticsearch用户拥有的可创建文件描述的权限太低,至少需要65536: 解决办法: 切换到root用户修改 vim  ...

  8. 实用指南:手把手搭建坚若磐石的DevSecOps框架

    长期以来,安全问题一直被当作软件开发流程中的最后一步.开发者贡献可以实现软件特性的代码,但只在开发生命周期的测试和部署阶段考虑安全问题.随着盗版.恶意软件及网络犯罪事件飙升,开发流程需要做出改变. 开 ...

  9. 听说火山引擎推出的 DataLeap,已经可以支持万级表的数据血缘图谱了!

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 数据来源广.量级大.场景多,导致数据之间关系变得异常复杂. 经过读取.清洗.存储.计算等一系列流程之后,数据最终汇 ...

  10. Kubernetes(K8S) 镜像拉取策略 imagePullPolicy

    镜像仓库,镜像已更新,版本没更新, K8S 拉取后,还是早的服务,原因:imagePullPolicy 镜像拉取策略 默认为本地有了就不拉取,需要修改 [root@k8smaster ~]# kube ...