Archive

Archive for November, 2010

Large-scale Incremental Processing Using Distributed Transactions and Notifications 译文

November 22, 2010 Leave a comment

 

摘要

抓取文档的同时更新web的一个索引需要在新文档到达时不断地转换大规模的现有文档储存库。这个任务是通过小型的,独立的突变转换大规模数据存储任务类型中的一个例子。这些任务在已有架构的不同性能之间存在差异。数据库不符合这些任务对于存储和吞吐量的要求:谷歌的索引系统存储数十PB的数据,每天处理上千台机器的数十亿的更新。MapReduce和其他批处理系统无法独自处理轻量级的小更新因为他们靠大型的批处理来提高效率。

我们已经建立了一个用于递增地将更新处理成大规模数据集的系统Percolator(过滤器),并已部署它来创建谷歌网络搜索索引。通过将以批处理为基础的索引系统更换成以使用Percolator进行增量处理为基础的索引系统,我们每天在处理数量相同的文档的情况下,谷歌的搜索结果文档的所用平均时间减少了50%

1.简介

考虑建立一个能用来回应搜索查询的Web索引的任务。索引系统开始在网络上抓取每个页面并在处理它们的同时在索引上维护一个不变量的集合。例如,如果根据多个网址抓取到了相同的内容,只有最高PageRank [28]的网址出现在索引中。每一个链接也被转化这样来自每个传出连接的锚文本就被附加在链接指向的网页。链接倒排必须跨副本工作:指向一个页面副本的链接如果有必要应转交到最高的PageRank副本上。

这个批量处理任务,可表述为一系列MapReduce[13]操作:一个为了集群副本,一个为了链接倒排等。维护不变量很容易,因为MapReduce限制了计算的并行度;所有文档在一个处理步骤中完成,否则不进行下一个处理步骤。例如,当索引系统正在写倒推链接到当前最高PageRank的网址,我们不必担心同时改变其PageRank;一个以前的MapReduce步骤已经确定其PageRank。

现在,考虑如何在抓取web的一小部分后更新索引。仅仅在新网页上运行MapReduces是不够的,例如,在新网页和剩下的web之间存在链接。MapReduces必须再次在整个存储库上运行,也就是说,新的和旧的网页。如果有足够的计算资源,MapReduc的可扩展性使得这种方法可行,而事实上,谷歌的网页搜索索引就是以优先于这里所描述的工作的这种方式设计的。然而,预处理整个Web丢弃了之前运行的时候所做的工作,也使延迟与存储库的大小成正比,而不是一个更新的大小。

索引系统可以把存储库存储在DBMS并且在使用事务维护不变量时更新单个文档。但是,现有的DBMSs无法处理庞大的数据量:谷歌的索引系统存储跨越数千台机器[30]数十PB的数据。像Bigtable[9]的分布式存储系统可以扩展到我们的资源库的大小,但不能提供工具来帮助程序员面对并发更新时维护数据的变量规模。

一个理想的能维护网络搜索索引任务的数据处理系统,能被以增量处理为目的进行优化;也就是说,它使我们能够维持一个非常大的文档库并在每一个新的文档被抓取时有效地更新它。鉴于系统将同时处理许多小的更新,一个理想的系统也将提供维护不变量的机制,而不管并发更新以及跟踪哪些更新已被处理。

本文的其余部分描述了一个特定的增量处理系统:Percolator。Percolator提供用户随机的访问多PB级的储存库。随机访问使我们能够单独地处理文档,避免了MapReduce需要的资源库的全局扫描。为了达到高吞吐量,许多机器上许多线程需要同时转化储存库,所以Percolator提供ACIDcompliant事务,使程序员更容易对存储库的状态进行推理;我们目前完成了快照隔离语义[5]。

除了推理并发,一个增量系统程序员需要不断跟踪增量计算的状态。为了帮助他们完成这个任务,Percolator提供了observer:即一些代码段,只要用户指定的列更改就被系统调用。Percolator的应用程序作为一系列observer被结构化;每个observer完成一个任务并通过写表为“下游”的observer创造更多的工作。一个外部进程通过写初始数据到表中触发处于链中的第一个observer。

Percolator是专为增量处理建立的,而不是为了取代解决大多数数据处理任务的现有的解决方案。结果不能被分解成小的更新(例如,排序一个文件)的计算能更好的被MapReduce处理。此外,计算应具有较强的一致性要求;否则,BigTable就足够了。最后,计算应在某些方面非常大(总数据大小,转换所需的CPU资源等);不合适MapReduce或Bigtable的较小的计算可以由传统的DBMSs处理。

在谷歌,Percolator的主要应用是准备将Web网页列入实时搜索索引。通过转换索引系统到一个增量系统,我们可以处理抓取的单独文档。这减少了100倍的文档平均处理延迟,在搜索结果中出现的一个文档的平均年龄下降了近百分之五十(搜索结果的年龄包括延迟而不是索引,如文档被改变和被抓取之间的时间)。该系统也被用来把页面渲染成图像;Percolator追踪网页和页面依赖的资源间的关系,因此当任何依赖资源变化时,页面可以被重新处理。

2.设计

Percolator对大规模数据的增量处理抽象为以下两个过程:对一个数据中心随机访问的ACID事务和组织增量计算的observers。

Percolator是由三个运行在每一个集群节点上的角色组成的,分别是Percolator worker、Bigtable tablet server以及GFS Chunkserver。所有的observer是连接到Percolator worker上的,它们负责对Bigtable中变化的列族进行扫描,而且可以对响应observers进行函数调用。观察者对事务的保证是通过向Bigtable tablet servers发送read/write RPC来实现的,而Bigtable tablet server反过来又向GFS Chunkserver 发送read/write RPC。整个系统是依赖于以下两个小服务的:timestamp oracle和轻量级锁服务。其中timestamp oracle提供严格的时间戳增量,这是正确操作集的隔离快照协议所必须的属性。而Percolator worker通过轻量级的锁服务使系统对更新通告的搜索更加高效。

从程序员的观点来看,Percolator数据中心是由少量表组成的。每一个表是由大量行列索引的数据单元组成的。每一个数据单元又包括一个值:一个无含义的字节数组。

由于需要在大规模系统上运行而且不需要对低延时的需求,我们设计了Percolator。不严格的延时需求让我们可以采用一种懒散的方法来清理因为事务在失败的机器上运行时遗留的锁。这种懒散而又执行过程简单的方法潜在地位事务提交时间延时数十秒。这种延时在运行于OLTP任务上的DBMS上是无法容忍的,但是在一个为处理web索引的增量系统上是可以的。Percolator在设计上是没有事务管理的中心结构的;而且它缺少一个全局的死锁检测器。这将增加冲突事务的延时,但是可以使系统可以扩展到数千台机器。

2.1 Bigtable概况

Percolator是建立在Bigtable分布式存储系统之上的。Bigtable提出了多维度的存储map:键是(行、列、时间戳)元组。Bingtable对每一行提供了查找和更新操作,而且Bigtable 的行事务可以确保单独的行之内的read-modify-write操作的原子性。Bigtable能够处理PB数量级的数据,在大量不可靠的机器上可靠地运行。

一个运行着的Bigtable是由一大堆tablet server所组成的,每一个tablet server负责为若干个tablet服务。一个master与tablet server通过指示他们装载或者卸载tablet来相互协作。每一个tablet作为一大堆只读文件的集合存储在Google SSTable中,而SSTable是存储在GFS上的,Bigtable对数据的保护是依赖于GFS的。Bigtable允许用户通过将一系列列族整合成一个locality group来控制表的性能特性。这些列族是存储在他们自己的SSTable上的,这样可以减小扫描开销。

Percolator是做在Bigtable之上的。Percolator维持了Bigtable的主要接口:数据是由Bigtable的行和列组成的,除此之外,Percolator元数据存储在特殊的列中(见图5)。Percolator的API和Bigtable非常类似:Percolator的库大部分是由Bigtable操作加上Percolator计算封装而成。Percolator的挑战就是Bigtable未能实现的:多行(multirow)事务和observer框架。

2.2 事务

Percolator提供具有ACID隔离快照语义特性的交叉行,交叉表事务。Percolator用户用一门语言(当前是C++)来写事务代码,而且用这些代码将对Percolar API的调用作混合处理。图2显示了一个简单的集群文档,它是利用对文档的内容作hash。在这个例子中,如果提交返回出错,那么事务将会发生冲突而且应该重新提交。Get()和Commit()的调用时阻塞的,并行性是由许多事务在一个线程池中同时运行来实现的。

clip_image002

对于增量数据处理而言,不需要任何强事务的优点,因此事务能够使用户更容易地处理系统的状态,并避免在生命力持久的数据中心中引入误差。举例来说,在一个基于web索引事务的系统中,程序员可以作以下假设:文档内容的hash值与表中的索引副本是具有一致性的。如果不采用事务,那么一个不合时宜的崩溃可能会导致永久的错误。事务也使构建索引表变得简单了,他们总是在更新而且要保持一致。请注意,这些例子都需要跨行的事务支持,而不是bigtable中已经提供的单行事务。

Percolator利用Bigtable 的时间戳对每一份数据保存有多个版本。对于提供隔离快照而言,多版本是有必要的,它能够在一些时间戳上从一个稳定的快照上读数据。而写则表现为不同的时间戳。隔离快照可以避免发生如下写写冲突:当事务A和事务B并发运行过程中,当写到同一个单元中去时,最多只能允许一个A和B中的其中一个提交。隔离快照不提供串行化,尤其是事务在隔离快照之下运行的时候,是服从写偏移(write skew)的。对于串行化策略来说,隔离快照的最大优势是使读更高效。因为任何时间戳表现为一个一致性的快照,对于一个数据单元的读取只需要对Bigtable实施一个给定时间戳的查找操作,也没有必要获取锁。图3描述了事务和隔离快照之间的关系。

clip_image004

Percolator是通过给客户提供一个库来访问Bigtable,而不是由自身访问存储来实现的,因此Percolator和传统的数据库管理系统相比,在实现分布式事务过程中,面临着一系列挑战。其他的并行数据库在管理磁盘访问时将锁机制整合到系统组件中:既然每一个节点已经可以通过协调来访问磁盘上的数据了,那么它可以授权访问请求的锁以及拒绝对违反访问请求的要求。

相比之下,Percolator的每一个结点都能够发出请求来直接修改Bigtable的状态:没有其他便捷的方法来拦截拥堵和指派锁。因此,Percolator必须明确地维持锁机制。在面对机器故障的时候锁必须是可用的;如果在两段任务提交的时候锁不可用了,那么系统将会错误地提交两个事务并因此可能发生冲突。由于上千台机器将会同时提出对锁服务的请求,因此锁服务必须提供高吞吐率的性能。除此之外,锁服务应该是低延时的;每一个Get()操作需要额外对锁的读取,我们希望将这个延时最小化。面对以上这些需求,锁服务需要作副本策略(可以免受因机器故障导致而导致的锁不可用)、分布式和负载均衡(为了处理高负载)、写到持久化的数据存储设备上。Bigtable本身满足以上所有的要求,因此Percolator将锁存储在和存放数据相同的Bigtable上特定的in-memory列中,当对数据进行访问的时候来读取或者修改bingtable行事务中相应的锁。

我们将来会对事务协议进行更为细致的考虑。图6描述的是Percolator事务的伪代码,图4描述的是在事务执行过程中Percolator数据和元数据的层次关系。这些系统中使用的元数据列在图5中有所描述。这些事务构造为start timestamp (line 6)请求一个 timestamp oracle,这将会决定由Get()操作可见的一致性快照。Set()调用是作为缓冲直到事务提交为止。对于缓冲区的写操作采用的基本方法是和客户端协同后两段提交。不同机器上的事务通过Bigtable tablet server上的行事务来实现交互。

两段提交的第一段提交操作是预写(prewrite),我们对所有正在写的单元进行加锁操作。(为了处理客户端失败,我们设计了一个最基本的锁primary lock,其机制将会在下面进行讨论)。事务通过读取元数据来检验每一个被写的单元的冲突操作。有以下两种类型的元数据冲突:1.如果事务在其开始时间戳之后发现了另一个写操作的记录,那将会终止(第32行);这是由防止写冲突的隔离快照来保证的。2.如果事务在任何时间戳上发现另一个锁,那也将会终止(第34行)。也有以下这种可能,当事务已经提交之后,锁暂时还没有释放,但是我们可能认为这也是一种异常情况而终止。如果没有冲突,我们在开始时间戳对每一个单元写锁和数据(第36-38行)。

如果没有单元发生冲突,那么事务将会被提交转而进行第二段操作。在第二段操作的开始的时候,客户从timestamp orale中获取提交时间戳(line 48)。然后,每一个客户端释放锁,以写记录替换锁的方式来确保对每一个读者来说其所写的内容是可见的。对读者来说这一条写记录表明这个数据在单元中是存在的;这包括一个指向开始时间戳的指针,帮助读者能够找到实际数据。一旦写是可见的(line 58),事务必须提交。

Get()操作首先校验锁的时间戳范围[0, start_timestamp],这个范围在事务的快照中是可见的(line 12)。如果一个锁被提出了,而另一个事务同时在写一个单元,那么读事务必须等到锁被释放才可以进行。如果没有发现冲突锁,Get()操作在时间戳范围中取最后更新的记录,然后返回这个记录。

客户端失败的情况发生(由于Bigtable确保了写锁的正确性,那么tablet server失败并不影响系统)将导致事务处理过程非常复杂。如果在事务正在提交的过程中一个客户端失败了,那么锁将会在遗漏在后面。Percolator必须清除这些锁,否则将会导致事务的失败。Percolator采取一种懒散的方法来清除这些锁:当一个事务A遇到了一个由事务B遗漏的冲突锁,A可能会认为B失败了,并清除该锁。

其实对于A来说,并不能明确地判定B发生了错误;因此我们必须避免A清除B的事务与一个事实上没有失败的B提交相同的事务之间的竞争。Percolator通过在每一个事务中为提交或者是清除操作指定一个单元作为同步点的方式来处理这种竞争。这个单元的锁就是前面提到的基本锁(primary lock)。A和B都是信任(agree on)基本锁的。无论是清除还是提交操作都需要修改基本锁;因为这个修改是基于Bigtable row而完成的,那么在清除和提交操作中只能二选一。特别地:当B提交之后,它必须校验它仍然占据着这个基本锁,而且用写记录来替换它。当A清除了B的锁后,A必须校验这个基本锁来确保B还没有提交;如果基本锁仍然存在,那就可以安全地清除它了。

当一个客户端在执行第二段提交时崩溃了,事务将会越过这个提交点,但是仍将表现出杰出的锁机制。我们必须对这些事务进行前滚(roll-forward)。对于一个事务而言,它能够通过检查基本锁来找出两种情况下的区别:如果基本锁被一条写记录取代了,那么写锁的事务一定已经提交了而且锁已经前滚了;否则它应该回滚(rolled back)(既然我们总是首先提交基本锁,那么我们能够保证如果基本锁还没提交时进行回滚是安全的)。对于前滚(roll forward)而言,事务用清除的方法来取代搁浅的锁。

既然清除操作在基本锁上是同步的,那么对于清除被客户端占有的锁也是安全的。但是,这也引发了性能上的下降,因为回滚会迫使事务终止。所以事务只有在怀疑一个锁是被死亡或者阻塞的worker占有时才会进行清理。Percolator使用简单的机制来判定另一个事务是否还活着。Running worker写一个令牌到Chubby 锁服务器中,来表明他们属于这个系统。其他workers能使用已经存在的令牌作为一个表明他们还活着的标志(当处理过程退出时,这些令牌会自动删除)。为了处理一个worker是活着的而不是正在工作的,我们在锁中额外写了经过时间(wall time);当锁所包含的经过时间太久之后,即使worker的令牌是有效的,也将会被清除。为了处理长时间的提交操作,workers在提交时定期地更新这个经过时间

2.3 时间戳

Timestamp oracle 是一个能够按照严格递增次序分派时间戳的服务器。由于每一个事务需要和timestamp oracle交互两次,这个服务必须能够稳定的承担。Oracle服务器通过写入最高的已经分派给稳定存储的时间戳去阶段性的生成一系列时间戳集合;通过这个时间戳集合,the oracle能够严格满足未来来自内存的请求。如果这个oracle重启,这些时间戳将会向前直接跳到最大已分派的时间戳(而不会往回)。为了节省RPC的开销(增加的事务延迟),每一个Percolator Worker会通过维持一个pending的RPC来批量事务处理这些请求。当oracle的负载变得越来越大,批处理自然会达到更好的效果。批处理在不会影响时间戳保证的情况下增长了oracle的可扩展性。我们的oracle每台单独的机器能够提供大概每秒两百万个时间戳。

事务协议采用严格递增的时间戳来保证Get()操作返回所有在事务开始时间戳之前已经提交的写入结果。为了说明它是如何提供这种保证的,我们考虑一个事务R在时间戳TR进行读操作,一个事务W在时间戳TW<TR时刻提交。我们将显示R能够看到W的写入。由于TW<TR,我们知道timestamp oracle分发TW在TR的之前或者同一批量处理中;因此,W请求得到TW会在R接受到TR之前. 我们知道R在得到它的时间戳TR之前是不能执行读操作的,而W写锁在请求他的提交时间戳TW之前。因此上面的处理保证了W必须在R做任何读操作之前已经写了所有的锁;R的读操作将会看到已经完全提交的写操作或者是锁限制,这里W将会阻塞知道锁被释放,或者W已经提交的写操作对R的Get()操作可见.

2.4 通告机制

事务让用户在维护不变量的同时需要对数据表做变更,但是用户也需要一种方法去引发和运行这些事务。在Percolator中,用户写code(“observers”)使得被数据表的更新所trigger,同时我们将所有的observers连接到一个同时运行在系统中每个tablet server的二进制程序上.每一个observer与Percalator注册一个函数和一个列的集合,同时Percolator在数据被写入这些列的任一行时调用这些函数。

Percolator应用被组织为一系列observers;每一个observer完成一个任务并通过写入一个table来创建更多的任务给后面流处理的observer. 在我们的索引系统中,a MapReduce通过运行loader事务将已经爬取的文档加载进入Percolator,这一过程中将引发文档process事务去给这些文档做索引(parse\抽取链接等等).文档process事务进而引发更多的事务,比如分类聚簇(clustering)等等.分类聚簇事务进而引发事务去导出更新的文档聚簇到服务的系统中(这里应该指接受搜索查询的系统)。

我们的通知机制和数据库的trigger机制以及在active database中的event机制比较类似,但是它不像数据库的trigger机制,他们不能被用来维持数据库的不变量.特别的,已经被引发的observer运行在一个和trigger write相独立的事务中,这使得trigger write和已经被引发的observer的写不是原子的。通知机制意图帮助构建一个增量的计算而不是去帮助维护数据的完整性。

语义和目的的不同让observer行为比起层叠trigger的复杂语义更加容易理解。Percalotor应用由几个oberser组成——Google的索引系统有大约10个observer。每一个observer在可执行的worker的main()中被明确的建立,所以哪一个observer可用的是很清楚的。有可能是的几个observer同时监控相同的列,但是我们避免了这个情况以至于当指定的列被写的时候observer将会运行什么。用户确实需要小心无限循环的的通知,但是Percolator不能对此有什么防止措施;用户需要组织一系列的observers去阻止无限的循环。

我们提供了一个保证:一个observer对每一个被监控的列的更新最多只有一个事务提交。这个说法是不对的,考虑对同一个列的多个写入或许只会引发相应的observer只被调用一次。我们将这个功能叫做Message Collapsing(消息压缩),由于他通过减少对许多消息回应的开销达到了减少计算的效果。比如,对于http://google.com阶段性的重新处理是足够的,而不需要每次我们发现有一个新的连接指向他时都去重新处理。

为了为通知提供这些语义,每一个被监控的列都有一个伴随的”acknowlegement”确认列与每一个observer对应, 它的内容包括observer最近开始运行的时间戳。当被监控的列被更新的时候,Percolator开始了一个事务去处理通知。这个事务去读被监控列和它相应的acknowledge确认列。如果被监控列是在最近一次确认之后写入更新的,则我们运行这个observer,并且同时设置这个确认列为我们这时开始的时间戳.否则,说明observer已经开始运行了,于是我们不需要再运行它。即如果Percolator偶然同时启动两个事务,他们将都会看见发生更新的通知,然后运行observer. 但是一个将会取消,因为他们将会在确认列时发生冲突。因此我们保证了至多只有一个observer将会为每一个通知提交。

为了实现通知机制,Percolator 需要有效的找到变更单元以便observer被其引发运行.这个搜索是复杂的,因为通知机制是极少的:我们的表有上万亿的单元,但是,如果跟上负载的情况下,只会有百万级别的通知。除此之外,监控代码是运行在大量分布在各个机器上的进程上,这意味着这个搜索工作必须是分布式的。

为了识别这些变更单元,Percolator维持了一个特殊的Bigtable 通知列,包括对于每一个变更单元的入口。当一个事务写了被监控的单元,他也将同时设置相应的通知单元。那些workers在上述通知列上执行一个分布式的搜索去找到变更的单元。在observer被引发并且事务被提交之后,我们在通知列上删除这个通知单元。由于这个通知列仅仅是一个Bigtable 列,而不是一个Percolator 列,因此他没有事务特性,仅仅只是起到一个提示的作用,它提示搜索程序去检查acknowledge列以便确认observer是否应该运行。

为了让搜索更加有效,Percolator 将这些通知列放在分离的Bigtable 位置组中,以至于让搜索程序仅仅需要读取几百万的改变单元而不是上亿万条的所有数据。每一个Percolator Workder 启动几个线程去搜索。对于每一个线程,这个worker为其分派 table的一部分,workder通过事先随机选择一个Bigtable tablet,然后随机选择一个key, 最后从这个key的位置开始搜索这个table. 由于每一个worker都在随机搜索这个table的一部分,我们担心两个workers在相同的row同时运行了observer。虽然这个行为并不会导致正确性问题,因为通知具有其事务特性,但是它是低效的。为了防止这个问题,每一个worker需要在搜索这个row之前从轻量级的所服务中获得一个锁。由于这仅是一个忠告,这个锁服务器不需要持续的状态,因此也具有很好的可扩展性.

这个随机的搜索方法需要一个特别的难点处理:当它开始部署的时候,我们发现搜索线程将会趋向于凑到该table的几个区域,从而减少了搜索的并行度。这个现象也发生在很常见的公共运输系统中,即”platooning”或者”bus clumping”(车辆扎堆),他在公汽是很慢的时候通常发生(因为交通或者很慢的载货载人)。因此每一个站点上旅客的数量随着时间而增长,加载延时将会变得更坏,并且更加减慢车辆的速度.同时,在那个慢车辆之后的任一个车辆将会加速,因为它在每一次停靠的时候将会加载更少的旅客。这样的结果就是经常一堆车辆同时到达一个停车点。我们的搜索线程和这是极其相似的,一个线程在运行observer后减速,然后在其后面的线程,快速的跳过这些rows,以至于和领头的线程相互扎堆,并且不能超越领头的线程,因为线程堆使得tablet 服务器过载了.为了解决这个问题,我们用一种公共交通系统所不能的方法修改我们的系统:当一个搜索线程发现他正在和另一个线程搜索同一个row,则他将在table中选择一个新的随机位置区搜索.进一步和交通系统类比,这些车辆(搜索线程)在我们的城市中为了避免扎堆在他们和前面的车隔得太近的时候,他们将随机传送到一个随机的站点(table中的位置).

最后,通知的经验让我们引入一种更加轻量级但是语义更弱的通知机制。我们发现当重复的相同页面需要同时处理时,每一个事务将会在试图引发同一个重复的cluster重新处理的时候遇到冲突。这让我们设计了一个没有事务冲突可能的方法去通知一个单元。我们通过写BigTable的notify列实现这个弱的通知机制。为了维持Percolator剩余部分这个事务的语义,我们严格限制这些弱的通知到一个特殊类型的列,它是不能被其他人写的,而只能被系统通知的。这个更弱的语义也意味着多个observer将会由于这个单一的语义在运行并且提交(虽然系统试图最小化这情况发生).这也成为了一个重要的管理冲突的功能;如果一个observer频繁在一个热点上冲突,可以将他分为两个在热点上被非事务通知连接的两个observer。

2.5讨论

Percolator 和基于MapReduce的系统比较起来,一个最低效的地方是每个工作单元RPC的数据量。当MapReduce做一个单一的大块读取到GFS系统,获取所有的数据需要10到100秒的时间,Percolator执行大约50个单一Bigtable操作去处理单一的文档。

额外的RPC发生的一个来源是在提交的过程中,当写一个锁,我们必须做一个read-modify-write操作需要两次Bigtable RPC:一个是读是否有冲突锁或者写入,另一个是写入新的锁。为了减少这个开销,我们修改了Bigtable 的API依靠添加条件突变使得读修改些操作只需要单一的RPC。许多条件突变的目的点是同一个tablet服务器,他们能够被在一起被批量处理成为一个单一的RPC,从而进一步减少了RPC总数量。我们通过延迟锁操作几秒钟来搜集他们到一个批处理中。因为锁是被并行获得的,这仅仅给每个事务增加了几秒的延迟。批处理也增加了冲突发生的时间窗口,但是在我们的低竞争环境下,这并不是一个问题。

当从一个表中读取数据的时候我们也执行相同的批处理:每一个读操作被延迟,使得他有机会能够和其他的作用于同一tablet服务器的操作一起形成一个批处理.在每一个读的时候都延迟一下,潜在的增加了事务的延时。一个最后的优化减轻了这个效果,即预读取。预读取利用的原理是,读在同一个row中两个或者更多的值和读取一个值几乎相同的代价。Bigtable必须从文件系统中读取整个SSTable,然后压缩它。Percolator试图预测,每一次一个列被读取,同一行中哪些列将会在后续事务中被读取。这个预测是基于过去行为的。预读取,和已经读取的缓存结合,几乎减少了10倍的系统读取Bigtable的数量

在Percolator的早期实现中,我们决定让所有的API调用阻塞,并且依靠在每个机器上运行成千上万的线程来提供足够的并行度,去维持一个好的CPU利用率。我们选择thread per request 模式主要在于让应用代码比在事件驱动模式下更加简单的编写。强制用户在每次读取数据的时候捆绑状态将会使得应用开发非常困难。thread per request 模式的经验就是,总体来说积极地方面:应用代码是简单的,我们在多核机器上获得了好的利用率,并且通过有意义和完整的堆栈跟踪crash调试是非常简单。在应用代码中我们几乎没有遇到比我们想象更坏的紊乱条件。这个方法最大的弱点是和linux内核可扩展性以及google基础架构相关的高线程数目。我们的内部内核开发小组能够部署补丁去解决这些内核问题。

image