Percolator导读

该篇翻译算是很到位了,但是为了便于回顾,我这里按自己的思路再整理下。
貌似目前尚未有基于hbase之类的开源实现(可能是我孤陋寡闻)。
论文里声称地比MR架构快100倍,只是在类似索引库更新这种特定应用上才成立,不可剖离上下文断章取义进行泛化,所以某些web文章中说的google Percolator的增量处理引擎取代了MapReduce是不准确的
MR是计算跟着数据走,而percolator其实是把delta数据推到计算点。里面的事务机制充分体现了googler在设计系统时的精细,所谓成败在于细节,该系统诠释得很充分。
 
percolator要解决的问题很明确,高效地对一个既有数据库进行增量更新,其直接动机是提高google的网页库索引更新时效性,否则网页库索引的更新只能依赖MR的全量,随着网页库的规模增大处理延时增大。此系统也被用来将页面渲染为图片:Percolator跟踪web页面和它们依赖的资源之间的关系,所以当任何依赖的资源改变时页面也能够被再处理。
核心问题:
1.如何在PB级的数据存储中高效地进行随机读写(因为增量计算只需要access关联的部分而不需要)
2.为了吞吐率,随机读写可能在大量机器上的很多线程上并发,如何提供ACID机制
两个需求:
1.必须运行在大规模数据上
2.不要求非常低的延迟,Percolator的事务管理缺乏一个中央总控:尤其是它缺少一个全局死锁检测器。这增加了事务冲突时的延迟,但是却可以帮助系统伸缩至几千台机器。
 
处理模型(观察者模式):为了解决并发问题,增量系统的开发者需要持续跟踪增量计算的状态。Percolator提供观察者来帮助实现此任务:每当一个用户指定的列发生变化时系统将调用的一段代码逻辑。Percolator应用的结构其实就是一系列的观察者;每个观察者完成一个任务并通过对table进行写操作,为“下游”的观察者创建更多的工作。一个外部的处理会将初始数据写入table,以触发链路中的第一个观察者。要点:
1.ACID transactions over a random-access repository
2. Observers
 
系统概述:
Percolator系统中集群的每台机器包含三个组件:Percolator worker,Bigtable, GFS,从前往后依赖(实际上还有Chubby)。
Percolator建立在Bigtable分布式存储系统之上。基于Bigtable来构建Percolator,也就大概确定了Percolator的架构样式。Percolator的API和Bigtable的API也很相似:Percolator中大量API就是在特定的计算中封装了对Bigtable的操作。实现Percolator的挑战就是提供Bigtable没有的功能:多行事务和观察者框架
Bigtable的职责是对数据进行结构化存储,除了存实际的数据,还使用某些列来存储“元数据”信息。
比较重要的几个列:
1.notify列仅仅是一个hint值(可能是个bool值),表示是否需要触发通知。
2.ack列是一个简单的时间戳值,表示最近执行通知的观察者的开始时间。
3.data列是KV结构,key是时间戳,value是真实数据,包含多个entry。
4.write列包含的是写记录,也是KV结构,key是commit时间戳,value是数据写入时间戳(即data字段中的key)。读数据的时候先读write列,所以只有出现在write列(已提交)的数据才可见。
5.lock列也是KV结构,key是时间戳,value是锁的内容。
 
Percolator Worker由两部分组成,一是用于扫描的线程池,二是开发者编写的观察者(observer)。Percolator的流程:
1.数据(记录)写入data列,如果写入事务成功,则将记录提交到write列,并设置notify列。
2.worker中的扫描线程扫描 notify列,
3.检测到notify后,还要比较ack列和write列的时间戳,猜测是不是应启动观察者(因为可能已经有观察者启动了)。如果可以启动,则更新ack列(原子操作,如果有冲突也只有一个成功)。
4.个观察者在worker中会注册自己感兴趣的列,扫描线程找到此次通知对应的观察者,启动并开始执行一个新事务。在计算结束后,输出的结果需要写入另一个table。
 
因为percolator是在bigtable这种PDBMS上进行数据处理,没有中心协调者的较色,所以其transaction实现有自己的特色,机制如下:
<写>
1.Set数据是先缓存在内存的vector中,由commit操作统一提交
2.commit时分为prewrite和commit两阶段,pre-write主要是判断是否冲突,不冲突则把数据写入data列并锁住;需要等到所有的数据都pre-write OK才会真正commit
3.pre-write时按以下条件判断是否发生写-写冲突:如果事务在它的开始时间戳之后看见另一个写记录;在任意时间戳看见另一个锁;因为要操作锁,primary和secondary都需要使用行的原子操作。
3.无论pre-write还是commit,都会选择一行数据作为primary(两阶段中primary行相同),其余作为secondary,secondary锁中要记录primary锁的位置
5.commit阶段,首先要检查primary lock是否还存在,在wirte列上进行commit然后清除掉lock,除了primary外,secondary们不需要使用行的原子操作。
<读>
读前先检查是否有早于自己transaction时间的锁,如果有说正在写,判断是否应清除锁前滚、还是退避等待锁释放,然后读取(读transaction时间之前的)最新的数据返回
<故障>
如果transaction中途fail,会遗留一些过时的锁,percolator使用 lazy approach来做清理工作,就是系统不主动扫描,如果transaction A挂掉,transaction B执行时可以主动清除锁,,只要该操作能终止可能只是pending住的transaction A。primary cell的存在就是为了定义一个同步点(上面的锁称为primary lock)。清除locks也需要先清除primary lock,而primary lock的操作包含在bigtable行事务中,有原子性保障,从而确保不会清除一个部分提交的transaction的lock。但是,可能在commit阶段primary cell commit成功了(此时primary lock被释放),但secondary们还没有commit都完成就挂掉,这是所有故障情况里面最棘手的一种情况,此时后续transaction执行Get或Commit操作时,会发现secondary上有锁,但其对应的primary上无锁,此种情况需要后来transaction执行roll-forward操作,即通过该锁,找到时间戳,以此时间戳往write列里提交个写记录即可(然后再删除锁)。
 
oracle时间服务器:
其巧妙之处是每次分配一个时间区间,然后把时间区间的右值(最大)进行持久化,如果oracle服务器挂掉重启,会从持久化的右值往后开始分配,保证分配的时间戳递增无重复。事务协议机制使用严格增长的时间戳来保证Get()能够返回所有在“开始时间戳”之前已提交的写操作(可跟据上面提到的机制仔细推导这句话)。
 
通知:
所有的事物执行者(观察者observers)都被链接到一个可执行的percolator worker,而每个tablet server伴随这样一个worker(从而让访问本地化)。
通知类似于数据库中的触发器或者事件,但是与数据库触发器不同,触发操作本身(write)与处理程序(观察者)之间不在一个事务中。虽然理论可以多个观察者观察一个列,但google避免这样做,原因是如果每个列只有一个观察者的话,其处理流要更清晰一些。
1.对一个被观察列的每次改变,至多一个观察者的事务被提交;这是通过ack列来实现的,只有当write列最新的时间戳大于ack列是时间戳时才回触发观察者事务,而观察者在开始事务时会更新ack列。
2.但一个被观察列的多次写可能只会触发一次观察者事务(这个特性为消息重叠,这样不是每次更新都会导致观察者事务,小周期就可以了)。在观察者被触发并且事务提交成功后,会删除对应的notify cell。
每个percolator worker会有多个线程同时扫描该tablet server,每个线程从随机的某个tablet的某个key开始,但这会导致“platooing”(bus clumbing)现象,解决方法是如果追尾的线程发现出现凝结现象,则“时空跳跃”到另一个随机点接着扫描。
 
提升效率之举:
1.notify列使用bigtable的locality group,充分利用了列存储在处理稀疏列时的高效性,扫描时仅需读取百万个脏cell,而不是万亿行个cell。
2.lock使用in-memory列(相关的数据载入内存以后就不会被替换出去)已增强访问的性能
3.Percolator的worker会维持一个长连接RPC到oracle,低频率的、批量的获取时间戳,所以单台oracle server即可服务2M qps。
4.打包RPC调用,减少RPC数量,通常MR只需要少量的批量读写次数,percolator因为处理是离散化的,RPC数量很容易通过依赖关系扩散到很大的数量级。如果不做批量化,对同一个tablet多行数据(n)的多列(m)进行提交操作,因为是两阶段,所以共有n*m*2次RPC调用。
5.预读,根据历史行为(这部分文章未详述)进行预读,尽量读取一行更多列的信息(因为读取一列和多列磁盘IO以及SSTable解压开销差不多)。
6.通过同步的大量线程来充分利用cpu(thread-per-request),为提高高线程数时的性能,内核组专门做了优化(没有提具体什么优化)

 

posted @ 2014-02-08 23:14  fernnix  阅读(1769)  评论(1编辑  收藏  举报