pregel -分布式图计算模型

 

Abstract
许多实际应用问题中都涉及到大型的图算法。比如网页链接关系和社会关系图等。这些图都有相同的特点:规模超大,常常达到数十亿的顶点和上万亿的边。这么大的规模,给需要在其上进行高效计算的应用提出了巨大的难题。在这篇论文中,我们将提出一种适合处理这类问题的计算模式。将程序用一系列的迭代来描述(Programs are expressed as a sequence of iterations),在每一次迭代中,每一个顶点都能接收来自上一次迭代的信息,并将这些信息传送给下一个顶点,并在此过程中修改其自身的状态信息,以该顶点为起点的出边的状态信息,或改变整个图的拓扑结构。这种面向顶点的方法足够的灵活,可以用来描述一系列的算法。这种计算模式被设计的足够高效,可扩展,和足够的容错,并在有上千台的计算节点的集群中得以实现。这种模式中隐式的同步性(implied synchronicity)使得它对程序的确认变得简单。分布式相关的细节已经被一组抽象的API给隐藏。而展现给人们的仅仅是一个表现力很强,很容易编程的大型图算法处理的计算框架。
Keywords
分布式计算,图算法
1.Introducetion
Internet使得Web graph成为一个人们争相分析和研究的热门对象。Web 2.0更是将对社会关系网的关注推向高潮。同时还有其他的大型图对象(如交通路线图,报纸文献,疾病爆发路径,以及科学研究的发表文章中的引用关系等),也已经被研究了很多年了。同时也有了许多相应的应用算法,如最短路径算法,page rank理论演变而来的图相关算法等等。同时还有许多其他的图计算问题也有着相当的实际价值,如最小切割,以及连通分支等相关问题。
事实证明,高效的处理大型图对象的计算是一件极具挑战性的事情。图算法常常暴露出类似不高效的本地内存访问,针对每个顶点的处理过少,以及在计算过程中改变并行度等问题。分布式的介入更是加剧了locality的问题,并且加剧了在计算过程中机器发生故障而影响计算的可能性。尽管大型图形无处不在,其商业应用也非常普及,但是一种通用的,适合各种图算法的大型分布式环境的实现到目前还不存在。
要实现一种大型图计算的算法通常意味着要在以下几点中作出选择:

1.   为特定的图应用定制相应的分布式框架。这通常要求对于新的图算法或者图相关的应用,就需要做大量的重复实现,不通用。
2.   基于已有的分布式计算平台,而这种情况下,通常已有的分布式计算框架并不是完全适合图计算的应用。比如mapreduce就是一个对许多大规模应用都非常合适的计算框架。它也常常被用来解决大型图的问题,但是通常对图算法来说都不是最优的解决方案,常常也不是最合适的方案。对数据处理的基本模式,通常有聚合(facilitate aggregation)以及类SQL的查询方式,但这些扩展方式通常对大型图计算这种消息传递模型来说并不理想。
3.   使用单机的图算法库,如BGL,LEAD,NetworkX,JDSL,Standford GraphBase,FGL等,但这种单机的方式就对图本身的规模有了很大的限制。
4.   使用已有的并行图计算系统。并行的BGL和CGM 图计算库就提供了并行图计算的方式。但是对容错等其他大规模分布式系统中非常重要的一些方面的支持却并不好。
以上的这些选择都或多或少的存在一些局限性。为了解决大型图的分布式计算问题,我们搭建了一套可扩展,有容错机制的平台,该平台提供了一套非常灵活的API来描述各种各样的图计算。这篇论文将描述这套名为Pregel的系统,并分享我们的经验。
对Pregel计算系统的灵感来自Valiant提出的Bluk Synchronous Parallell mode。Pregel计算由一系列的迭代(iterations)组成,每一次的迭代被我们称之为”superstep”。在每一次的superstep中,计算框架都会invoke用户针对每个顶点自定义的函数,在概念上,这个过程是并行的。该用户自定义函数定义了对于一个顶点V以及一个superstep S中需要执行的操作。该函数可以将前一轮迭代(S-1)中发送给V的message读入,并将该message通过下一轮迭代(S+1)发送给另外的顶点,并且在此过程中修改V的状态以及其出边的状态。message通常通过顶点的出边发送,但一个消息可能会被发送到许多已知ID的特定的顶点上去。
这种面向顶点的计算方法很容易让人联想起mapreduce,因为他们都让用户只需要关注其本身的执行逻辑,分别独立的执行各自的处理过程,而系统都将负责处理其余剩余的大量复杂的事情。这种计算模式非常的适合分布式化的实现:它没有限制每个superstep的执行顺序,所有的通信都仅限于S到S+1之间。
这种计算模式在同步性上的特点使得在实现算法时推算程序执行的语义变得简单,并且能够在Pregel系统层面保证程序在异步系统中是天然的对死锁以及临界资源竞争免疫的。理论上,Pregel程序的性能应该即使在与足够并行化的异步系统的对比中都有一定的竞争力。因为通常情况下图计算的应用中顶点的数量要远远大于机器的数量,所以必须要平衡各机器之间的负载,已达到各个superstep之间的同步不会增加额外的延迟。
本文接下来的结构如下:Section2主要描述Pregel模式;Section3描述其C++的API;Section4讨论实现方面的情况,如性能和容错等;Section5中将例举几个实际应用;Section6将提供性能的对比数据;最后是其他一些相关工作和将来的方向。
2.Model of Computation
在Pregel计算模式中,输入是一个有向图,该有向图的每一个顶点都有一个相应的由String描述的vertex identifier。每一个顶点都有一些属性,这些属性可以被修改,其初始值由用户定义。每一条有向边都和其源顶点关联,并且也拥有一些用户定义的属性和值,并同时还记录了其目的顶点的ID。
一个典型的Pregel计算过程如下:读取输入,初始化该图,当图被初始化好后,运行一系列的supersteps,每一次superstep都在全局的角度上独立运行,直到整个计算结束,输出结果。
在每一次的superstep中,顶点的计算都是并行的,每一次执行用户定义的同一个函数。每个顶点可以修改其自身的状态信息或以它为起点的出边的信息,从前序superstep中接受message,并传送给其后续superstep,或者修改整个图的拓扑结构。边,在这种计算模式中并不是核心对象,没有相应的计算运行在其上。
算法是否能够结束取决于是否所有的顶点都已经“vote”标识其自身已经达到“halt”状态了。在superstep 0,所有顶点都会被置于active状态,每一个active的顶点都会在计算的执行中在某一次的superstep中被计算。顶点通过将其自身的status设置成“halt”来表示它已经不再active。这就表示该顶点没有进一步的计算需要进行,除非被额外的触发其他的运算,而Pregel框架将不会在接下来的superstep中计算该顶点,除非该顶点收到一个其他superstep传送的消息。如果顶点接收到message,该message将该顶点重新置active,那么在随后的计算中该顶点必须在此deactive其自身。整个计算在所有顶点都达到“inactive”状态,并且没有message在传送的时候宣告结束。这种简单的状态机制在下图中描述:


整个Pregel程序的输出是所有顶点输出的集合。通常来说Pregel程序的输出是跟输入时同构的有向图,但是并非一定是这样,因为在计算的过程中,可以对顶点和边进行添加和删除。比如一个聚类算法,就有可能从一个大图中选出满足需求的几个不相连的点;一个对图的挖掘算法就可能仅仅是输出了从图中挖掘出来的聚合数据等。

Figure2举了这种情况的简单示例:给定一个强连通图,图中每个顶点都包含一个值,该值保存的是该顶点的所有相邻节点中值最大的。在每一次的superstep中,任何一个顶点在从message中接收到一个比其当前值更大的值,那么就将这个值传送给其所有的相邻顶点。当某一次superstep中已经没有顶点更新其值,那么这轮计算就宣告结束。

 


我们选择了一种纯message传递的模式,忽略远程数据读取和其他方式共享内存的方式,这样做有两个原因。第一,messages的传递有足够高效的表达能力,不需要远程读取(remote reads)。我们从来没有发现过在哪种图算法中消息传递不够高效的。第二是出于性能的考虑。在一个集群环境中,从远程机器上读取一个值是会有很高的延迟的,这种情况很难避免。而我们的消息传递模式通过异步和批量的方式传递消息,可以缓解这种远程读取的延迟。
图算法其实也可以被写成是一系列的链式mapreduce作业。我们选择了另外一种不同的模式的原因在于可用性和性能。Pregel将顶点和边在本地机器进行运算,而仅仅利用网络来传输信息,而不是传输数据。而MapReduce本质上是面向函数的,所以将图算法用mapreduce来实现就需要将整个图的状态从一个阶段传输到另外一个阶段,这样就需要许多的通信和随之而来的序列化和反序列化的开销。另外,在一连串的mapreduce作业中各阶段需要协同工作也给编程增加了难度,这样的情况能够在Pregel的各轮superstep的迭代中避免。
3.C++ API
这一节主要介绍Pregel C++ API中最重要的几个方面,而相关的其他机制暂时忽略。编写一个Pregel程序需要继承Pregel中已预定义好的一个基类——Vertex类(见Figure3)。

 


该类的template参数中定义了三个值类型参数,分别表示顶点,边和messages。每一个顶点都有一个值。这种设计方式可能看上有有一些局限性,但用户可以用protocol buffer来管理增加的其他定义和属性。而边和message类型的作用比较相似。
用户覆写Vertex类的virtual函数Compute(),该函数会在每一次superstep中对每一个顶点进行调用。预定义的Vertex类方法允许Compute()方法查询当前顶点的信息,其边的信息,并send message到其他的顶点。Compute()方法可以通过调用GetValue()方法来得到当前顶点的值,或者通过调用MutableValue()方法来修改当前定点的值。同时还可以通过边定义方法来get和set以该顶点为起点的边对应的值。这种状态的修改时立时生效的。由于这种可见性是仅限于修改顶点的时候,所以在对不同的顶点进行并行的数据访问时不存在数据的竞争关系。
顶点的值和其对应的边的值是仅有的在superstep之间per-vertex值。将由计算框架管理的图状态限制在一个单一的顶点值或边值的这种做法,可以使得估算计算轮次,图分布化以及错误恢复变得简单。
3.1 Message Passing
顶点之间的通信是通过直接的message传递的方式,每一个message都包含了message的值和目的顶点的名称。Message值的数据类型则由用户通过Vertex类的template参数来制定。
一个顶点可以send任意多次的messages到一个superstep。所有通过在superstep S+1中调用顶点V的Compute()方法从superstep S发送给顶点V的messages都可以通过一个迭代器来使用。在该迭代器中并不保证messages的顺序,但是可以保证message一定会被传送并且不会重复。
一种通用的使用方式为:对一个顶点V,遍历其自身的出边,向每条出边发送message到该边的目的顶点,如下图Figure4中所表示PageRank算法那样。

 


但是,dest_vertex必须不是顶点V的相邻顶点。一个顶点可以从当前superstep的前几次superstep中获取其非相邻顶点的信息,或者顶点id可以隐式的得到。比如,图可能是一个分团问题,有一些well-known的顶点IDs(从V1到Vn),这些顶点的信息,可以不用显示的包含在包含在边中。
当任意一个message的目标顶点不存在时,便执行用户定义的handlers,,一个handler可以创建该不存在的顶点或从该源顶点中删除这条边。
3.2 Combiners
当一个顶点发送message,尤其是到另外一台机器的顶点时,会产生一些开销。这种情况用户可以自己想办法来缓解。比方说,假如Compute() 收到许多的int messages,而它仅仅关心的是这些值的和,而不是每一个int的值,这种情况下,系统可以将多个messages合并成一个message,该message中仅包含和的值,这样就可以削传输和缓存的开销。
Combiners在默认情况下并没有被开启,这是因为要找到一种对所有顶点的compute()函数都合适的Combiner是不可能的。而用户如果想要开启Combiner的功能,可以继承Combiner类,覆些其virtual函数Combine()。框架并不会确保哪些message会被combine而哪些不会,也不会确保传送给Combine()的值和combining操作的执行顺序。所以Combiner应该仅仅被用来减少关联和交互的开销中使用。
3.3 Aggregators
Pregel的aggregator是一种提供全局通信,监控和数据的机制。每一个顶点都可以在superstep S中向一个aggregator提供一个数据,系统会使用一种聚合操作来负责聚合这些值,而产生的值将会对所有的顶点在superstep S+1中可见。如min,max,sum等或者一些字符串类型的值。
Aggregators可以用来做统计。例如,一个sum aggregator可以用来统计每个顶点的出度,最后相加就为整个图的边的条数。更多复杂的类似操作可以产生一些历史统计信息。
Aggregators可以用来做全局协同。例如,一些列的Compute()可以在某些supersteps()中被调用,直到所有的调用产生的结果满足某种条件后,另一批Compute()才被执行。又比如一个min和max aggregator,可以用于顶点ID,用来选择某个顶点在整个计算过程中扮演某种角色等。
要定义一个新的aggregator,用户可以继承预定义的Aggregator类,并定义在第一次接收到输入值后如何初始化,以及如何将接收到的多个值最后reduce成一个值。Aggregator操作也将带来关联和交互的开销。
默认情况下,一个aggregator仅仅会reduce从一个superstep来的输入,但是对于用户来说,也可以定义一个sticky aggregator,它可以从所有的supersteps中接收数据。这是非常有用的,比如要保存全局的边条数,那么就仅仅在增加和删除边的时候才调整这个值了。
3.4 Topology Mutations
有一些图算法可能需要改变图的整个拓扑结构。比如一个聚类算法,可能会需要将每个聚类替换成一个单一顶点,又比如一颗最小生成树算法会将所有的顶点删除,只保留树的边。就好像用户定义的Compute()函数能send message一样,它同样可以在图中增添和删除边或顶点。
多个顶点有可能会在同一个superstep中产生请求冲突(比如两个请求都要增加一个顶点V,但初始值不一样),Pregel中用两种机制来决定如何调用:局部有序和handlers。
和messages一样,拓扑的改变会在请求发出以后在superstep中变得很高效。在首先删除某些顶点或边的superstep中,会先删除边,后删除顶点,因为顶点的删除通常也意味着其边的删除。而在增加顶点或边的superstep中,就需要先增加顶点后增加边,并且所有的拓扑改变都会在Compute()函数调用前完成。这种局部有序就决定了大多数的访问冲突的时序。
而其他的访问冲突就需要通过用户定义的handlers来解决。如果在一个superstep中有多个请求需要创建一个相同的顶点,在默认情况下系统会随便挑选一个请求,但有特殊需求的用户可以定义一个更好的冲入解决策略,用户可以在Vertex类中通过定义一个适当的handler函数来解决冲突。同一种handler机制将被用于解决由于多个顶点删除请求或多个边增加请求或删除请求而造成的冲突。我们委托handler来解决这种类型的冲突,从而使得Compute()函数变得简单,而这样同时也会限制handler和Compute()的交互,但这种情况在应用中还没有发生过。
我们的协同机制比较懒,全局的拓扑改变在它没有起到任何影响之前不需要进行同步。这种设计的选择是为了优化流式处理。对顶点V的访问冲突由V自己来处理。
Pregel同样也支持纯local的拓扑改变,例如一个顶点添加或删除其自身的出边或删除其自己。Local的拓扑改变不会引发冲突,并且顶点或边的本地增减能够立即生效,很大程度上简化了分布式的编程。
3.5 Input and Output
对于图的表现方式有多种,有text文件的方式,一系列储存在关系数据库中的顶点的方式,或者Bigtable中的一个行。为了避免规定死一种特定文件格式,Pregel将从一个输入文件中解析出图结构的任务下放到了对图的计算任务中。类似的,计算的输出可以以任何一种格式和存储方式输出,只要应用程序本身能够认识就行。Pregel library本身提供了对多种文件格式的readers和writers,但是用户可以通过继承Reader和Writer类来定义他们自己的读写方式。
4.Implementation
    Pregel是为Google的集群架构而设计的。每一个集群都包含了上千台机器,这些机器都分列在许多机架上,机架之间有这非常高的内部通信带宽。集群之间是内部互联的,但地理上是分布着的。
    应用程序通常被执行在一个集群管理系统上,该管理系统会通过调度作业来优化集群资源的使用率,有时候会通过kill task或将task move到其他机器上去。该系统中提供了一个名字服务系统,所以各任务间可以通过逻辑名称来各自标识自己云新在哪台机器上。持久化的数据被存储在GFS或Bigtable中,而临时文件则存储在本地磁盘中。
4.1 Basic Architecture
    Pregel library将一张图分解成许多的partitions,每一个partition包含了一些顶点和以这些顶点为起点的边。将一个顶点分配到某个partition上去取决于该顶点的ID,这样也表示即使在别的机器上,也是可以通过顶点的ID来知道该顶点是属于哪个partition,甚至在顶点尚不存在时就可以知道其所属的partition。默认的partition函数仅仅通过将顶点ID mod N,来产生,N为所有partition总数,但是这个partition函数用户可以覆盖。
    将一个顶点分配给worker机器是整个Pregel中主要对分布式不透明的地方。有些应用程序对默认的分配策略可以支持的很好,但是定义用户自定义的分配函数可以减少由于分配策略带来的开销。比如,一种典型的启发式Web graph就可以将来自同一个站点的网页顶点的数据分配到同一台计算节点上进行计算。
    在不考虑出错的情况下,一个Pregel程序的执行过程分为如下几个步骤:
1.   用户定义的程序分别在集群中的某些计算节点上开始执行。其中有一个程序将会作为master。这时候还并没有分配图本身的任何一个部分到计算节点上运算,而只是用来做计算节点 activity之间的协同。计算节点利用集群管理系统中提供的名字服务来定位master进程是运行在哪台计算节点上,并发送注册信息到这个master进程中。
2.   Master进程决定了对这个图需要多少个partition,并分配图的一个或多个partitions到计算节点上进行运算。一个计算几点上有多个partition的情况下,可以提高partitions间的并行度,更好的load balance,并产生更高的执行效率。每一个计算几点都需要maintain在其之上的图的那一部分的状态,对该部分中的顶点执行Compute()函数,并管理从其他机器上传过来的messages。每一个计算节点都知道该图的计算在所有计算节点中的分布。
3.   Master进程将用户的输入中的一部分分配到集群的计算节点中,这些输入被看做是一系列的records,每一条records都包含一些顶点和边。对input的分割和整个graph是垂直正交的,而且通常都是基于文件边界。如果一个计算节点加载的顶点刚好是数据这个计算节点所分配到的图的partition,那么相应的对图结构的修改就会立刻生效。否则,该计算节点就会将需要发送到属于其partition但在其他机器上运行的顶点的messages写入队列,并发送。当所有的input都被load完成后,所有的顶点就都已经处于active状态,等待被执行Compute()了。
4.   Master给每个计算节点发指令,让其运行一个superstep,计算节点轮询在其之上的顶点,每个partition会launch一个线程来做轮询。计算节点调用每个active顶点的Compute()函数,接收从上一次superstep来的messages。Messages是被异步发送的,这是为了使得计算,通信变得并行化,但是messages的发送会在每一次superstep结束前被delivered。当一个计算节点完成了其所有的计算后,会告诉master当前该计算节点上在下一次superstep中还有多少active节点。这个过程被不断的重复,只要有顶点还处在active状态,或者还有messages在传输。
5.   当所有的顶点都处于halt状态,master给所有的计算节点发指令,让计算节点保存那一部分的计算结果。
 

4.2 Fault tolerance
    容错是通过checkpoint来保证。在每一次的superstep的开始阶段,master会instruct计算节点,让它保存计算节点上partitions的状态到持久存储设备,包括顶点值,边值,以及接收到的messages。Master也会阶段性的保存aggregator的值。
    计算节点的出错时通过普通的ping消息,master通过ping来确定计算节点是否出错。如果一个计算节点在一段时间内没有收到ping消息,该计算节点上的计算就终止。如果master在一定时间内没有收到计算节点的反馈,就会认为该计算节点发生了故障。
    当一个或多个计算节点发生故障,被分配到这些计算节点的partitions的状态信息就丢失了。Master重新分配这些partition到其他可用的计算节点上,这些计算节点会在superstep S开始时,从最近的checkpoint中重新加载这些partition的状态信息。该superstep可能是在失败的计算节点上最后运行的superstep S’的好几个step之前的阶段,此时失去的几个superstep将需要被重新执行。对checkpoint的频率也要基于一定的策略,这样才能平衡checkpoint的开销和恢复执行的开销。
    Google内部正在开发一个叫做Confined recovery的checkpoint策略,用来提高checkpoint和恢复执行的开销效率。除了基本的checkpoint策略,计算节点同时还会对其发送出去的messages进行日志记录。Recovery就会被限制在lost partition上,该partition从checkpoint中进行恢复。系统会通过回放logging messages日志重新计算失去的supesteps到S’阶段。通过这种方式,就可以节省在恢复partition计算时消耗的资源和时间,并可以减少恢复partition计算时的延迟。对发送出去的messages进行保存会产生一定的开销,但是通常计算节点上的磁盘读写带宽不会让这种保存操作成为瓶颈。
4.3 Worker implementation
    一个计算节点机器将分配到其之上的graph partition的状态保存在内存中,概念上讲,这是一个类似从顶点ID到其顶点状态的Map,其中顶点状态包括如下信息:该顶点的当前值,一个以该顶点为起点的边列表,每条边的值,一个保存了incoming messages的队列以及一个记录当前是否active的标志位。当该计算节点运行一次superstep时,会loop through所有的顶点,并调用每个顶点的Compute()函数,给该函数传当前顶点的值,一个incoming messages的iterator和一个出边的iterator。这里没有对入边的访问,原因是每一条入边其实都是其起点的所有出边的一部分,通常在另外的机器上。
    出于性能的考虑,标志顶点是否为active的标志位是和incoming messages 队列分开保存的。另外,当只有一个顶点值和边值存在时,会有两个顶点active flag和incoming messages队列存在,一个是for 当前superstep,另一个for下一次的superstep。当一个计算节点在处理其superstep S时,同时还会有另外一个线程在从其他计算节点的同一个superstep步骤接收messages。由于顶点接收的是superstep s-1的messages,那么对superstep S和superstep S+1的messages就必须分开保存。类似的,顶点V接收队列表示V将会在下一个superstep中处于active,而不是为当前这一次的superstep接收的。
    当Compute()请求send一个message到其他的顶点时,计算进程首先确认是否目标顶点是owned by 远程的计算节点,还是在当前计算节点本地。如果是在远程的计算节点上,那么messages就会被缓存,当buffer size达到一个阈值,才发送到远程计算节点。如果是在当前计算节点,那么就可以做相应的优化:message就会直接被放到目标顶点的incoming message队列中。
    如果用户使用了Combiner,那么在message被加入到outgoing message或者有message加入到incoming message队列时是会执行combiner函数。后一种情况并不会节省网络开销,但是会节省本地存储的开销。
4.4 Master implementation
    Master主要负责的是协同各个计算节点之间的工作,每一个计算节点在其注册到master上来的时候会分配到一个唯一的ID。Master内部维护着一个计算节点列表,表明当前哪些计算节点出于alive状态,该列表中就包括每个计算节点的ID和地址信息,以及哪些计算节点上被分配到了整个图的哪一部分。Master中这些信息的数据结构大小取决于整个图被分成多少个partition,而不是取决于图中的点和边的数目。因此,一台single master也足够用来协调对一个大graph的计算。
    绝大部分master的工作,如input ,output,computation,saving以及resuming from checkpoint,都将会在一个叫做barriers的地方终止。Master会在每一次操作都会发送相同的指令到所有的计算节点,然后等待从每个计算节点的response。如果任何一个计算节点失败了,master便进入恢复模式(如4.2中描述)。如果barrier同步成功,master便会进入下一个处理阶段,例如master增加全局superstep的index,并进入下一个superstep。
    Master同时还保存着整个计算过程的统计数据,以及整个graph的状态,如total size of the graph,out-degrees的柱状图,处于active顶点的个数,message在当前superstep中的传输的计时,以及所有用户自定的aggregator等。Master在内部运行了一个http server来display这些数据信息的监控。
4.5 Aggregators
    Aggregators代表着整个计算过程中某一个全局信息,它通过一个用户定义aggregation函数在整个计算过程中不听的更新这个信息。每一个计算节点都保存了一个aggregators的引用集,由type name和instance name来标识。当一个计算节点对graph的某一个partition执行一个superstep时,计算节点combine所有的对一个aggregator的值到一个local value:aggregator的值会在当前partition中进行局部的规约。在superstep结束时,所有计算节点会将所有部分规约的aggregator的值进行最后的汇总,并汇报给master。这个过程是由所有计算节点构造出一棵规约树而不是顺序的通过pipe方式来规约,这样做的原因是为了利用cpu的并行性来快速规约。在下一步的superstep中,master就会将aggregator的新值发送给每一个计算节点

你可能感兴趣的:(算法计算方法)