Google的索引更新速度提升了100倍Word下载.docx
《Google的索引更新速度提升了100倍Word下载.docx》由会员分享,可在线阅读,更多相关《Google的索引更新速度提升了100倍Word下载.docx(25页珍藏版)》请在冰豆网上搜索。
![Google的索引更新速度提升了100倍Word下载.docx](https://file1.bdocx.com/fileroot1/2022-11/22/920f7c9f-2d10-4868-b99a-d5b4a67d5220/920f7c9f-2d10-4868-b99a-d5b4a67d52201.gif)
链接反向处理还要考虑复制品(意指内容相同的多个页面):
在必要的情况下指向一个复制品的链接应该被指向最高PageRank的页面(这样能增强最高PageRank的页面的评估)。
上述任务能够被表达为一系列的MR操作:
一个用于页面聚类分析,一个用于链接反向处理,等等。
在MR任务中维护不变量很简单,因为它是组织型计算(计算是按照一定逻辑和安排执行的,该并行的地方并行,该有序的地方有序),限制了计算的并行;
所有文档都是按照步骤依次完成一个个阶段的处理。
比如,当索引系统正附加锚文本到当前最高PageRank的URL时,我们不需要担心它的PageRank会并发改变:
之前的MR步骤已经完成了PageRank的计算,确定了它的PageRank。
现在考虑一下,在只重新抓取了一小部分文档时如何处理。
对于MR来说,仅仅对新抓取的页面执行作业是不够的,比如新来页面和已存在的老页面之间可能会有链接关系。
MR必须在整个库之上再次运行,也就是说,既包括新页面也包括所有老页面。
如果提供足够的计算资源,加上MR的可扩展性,这个途径确实是可行的,而且事实上,在Percolator问世前,Google的索引制造一直都是按这种方式。
然而,对整个库再处理的做法丢弃了之前的工作成果,延迟随着整个库的增长而成比例增长,而不是这一次更新的量。
【译者注】MR之所以不能有效重复利用上一次的工作成果,其中一个原因是索引制造的计算不是“mergablecomputation”。
mergable用数学公式表达就是:
在mergable函数y=f(x)中,对于x中任意的子集x1和x2,此公式可以表示为y=l(m(x1),n(x2))。
也就是说x中的数据无论怎么切分,都可以逐一基于上一次的结果进行计算。
译者找到另一篇incoop的论文,它用MR的视角来诠释了传统的MR为何不适合增量计算:
如图所示,假设Firstrun中Reduce的结果为A,在第二次计算时
(2)出现了,要为它执行增量计算,能不能直接将
(2)的值与A进行计算得到新的结果?
答案是不一定。
假如这个MR仅仅是对1到23各个数据进行求和,那就可以直接将A+
(2)得到新的值(这就是mergablecomputation)。
假如不是mergable(比如是将1、2、3、5的和与6到23的和相除),不仅1、2、3、5要重新计算,6到23都需要重新计算(因为MR仅仅保存了最终Reduce结果,丢失了中间计算结果)。
这就是为何Google要重复的执行全量的MR。
对于此问题,译者曾发邮件给作者DanielPeng,他的回复是:
Therearepartialresultsoftheindexcomputationthatareneededinthenextincrementalstep.
Inthemapreducecase,itisnoteasytosaveandretrievethesepartialresults.
Inpercolator,it’seasytostorethesepartialresultsandretrievethemforthenexttimetheyareneeded.
邮件中可以看到两个关键性词语。
第一个是“partialresults”,它强调了在索引计算中需要的不仅是上一轮的最终结果,更需要局部的、中间的计算结果,从某种意义上可以简单的理解为计算是unmergable的;
第二个是“noteasytosaveandretrieve”,这个突出了MR的软肋,如图中那么多中间结果,哪些需要保存、如何保存、如何检索等等,这些都是MR没有考虑过的事情。
与其花费很大精力去弥补此软肋,还不如有的放矢,针对新的场景重新设计一套架构,这就是Percolator选择的方案(但是在另一篇incoop的论文中,incoop的作者将MR优化改造成了能保存、检索中间结果的增量型计算架构,同样解决了问题,孰优孰劣,这里就不过多讨论了)。
索引系统理论上可以将数据存储在一个DBMS,就可以轻易实现只为单独的文档执行更新,而且可以使用事务来维护不变量。
然而,当今的DBMS不能处理数量如此庞大的数据:
Google的索引系统使用几千台机器存储了10PB的数据。
像BigTable这样的分布式存储系统可以扩展到我们需要的容量,但是在面对高并发更新时不能很好的帮助开发者维护不变量。
理想的处理系统是为增量处理优化定制的;
它应该允许我们维护一个非常大型的文档库,并且当每一个新文档被抓取时高效率的更新。
它可以高并发的处理很多小的更新,而且要为并发更新维护不变量。
论文下面的部分描述了这样一个特殊的增量处理系统:
Percolator。
Percolator提供在PB级别存储库中随机访问的能力。
随机访问允许我们单独的处理文档,避免全局的扫描(未优化的MR往往需要全局扫描)。
为了达到高吞吐量,它允许大量机器上的很多线程并发的对存储库执行更新,所以Percolator为开发者提供了遵循ACID的事务机制;
我们目前是通过快照隔离语义来实现。
为了解决并发问题,增量系统的开发者需要持续跟踪增量计算的状态。
Percolator提供观察者来帮助实现此任务:
每当一个用户指定的列发生变化时系统将调用的一段代码逻辑。
Percolator应用的结构其实就是一系列的观察者;
每个观察者完成一个任务并通过对table进行写操作,为“下游”的观察者创建更多的工作。
一个外部的处理会将初始数据写入table,以触发链路中的第一个观察者。
Percolator为增量处理量身定制,而且并不希望代替已存在的大多数数据处理任务的解决方案。
如果结果不能被分解为小而多更新(比如文件排序),最好用MR。
另外,一致性很强的场景下才需要使用Percolator:
否则Bigtable就足够了。
最后计算也要非常庞大:
计算很小不需要用到MR或Bigtable的情况下,DBMS就足够了。
在Google,Percolator的主要应用是实时构建web搜索索引。
索引系统使用Percolator之后,我们能在文件被抓取时就单独的处理它。
这几乎减少了100倍的平均文档处理延迟,而且搜索结果中文档的平均年龄也降低了50%(除了索引构建耗时,搜索结果的年龄还包含文档从改变到被抓取之间的时间)。
此系统也被用来将页面渲染为图片:
Percolator跟踪web页面和它们依赖的资源之间的关系,所以当任何依赖的资源改变时页面也能够被再处理。
2.设计
Percolator为执行大规模增量处理提供了两个主要抽象:
在随机访问库和观察者模式之上的ACID事务机制、增量计算过程的组织方法。
Percolator系统中集群的每台机器包含三个执行文件:
一个Percolator的worker,一个Bigtable的tablet服务器,和一个GFS的chunkserver(每台机器都同时扮演三种角色,而不是严格划分三个layer各自负责一种角色)。
所有的观察者都在Percolator的worker中,worker扫描Bigtable中发生改变的列(“通知”)并且就像本地方法调用一样调用对应的观察者的处理逻辑。
观察者通过发送读写RPC请求到Bigtable的tablet服务器来执行事务(可能发送到任意一台机器的tablet服务器),后者接着发送读写RPC请求到GFS的chunkserver。
系统也依赖两个小服务:
时间戳oracle服务(原文为timestamporacle,下文中所有“时间戳oracle”、“oracle”都是指此服务,译者注)和轻量锁服务。
时间戳oracle提供了严格的递增时间戳:
快照隔离协议需要依赖此属性。
Worker需要使用轻量锁服务来更加高效的搜索“脏”通知(“脏”原文dirty,意指某个数据发生了改变,等待后续处理,后续“脏”都为此含义,译者注)。
从开发者视角,一个Percolator库包含少量的table。
每个table是“cell”的集合(某一行的某一列就是一个cell)。
每个cell包含一个值,某类cell为支持快照隔离,会包含按时间戳索引的一系列的值。
有两个前提影响着Percolator设计,一是必须运行在大规模数据上,二是并不要求非常低的延迟。
不严格的延迟要求让我们采用了一个懒惰的途径来清理故障机器上被事务遗留下的锁。
这个途径虽懒惰但实现很简单,不过它可能会导致事务延缓提交几十秒钟。
这个延缓在DBMS运行OLTP任务时是无法接受的,但是在增量处理系统创建web索引时可以忍受。
另外,Percolator的事务管理缺乏一个中央总控:
尤其是它缺少一个全局死锁检测器。
这增加了事务冲突时的延迟,但是却可以帮助系统伸缩至几千台机器。
2.1Bigtable概览
Percolator建立在Bigtable分布式存储系统之上。
Bigtable对用户呈现了一个多维度排序的map:
map的keys是指(行、列、时间戳)元组。
Bigtable为每个行提供查询和更新操作,而且Bigtable的行事务能够支持单行的原子“读-修改-写”操作。
Bigtable处理PB级别数据,能够可靠地运行在大数量的(不可靠)机器上。
一个运行中的Bigtable包含一批tablet服务器,每个负责服务多个tablet(key空间内连续的域)。
一个master负责协调控制各tablet服务器的操作,比如指示它们装载或卸载tablet。
一个tablet在GoogleSSTable上被存储为一系列只读的文件。
SSTable被存储在GFS;
Bigtable依靠GFS来保护数据以防磁盘故障。
Bigtable允许用户控制table的执行特征,比如将一批列分配为一个localitygroup。
localitygroup中的列被存储在独立隔离的SSTable集合中,在其他列不需要被扫描时可以有效降低扫描成本。
基于Bigtable来构建Percolator,也就大概确定了Percolator的架构样式。
Percolator充分利用了Bigtable的接口:
数据被组织到Bigtable行和列中,Percolator会将元数据存储在旁边特殊的列中(见图5)。
Percolator的API和Bigtable的API也很相似:
Percolator中大量API就是在特定的计算中封装了对Bigtable的操作。
实现Percolator的挑战就是提供Bigtable没有的功能:
多行事务和观察者框架。
【译者预读】下面作者将介绍Percolator最核心的事务机制和通知机制,然而附图较少,陈述性的语言太多,尤其是事务部分的文字非常晦涩,还要结合源代码仔细阅读,不太适合读者快速的接收信息。
所以译者附上译者YY环节,无论是否准确,相信能帮助读者快速的接收信息,了解其大概全貌,继而再结合原文探究其细节。
下面附YY图一张:
图中描述了在一台Percolator的机器上有两个部分:
PercolatorWorker和Bigtable(GFS对大家来说是透明的、封装的,暂不考虑)。
Bigtable的职责无需多说,就是结构化存储,需要注意的是Percolator对它的使用。
对任何一种data(比如PageRank值),Percolator为它分配一张表,表中C:
data列存储的才是真实的data,其他的列全是为了服务于某种机制而附加上去的“元数据列”。
C:
data、C:
write、C:
lock是和数据读写有关的列,用于事务机制;
而C:
notify和C:
ack_xxx只用于通知机制。
各列中,
notify列仅仅是一个hint值(可能是个bool值),表示是否需要触发通知。
ack列是一个简单的时间戳值,表示最近执行通知的观察者的开始时间。
data列是KV结构,key是时间戳,value是真实数据,包含多个entry。
write列包含的是写记录,也是KV结构,key是时间戳,value是各个时间戳下曾经写入的值。
lock列也是KV结构,key是时间戳,value是锁的内容。
另一方面,PercolatorWorker由两部分组成,一是用于扫描的线程池,二是开发者编写的观察者(observer)。
下面按照图中序号大致描述一下Percolator中的流程:
step1:
由于某逻辑(此逻辑可以是Percolator之外的第一个往Percolator写入数据的初始化输入,也可以是中间过程里某个观察者逻辑往表中写入了数据,对应step6),需要往Bigtable的C:
data列中写入新的数据,而且可能是多个逻辑并发的写,此时可能会遇到“写/写冲突”,需要巧妙的利用write列和lock列,并利用它们KV结构中的时间戳帮助实现快照隔离,以实现ACID事务(细节可参考原文的事务章节)。
写入成功的事务会将新的写记录提交到write列;
并设置notify列(如图中“Changed!
”),通知此值已经发生了变化。
step2:
worker中各个扫描线程通过巧妙的分工(分工之巧妙、如何避免公交车凝结效应等,请看原文通知章节),尽可能高效的对特殊的notify列进行扫描(notify列是Bigtable中特殊的localitygroup,提升效率)。
step3:
扫描线程发现了step1设置的notify列(如图中“Changed!
”),需要通知相关的观察者来执行后续逻辑,但是为了避免非预期的并发问题导致多个线程同时扫描到此行,导致启动重复的观察者事务,这里扫描线程需要判断ack列,得知此行最近被观察者在哪个时间点做了处理,通过对write列和ack列中时间戳的分析,扫描线程可以“猜测”是不是可以启动观察者。
若可以启动,则将新启动的观察者的开始时间戳写入ack列(由Bigtable行事务保护),以便下次扫描。
即使在极小的概率下两个线程同时“猜测”可以启动,也会在写入ack列时发生冲突而避免重复。
step4:
各观察者会在PercolatorWorker中注册自己感兴趣的列。
扫描线程找到此次通知对应的观察者,启动并开始执行一个新事务。
事务中观察者执行自己的计算逻辑,并可能需要从其他table中查询必需的数据(对应step5,此过程可能涉及事务中的读/写冲突,读事务会查看write列和lock列来判断是否冲突,冲突时读事务会等待,直到写事务结束,细节请参考原文和图6源码)。
在计算结束后,输出的结果需要写入另一个table(对应step6,此时会遇到和step1类似的写/写冲突)。
提交成功的写操作将触发step1,依次循环直至不再写入任何列或没有任何观察者需要被触发。
以上是译者YY的大致流程,希望可以帮助读者参考以窥全貌,继而结合原文、源代码细化阅读。
事务部分的源码其实非常值得精度,只是原文的事务章节陈述性语句太多而且晦涩难懂、逻辑跳跃。
强烈建议读者阅读图6甚至更多的源码,以了解一个巧妙的分布式事务方案。
译者也提供了比较通俗的总结环节来帮助读者理解。
2.2事务
Percolator利用ACID快照隔离语义提供了跨行、跨表事务。
Percolator的用户可使用必要的语言(当前是C++)编写它们的事务代码,然后加上对PercolatorAPI的调用。
图2表现了一段简化的基于内容hash的文档聚类分析程序。
在这个例子中,如果Commit()返回false,事务冲突了(可能两个有内容hash相同的URL被同时处理)需要在回退后被重新尝试。
对Get()和Commit()的调用是阻塞式的;
通过在一个线程池里同时运行很多事务来增强并行。
尽管不利用强事务的优势也可能做到数据增量处理,但事务使得用户能更方便的推导出系统状态,避免将难以发现的错误带到长期使用的存储库中。
比如,在一个事务型的web索引系统中,开发者能保证一个原始文档的内容hash值永远和索引复制表中的值保持一致。
而没有事务,一个不合时的冲击可能造成永久的不一致问题。
事务也让构建最新、一致的索引表更简单。
注意我们说的事务指的是跨行事务,而不是Bigtable提供的单行事务。
Percolator使用Bigtable中的时间戳维度,对每个数据项都存储多版本,以实现快照隔离。
在一个事务中,按照某个时间戳读取出来的某个版本的数据就是一个隔离的快照,然后再用一个较迟的时间戳写入新的数据。
快照隔离可以有效的解决“写-写”冲突:
如果事务A和B并行运行,往某个cell执行写操作,大部分情况下都能正常提交。
任何时间戳都代表了一个一致的快照,读取一个cell仅需要用给出的时间戳执行一个Bigtable查询;
获取锁不是必要的。
图3说明了快照隔离下事务之间的关系。
传统PDBMS为了实现分布式事务,可以集成基于磁盘访问管理的锁机制:
PDBMS中每个节点都会间接访问磁盘上的数据,控制磁盘访问的锁机制就可以控制生杀大权,拒绝那些违反锁要求的访问请求。
而Percolator是基于Bigtable的,它不会亲自控制对存储介质的访问,所以在实现分布式事务上,与传统的PDBMS相比,Percolator面对的是一系列不同的挑战。
相比之下,Percolator中的任何节点都可以发出请求,直接修改Bigtable中的状态:
没有太好的办法来拦截并分配锁。
所以,Percolator一定要明确的维护锁。
锁必须持久化以防机器故障;
如果一个锁在两阶段提交之间消失,系统可能错误的提交两个会冲突的事务。
锁服务一定要高吞吐量,因为几千台机器将会并行的请求锁。
锁服务应该也是低延迟的;
每个Get()操作都需要申请“读取锁”,我们倾向于最小化延迟。
给出这些需求,锁服务器需要冗余备份(以防异常故障)、分布式和负载均衡(以解决负载),并需要持久化存储。
Bigtable作为存储介质,可以满足所有我们的需求,所以Percolator将锁和数据存储在同一行,用特殊的内存列,访问某行数据时Percolator将在一个Bigtable行事务中对同行的锁执行读取和修改。
我们现在考虑事务协议的更多细节。
图6展现了Percolator事务的伪代码,图4展现了在执行事务期间Percolator数据和元数据的布局。
图5中描述了系统如何使用这些不同的元数据列。
事务构造器向oracle请求一个开始的时间戳(第六行),它决定了Get()将会看到的一致性快照。
Set()操作将被缓冲(第七行),直到Commit()被调用。
提交被缓冲的Set操作的基本途径是两阶段提交,被客户端协调控制。
不同机器上基于Bigtable行事务执行各自的操作,并相互影响,最终实现整体的分布式事务。
【译者注】十分抱歉此段截图狭长导致格式不畅,而且此章内容十分晦涩,需要结合图和文字一起阅读,所以建议读者打开原文PDF中的图解(尤其是图6的源码)和下文对照阅读。
并建议阅读本章结束的【译者总结】的补充内容。
在Commit的第一阶段(“预写”,prewrite),我们尝试锁住所有被写的cell。
(为了处理客户端失败的情况,我们指派一个任意锁为“primary”;
后续会讨论此机制)事务在每个被写的cell上读取元数据来检查冲突。
有两种冲突场景:
如果事务在它的开始时间戳之后看见另一个写记录,它会取消(32行);
这是“写-写”冲突,也就是快照隔离机制所重点保护的情况。
如果事务在任意时间戳看见另一个锁,它也取消(34行):
如果看到的锁在我们的开始时间戳之前,可能提交的事务已经提交了却因为某种原因推迟了锁的释放,但是这种情况可能性不大,保险起见所以取消。
如果没有冲突,我们将锁和数据写到各自cell的开始时间戳下(36-38行)
如果没有cell发生冲突,事务可以提交并执行到第二阶段。
在第二阶段的开始,客户端从oracle获取提交时间戳(48行)。
然后,在每个cell(从“primary”开始),客户端释放它的锁,替换锁为一个写记录以让其他读事务知晓。
读过程中看到写记录就可以确定它所在时间戳下的新数据已经完成了提交,并可以用它的时间戳作为“指针”找到提交的真实数据。
一旦“primary”的写记录可见了(58行),其他读事务就会知晓新数据已写入,所以事务必须提交。
一个Get()操作第一步是在时间戳范围[0,开始时间戳]内检查有没有锁,这个范围是在此次事务快照所有可见的时间戳(12行)。
如果看到一个锁,表示另一个事务在并发的写这个cell,所以读事务必须等待直到此锁释放。
如果没有锁出现,Get()操作在时间戳范围内读取最近的写记录(19行)然后返回它的时间戳对应的数据项(22行)。
由于客户端随时可能故障,导致了事务处理的复杂度(Bigtable可保证tablet服务器故障不影响系统)。
如果一个客户端在一个事务被提交时发生故障,锁将被遗弃。
Percolator必须清理这些锁,否则他们将导致将来的事务被非预期的挂起。
Percolator用一个懒惰的途径来实现清理:
当一个事务A遭遇一个被事务B遗弃的锁,A可以确定B遭遇故障,并清除它的锁。
然而希望A很准确的判断出B失败是十分困难的;
可能发生这样的情况,A准备清理B的事务,而事实上B并未故障还在尝试提交事务,我们必须想办法避免。
现在就要详细介绍一下上面已经提到过的“primary”概念。
Percolator在每个事务中会对任意的提交或者清理操作指定一个cell作为同步点。
这个cell的锁被称之为“primary锁”。
A和B在哪个锁是primary上达成一致(primary锁的位置被写入所有cell的锁中)。
执行一个清理或提交操作都需要修改pr