事务在触发器中结束。批处理已中止。_Google Percolator 事务模型的利弊分析

Google于2011年发表了percolator事务模型的论文[1],我最近分析了一下percolator事务模型的特点、适用范围以及它与传统的DBMS的两阶段提交分布式事务模型的工作方式以及在性能,可扩展性等方面的利弊对比,故有此文。

本文并不是数据库产品选型的指导,使用percolator事务模型的数据库产品未必就有本文中列出的问题或者缺点,因为或许这些问题已经在具体产品中被解决了。具体情况需要用户自己分析判断。我希望读者主要把阅读本文作为阅读和理解percolator论文的一个补充,我试图通过运用数据库事务处理的理论知识以及数据库系统的应用经验来分析其利弊,来让自己更好地理解、掌握和扩充这些知识、技能和经验,或许读者也能有同样的收获。最后,文中如有纰漏敬请指正,欢迎技术层面的讨论。

背景

从论文[1]的介绍章节看到,Percolator事务模型是google为其负责对搜索引擎的web 页面索引做增量式更新的批处理系统使用的。分析percolator算法和执行过程的文章,别人已经写过了,网上很容易可以找到,我在此不再赘述。我想首先根据[1]论文的内容,介绍一下percolator事务模型出现的背景和解决的问题。

在论文[1]的介绍章节可以看到,Google的web搜索引擎的搜索爬虫是在不停地抓取web页面,其中有些web页面是新增的,有些是原有的但是发生过更新,另一些是已有的并且没有发生变化。有些抓取的页面是重复的,这时要处理重复页面和重复链接,等等。爬虫抓取的内容需要合并到已有的全局数据上面,因此需要增量式处理(incremental processing)。通过使用事务,可以确保在发生节点故障的情况下,系统全局数据状态是一致的。Google的web 索引数据量多达上百PB,传统的DBMS无法处理,因此需要在Bigtable集群中存储数据,并使用percolator事务模型实现ACID。

优点

Percolator事务模型的优点就是为google web搜索引擎的海量(几百PB)web index数据的增量式更新的批处理任务提供了一个可扩展的ACID事务模型,这样方便上层应用开发者,他们就不用考虑在错误处理中如何把修改了一半的web index数据退回到一个一致的状态了。正如论文[1]所说,percolator不是用来代替MapReduce, BigTable或者传统的DBMS的,它们各自有其使用范围。下面截图来自该论文[1],它很好地描述了这4中选项的适用范围。

b173e08fde9b070ce50a0b6dcf68839b.png

可扩展是通过去除中心节点实现的,percolator模型甚至不需要一个事务管理器维护事务状态,一个事务的状态是在客户端和这个事务所写(insert/delete/update)的行中维护的。这样,percolator事务模型是没有中心节点的(如果percolator用到的big table和GFS的功能也没有访问任何中心节点的话),也就没有性能瓶颈,可扩展性自然很好。但是这么做也有其缺点,我下面会讲到。

然后,讲一下percolator的‘两阶段’提交。这个不算优点也不算缺点。它的这个’2PC’不是传统意义的 XA 2PC算法。事实上percolator的第一阶段只是做了写行到big table,以及记录行锁和start_ts版本字段。在这个第一阶段之前,客户端调用的Set() 写的数据只是被缓存起来并没有写入big table. Perconator事务的第二阶段,才是真正的事务提交阶段。只要一个percolator事务的primary row的lock字段被清除并且其write record写好(这两个动作是在同一个bigtable 事务中原子完成的)后,这个事务就算是提交完成了,它的所有改动就立即被其他事务可见了。所以percolator事务的提交其实是1阶段提交。之所以它避免了传统的两阶段提交,是因为检查一个行是否可见时候,如果其lock信息指向primary row,那么直接读取primary row,不管primary row是不是在同一个big table节点上面 ---percolator已经忽略了跨节点网络通信的开销了,简单地把big table当做一个实体来读写数据。 而传统DBMS 之所以需要两阶段提交,是因为试图尽力避免从一个db实例中连接到另一个db实例中去读取数据或者状态。

缺点

我分析下来percolator事务模型对于OLTP的DBMS 来说,有如下缺点和问题。

事务提交延时较大

该论文[1] 的这段文字讲述了percolator事务模型的事务提交延时很大的问题,并列出了一种原因,但是我认为导致延时很大的主要原因还不止这个。

42b575c91f36f10f5e1d84cafbcdcb10.png

我认为导致一个percolator事务提交延时很大的主要原因是:一个事务T在提交期间,需要再次更新它插入、更新、删除的每一行,写上提交时间戳(commit-ts)并且去除锁信息。这对于OLTP的DBMS来说是难以承受的,因为通常一个事务插入、更新的行可能有很多,在正常的事务insert/update/delete语句修改过这些行之后,还要在事务提交时刻再次修改一遍,这涉及到buffer pool可能要重新把这些行所在的页面从外存装入内存,然后逐行获取行锁然后修改,并且这些修改仍然要记录事务日志并且提交本地bigtable事务时候还要再次刷盘。这样,事务提交延时就会很大。对于bigtable来说,上述这些开销一样都少不了。或许对于批处理系统,这种延时是可以忍受的,但是对于OLTP 的DBMS,这种延时绝对是无法忍受的。

写热点行的并发性损失

另外由于没有事务管理器维护事务状态,当客户端宕机后,它遗留的未开始提交的事务持有的锁,以及一个事务因为写冲突而abort后它持有的锁,都要到事务超时后,才能被其他事务的read操作清除,这就增加了其他并发执行的事务发生写冲突被回滚的几率,从而降低了系统并发性,浪费了系统资源。另外,percolator的‘锁’,其实不是真正的事务锁,因为它并不能导致试图获取锁的事务阻塞等待这个锁,也不能在持有锁的事务放锁(通常是结束时)时调度等待的事务获取锁继续执行。Percolator的锁只是一个‘被修改的标志’,一旦事务T1要修改一行R1却发现它已经被T2 锁住了,T1就只能回滚了,无法在锁上面阻塞等待T2结束后再继续。也就是说percolator的读写都是用mvcc做并发控制的,与postgresql相同。这带来的问题就是如果大量并发事务更新热点行,那么系统TPS会很低,只能排队执行并且发生大量事务回滚。对于Google的web index数据来说,可能没有热点行,但是对于通用的DBMS来说,热点行是经常会出现的。

读数据的并发性损失

Percolator的数据行可见性判断也与传统的MVCC有所不同,这导致它读取数据行也可能需要等待,从而损失了并发性能。要知道MVCC最重要的有点就是读操作不阻塞,以确保读的性能。

传统DBMS的MVCC中,一个事务T1的快照记录的是它‘拍’这个快照时刻活跃事务的集合,通常以这样的方式表示:{ts-min, ts-max}, [ative-t0, active-t1... Active-tN],这里ts-min表示比ts-min小的事务全部已经提交(所以T1必然可以看到它们的改动), ts-max表示比ts-max大的一定还没有启动(所以T1必然无法看到它们的改动), active-ti数组中是活跃的事务id列表,这些事务的改动T1也是看不到的。如果一个事务T2.id在此快照中(即T2.id在数组中或者T2.id > ts-max),那么T1看不到T2生成的行版本,否则T1可以看到T2生成的行版本。

而percolator事务模型中,按说其MVCC可见性判断应该是给定事务T1,T2,当且仅当 T2.start-ts > T1.commit-ts,T2才能看到T1的改动。但是从Get()函数的伪代码以及论文中的描述来分析判断,实际情况似乎是如果T2.start-ts > T1.start-ts,那么 T2就 *应该* 看到T1的改动。于是,当T2需要读取1行的时候发现其lock字段的可见的版本有锁(此时该版本必然是最新版本并且其write record必然还没有写入commit-ts),T2就要等待T1提交后再读取。此时T2.start-ts与T1.commit-ts的大小关系哪个大都有可能。总之这样等待就会损失并发性。为什么这种情况下T2不去读取那个最新的已提交的行版本呢? 我认为这才是正确的做法,这样做也才能让write record的commit-ts 有意义。

之所以需要在每个行提交时刻写入commit-ts,还是因为percolator事务没有全局的事务管理器,每个客户端存储着自己的事务状态。所以系统无法获知此刻有哪些活跃的事务,也就无法为事务创建快照,只能在行中写入提交时间戳,才能做MVCC读。

=================== 下面这一段是错误的,请忽略 =========

事务回滚失败可能导致数据脏读

说到回滚,这也是percolator论文中没有提及的一个问题,但是这其实是个大问题。因为percolator事务模型中,如果一个primary 行的lock被清除,那么它就是有效的和可见的,指向primary lock的secondary 行的可见性判断也是使用这个primary lock。如果一个行有primary lock那么这个行就是属于一个正在运行的活跃事务。也就是说一个行没有字段表明创建它的事务是提交成功了还是回滚了。 这意味着如果一个事务需要回滚,那么回滚事务的方法必须要把这个事务生成的新的行版本都删除掉,否则一旦它生成的行中记录的事务锁超时后被清除掉,这个本应该不可见的行版本就可见了,这是严重的错误。然而,”删掉一个回滚的事务生成的所有行版本” 这个操作,可能会因为客户端宕机而中途停掉。一旦停掉之后,由于没有事务管理器,就无法轻易找到要删除那些行了,除非做全表扫描(代价巨大,对bigtable系统会形成很大负载)并且还要是知道要回滚的事务id。找到已经回滚的事务的列表这件事,在percolator描述的设计里面这又是不可能的。Percolator最终是如何回滚的,在[1]文中并没有提及。我认为即使不使用事务管理器这样的中心节点,也应该把完成提交的事务记录下来,就像PostgreSQL的clog一样,也可以像TDSQL XA的commit log一样,这样才可以可靠地处理事务回滚。

写的时候仓促疏忽了,抱歉。 错误的原因: 当且仅当一个行版本R 有write record才是提交成功的,其他事务才可以看到(根据可见性规则:当且仅当 R.commit_ts < T.start_ts,T 可以看到R),否则就是还没有提交的或者已经回滚的事务产生的版本,其他事务就不能看到。

=================== 上面这一段是错误的,请忽略 =========

网络通信开销较大

一个percolator事务执行过程中,有大量的跨节点网络通信,具体来说包括:获取两次事务timestamp(start-ts, commit-ts),每次写入一行时与big table系统的通信(big table本身也是一个分布式数据库,所以实际每写一行的网络通信很可能有更多)。事务提交时,每一行要再次写big table,通信开销翻倍。[1]文提到为了降低通信成本,他们合并了对同一行多个字段(比如写行时写入lock和data字段, 提交时清除lock字段和写write record)的更新操作为单一的big table调用,如果没有这个优化,那么通信成本还要再次翻倍。另外,每次要读取一行时,如果lock字段是指向primary row的指针,那么还要再次读取primary row的lock字段。要知道一个事务只有1个primary row,其余写入的行都是secondary row,也就是说大概率一旦行上面有锁,就需要再多一次网络通信(读取这行对应的primary row的lock字段)。

清理无效行的开销

Percolator系统还需要一组purge后台进程,把那些没有任何事务可以看到(根据行版本的commit-ts来判断)的行版本清除掉,以及清除掉那些被删除掉的行。由于行的所有版本都是存储在big table系统的数据表中的,所以后台的purge 任务也会对big table系统构成很大的负载。这个问题是PostgreSQL也有的问题。Innodb没有这个问题是因为数据表上面永远只有最新版本的行,老版本的行是通过undo日志临时生成的。Innodb Undo日志集中存放在undo表空间中,清理的代价要低很多。

总结

没有完美的理论和技术,每一种技术和方法都有其适用范围和场景,满足特定类别的需求。从论文描述来看,Percolator事务模型在其适用的领域 ---几百PB级别的海量数据的分布式批处理系统的事务处理 --- 能够很好的工作,在其他领域使用时,如何借鉴其思想精髓,扬长避短,是数据库系统设计和研发人员需要认真考虑的。

[1] Large-scale Incremental Processing Using Distributed Transactions and Notifications

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值