大规模图计算框架之GraphLite

来源:互联网 发布:浮点型数据是什么 编辑:程序博客网 时间:2024/06/16 09:16

3.C++ API

这一节主要介绍Pregel C++ API中最重要的几个方面,暂时忽略相关其他机制。编写一个Pregel程序需要继承Pregel中已预定义好的一个基类——Vertex类(见图3)。
Pregel: A System for Large-Scale Graph Processing(译) - 星星 - 银河里的星星
该类的模版参数中定义了三个值类型参数,分别表示顶点,边和消息。每一个顶点都有一个对应的给定类型的值。这种形式可能看上有很多限制,但用户可以用protocol buffer来管理增加的其他定义和属性。而边和消息类型的行为比较类似。

用户覆写Vertex类的虚函数Compute(),该函数会在每一个超级步中对每一个顶点进行调用。预定义的Vertex类方法允许Compute()方法查询当前顶点及其边的信息,以及发送消息到其他的顶点。Compute()方法可以通过调用GetValue()方法来得到当前顶点的值,或者通过调用MutableValue()方法来修改当前顶点的值。同时还可以通过由出边的迭代器提供的方法来查看修改出边对应的值。这种状态的修改是立时可见的。由于这种可见性仅限于被修改的那个顶点,所以不同顶点并发进行的数据访问是不存在竞争关系的。

顶点和其对应的边所关联的值是唯一需要在超级步之间持久化的顶点级状态。将由计算框架管理的图状态限制在一个单一的顶点值或边值的这种做法,简化了主计算流程,图的分布以及故障恢复。

3.1 消息传递机制

顶点之间的通信是直接通过发送消息,每条消息都包含了消息值和目标顶点的名称。消息值的数据类型是由用户通过Vertex类的模版参数来指定。

在一个超级步中,一个顶点可以发送任意多的消息。当顶点V的Compute()方法在S+1超级步中被调用时,所有在S超级步中发送给顶点V的消息都可以通过一个迭代器来访问到。在该迭代器中并不保证消息的顺序,但是可以保证消息一定会被传送并且不会重复。

一种通用的使用方式为:对一个顶点V,遍历其自身的出边,向每条出边发送消息到该边的目标顶点,如图4中PageRank算法(参见5.1节)所示的那样。但是,dest_vertex并不一定是顶点V的相邻顶点。一个顶点可以从之前收到的消息中获取到其非相邻顶点的标识符,或者顶点标识符可以隐式的得到。比如,图可能是一个clique(一个图中两两相邻的一个点集,或是一个完全子图),顶点的命名规则都是已知的(从V1到Vn),在这种情况下甚至都不需要显式地保存边的信息。

当任意一个消息的目标顶点不存在时,便执行用户自定义的handlers。比如在这种情况下,一个handler可以创建该不存在的顶点或从源顶点中删除这条边。

3.2 Combiners

发送消息,尤其是当目标顶点在另外一台机器时,会产生一些开销。某些情况可以在用户的协助下降低这种开销。比方说,假如Compute() 收到许多的int 值消息,而它仅仅关心的是这些值的和,而不是每一个int的值,这种情况下,系统可以将发往同一个顶点的多个消息合并成一个消息,该消息中仅包含它们的和值,这样就可以减少传输和缓存的开销。

Combiners在默认情况下并没有被开启,这是因为要找到一种对所有顶点的Compute()函数都合适的Combiner是不可能的。而用户如果想要开启Combiner的功能,需要继承Combiner类,覆写其virtual函数Combine()。框架并不会确保哪些消息会被Combine而哪些不会,也不会确保传送给Combine()的值和Combining操作的执行顺序。所以Combiner只应该对那些满足交换律和结合律的操作打开。

对于某些算法来说,比如单源最短路径(参见5.2节),我们观察到通过使用Combiner将流量降低了4倍多。

3.3 Aggregators

Pregel的aggregators是一种提供全局通信,监控和数据查看的机制。在一个超级步S中,每一个顶点都可以向一个aggregator提供一个数据,系统会使用一种reduce操作来负责聚合这些值,而产生的值将会对所有的顶点在超级步S+1中可见。Pregel包含了一些预定义的aggregators,如可以在各种整数和string类型上执行的min,max,sum操作。

Aggregators可以用来做统计。例如,一个sum aggregator可以用来统计每个顶点的出度,最后相加就是整个图的边的条数。更复杂的一些reduce操作还可以产生统计直方图。

Aggregators也可以用来做全局协同。例如, Compute()函数的一些逻辑分支可能在某些超级步中执行,直到and aggregator表明所有顶点都满足了某条件,之后执行另外的逻辑分支直到结束。又比如一个作用在顶点ID之上的min和max aggregator,可以用来选定某顶点在整个计算过程中扮演某种角色等。

要定义一个新的aggregator,用户需要继承预定义的Aggregator类,并定义在第一次接收到输入值后如何初始化,以及如何将接收到的多个值最后reduce成一个值。Aggregator操作也应该满足交换律和结合律。

默认情况下,一个aggregator仅仅会对来自同一个超级步的输入进行聚合,但是有时也可能需要定义一个sticky aggregator,它可以从所有的supersteps中接收数据。这是非常有用的,比如要维护全局的边条数,那么就仅仅在增加和删除边的时候才调整这个值了。

还可以有更高级的用法。比如,可以用来实现一个△-stepping最短路径算法所需要的分布式优先队列[37]。每个顶点会根据它的当前距离分配一个优先级bucket。在每个超级步中,顶点将它们的indices汇报给min aggregator。在下一个超级步中,将最小值广播给所有worker,然后让在最小index的bucket中的顶点放松它们的边。{!说明此处的核心在于说明aggregators用法,关于△-stepping最短路径算法不再解释,感兴趣的可以参考这篇文章:Δ-Stepping: A Parallel Single Source Shortest Path Algorithm }

3.4 Topology Mutations

有一些图算法可能需要改变图的整个拓扑结构。比如一个聚类算法,可能会将每个聚类替换成一个单一顶点,又比如一个最小生成树算法会删除所有除了组成树的边之外的其他边。正如用户可以在自定义的Compute()函数能发送消息,同样可以产生在图中增添和删除边或顶点的请求。

多个顶点有可能会在同一个超级步中产生冲突的请求(比如两个请求都要增加一个顶点V,但初始值不一样)。Pregel中用两种机制来决定如何调用:局部有序和handlers。

由于是通过消息发送的,拓扑改变在请求发出以后,在超级步中可以高效地执行。在该超级步中,删除会首先被执行,先删除边后删除顶点,因为顶点的删除通常也意味着删除其所有的出边。然后执行添加操作,先增加顶点后增加边,并且所有的拓扑改变都会在Compute()函数调用前完成。这种局部有序保证了大多数冲突的结果的确定性。

剩余的冲突就需要通过用户自定义的handlers来解决。如果在一个超级步中有多个请求需要创建一个相同的顶点,在默认情况下系统会随便挑选一个请求,但有特殊需求的用户可以定义一个更好的冲突解决策略,用户可以在Vertex类中通过定义一个适当的handler函数来解决冲突。同一种handler机制将被用于解决由于多个顶点删除请求或多个边增加请求或删除请求而造成的冲突。我们委托handler来解决这种类型的冲突,从而使得Compute()函数变得简单,而这样同时也会限制handler和Compute()的交互,但这在应用中还没有遇到什么问题。

我们的协同机制比较懒,全局的拓扑改变在被apply之前不需要进行协调{!即在变更请求的发出端不会进行任何的控制协调,只有在它被接收到然后apply时才进行控制,这样就简化了流程,同时能让发送更快}。这种设计的选择是为了优化流式处理。直观来讲就是对顶点V的修改引发的冲突由V自己来处理。

Pregel同样也支持纯local的拓扑改变,例如一个顶点添加或删除其自身的出边或删除其自己。Local的拓扑改变不会引发冲突,并且顶点或边的本地增减能够立即生效,很大程度上简化了分布式的编程。

3.5 Input and Output

可以采用多种文件格式进行图的保存,比如可以用text文件,关系数据库,或者Bigtable[9]中的行。为了避免规定死一种特定文件格式,Pregel将从输入中解析出图结构的任务从图的计算过程中进行了分离。类似的,结果可以以任何一种格式输出并根据应用程序选择最适合的存储方式。Pregel library本身提供了很多常用文件格式的readers和writers,但是用户可以通过继承Reader和Writer类来定义他们自己的读写方式。

4.Implementation

Pregel是为Google的集群架构[3]而设计的。每一个集群都包含了上千台机器,这些机器都分列在许多机架上,机架之间有这非常高的内部通信带宽。集群之间是内部互联的,但地理上是分布在不同地方的。

应用程序通常通过一个集群管理系统执行,该管理系统会通过调度作业来优化集群资源的使用率,有时候会杀掉一些任务或将任务迁移到其他机器上去。该系统中提供了一个名字服务系统,所以各任务间可以通过与物理地址无关的逻辑名称来各自标识自己。持久化的数据被存储在GFS[19]或Bigtable[9]中,而临时文件比如缓存的消息则存储在本地磁盘中。

4.1 Basic Architecture

Pregel library将一张图划分成许多的partitions,每一个partition包含了一些顶点和以这些顶点为起点的边。将一个顶点分配到某个partition上去取决于该顶点的ID,这意味着即使在别的机器上,也是可以通过顶点的ID来知道该顶点是属于哪个partition,即使该顶点已经不存在了。默认的partition函数为hash(ID) mod N,N为所有partition总数,但是用户可以替换掉它。

将一个顶点分配给哪个worker机器是整个Pregel中对分布式不透明的主要地方。有些应用程序使用默认的分配策略就可以工作地很好,但是有些应用可以通过定义更好地利用了图本身的locality的分配函数而从中获益。比如,一种典型的可以用于Web graph的启发式方法是,将来自同一个站点的网页数据分配到同一台机器上进行计算。

在不考虑出错的情况下,一个Pregel程序的执行过程分为如下几个步骤:
1. 用户程序的多个copy开始在集群中的机器上执行。其中有一个copy将会作为master,其他的作为worker,master不会被分配图的任何一部分,而只是负责协调worker间的工作。worker利用集群管理系统中提供的名字服务来定位master位置,并发送注册信息给master。
2. Master决定对这个图需要多少个partition,并分配一个或多个partitions到worker所在的机器上。这个数字也可能由用户进行控制。一个worker上有多个partition的情况下,可以提高partitions间的并行度,更好的负载平衡,通常都可以提高性能。每一个worker负责维护在其之上的图的那一部分的状态(顶点及边的增删),对该部分中的顶点执行Compute()函数,并管理发送出去的以及接收到的消息。每一个worker都知道该图的计算在所有worker中的分配情况。
3. Master进程为每个worker分配用户输入中的一部分,这些输入被看做是一系列记录的集合,每一条记录都包含任意数目的顶点和边。对输入的划分和对整个图的划分是正交的,通常都是基于文件边界进行划分。如果一个worker加载的顶点刚好是这个worker所分配到的那一部分,那么相应的数据结构就会被立即更新。否则,该worker就需要将它发送到它所应属于的那个worker上。当所有的输入都被load完成后,所有的顶点将被标记为active状态,
4. Master给每个worker发指令,让其运行一个超级步,worker轮询在其之上的顶点,会为每个partition启动一个线程。调用每个active顶点的Compute()函数,传递给它从上一次超级步发送来的消息。消息是被异步发送的,这是为了使得计算和通信可以并行,以及进行batching,但是消息的发送会在本超级步结束前完成。当一个worker完成了其所有的工作后,会通知master,并告知当前该worker上在下一个超级步中将还有多少active节点。
不断重复该步骤,只要有顶点还处在active状态,或者还有消息在传输。
5. 计算结束后,master会给所有的worker发指令,让它保存它那一部分的计算结果。

4.2 Fault tolerance

容错是通过checkpointing来实现的。在每个超级步的开始阶段,master命令worker让它保存它上面的partitions的状态到持久存储设备,包括顶点值,边值,以及接收到的消息。Master自己也会保存aggregator的值。

worker的失效是通过master发给它的周期性的ping消息来检测的。如果一个worker在特定的时间间隔内没有收到ping消息,该worker进程会终止。如果master在一定时间内没有收到worker的反馈,就会将该worker进程标记为失败。

当一个或多个worker发生故障,被分配到这些worker的partitions的当前状态信息就丢失了。Master重新分配图的partition到当前可用的worker集合上,所有的partition会从最近的某超级步S开始时写出的checkpoint中重新加载状态信息。该超级步可能比在失败的worker上最后运行的超级步 S’早好几个阶段,此时失去的几个superstep将需要被重新执行{!应该是所有的partition都需要重新分配,而不仅仅是失败的worker上的那些,否则如何重新执行丢失的超级步,也正是这样才有了下面的confined recovery}。我们对checkpoint频率的选择基于某个故障模型[13]的平均时间,以平衡checkpoint的开销和恢复执行的开销。

为了改进恢复执行的开销和延迟, Confined recovery已经在开发中。除了基本的checkpoint,worker同时还会将其在加载图的过程中和超级步中发送出去的消息写入日志。这样恢复就会被限制在丢掉的那些 partitions上。它们会首先通过checkpoint进行恢复,然后系统会通过回放来自正常的partitions的记入日志的消息以及恢复过来的partitions重新生成的消息,更新状态到S’阶段。这种方式通过只对丢失的partitions进行重新计算节省了在恢复时消耗的计算资源,同时由于每个worker只需要恢复很少的partitions,减少了恢复时的延迟。对发送出去的消息进行保存会产生一定的开销,但是通常机器上的磁盘带宽不会让这种IO操作成为瓶颈。

Confined recovery要求用户算法是确定性的,以避免原始执行过程中所保存下的消息与恢复时产生的新消息并存情况下带来的不一致。随机化算法可以通过基于超级步和partition产生一个伪随机数生成器来使之确定化。非确定性算法需要关闭Confined recovery而使用老的恢复机制。

4.3 Worker implementation

一个worker机器会在内存中维护分配到其之上的graph partition的状态。概念上讲,可以简单地看做是一个从顶点ID到顶点状态的Map,其中顶点状态包括如下信息:该顶点的当前值,一个以该顶点为起点的出边(包括目标顶点ID,边本身的值)列表,一个保存了接收到的消息的队列,以及一个记录当前是否active的标志位。该worker在每个超级步中,会循环遍历所有顶点,并调用每个顶点的Compute()函数,传给该函数顶点的当前值,一个接收到的消息的迭代器和一个出边的迭代器。这里没有对入边的访问,原因是每一条入边其实都是其源顶点的所有出边的一部分,通常在另外的机器上。

出于性能的考虑,标志顶点是否为active的标志位是和输入消息队列分开保存的。另外,只保存了一份顶点值和边值,但有两份顶点active flag和输入消息队列存在,一份是用于当前超级步,另一个用于下一个超级步。当一个worker在进行超级步S的顶点处理时,同时还会有另外一个线程负责接收从处于同一个超级步的其他worker接收消息。由于顶点当前需要的是S-1超级步的消息,那么对superstep S和superstep S+1的消息就必须分开保存。类似的,顶点V接收到了消息表示V将会在下一个超级步中处于active,而不是当前这一次。

当Compute()请求发送一个消息到其他顶点时,worker首先确认目标顶点是属于远程的worker机器,还是当前worker。如果是在远程的worker机器上,那么消息就会被缓存,当缓存大小达到一个阈值,最大的那些缓存数据将会被异步地flush出去,作为单独的一个网络消息传输到目标worker。如果是在当前worker,那么就可以做相应的优化:消息就会直接被放到目标顶点的输入消息队列中。

如果用户提供了Combiner,那么在消息被加入到输出队列或者到达输入队列时,会执行combiner函数。后一种情况并不会节省网络开销,但是会节省用于消息存储的空间。

4.4 Master implementation

Master主要负责的worker之间的工作协调,每一个worker在其注册到master的时候会被分配一个唯一的ID。Master内部维护着一个当前活动的worker列表,该列表中就包括每个worker的ID和地址信息,以及哪些worker被分配到了整个图的哪一部分。Master中保存这些信息的数据结构大小与partitions的个数相关,与图中的顶点和边的数目无关。因此,虽然只有一台master,也足够用来协调对一个非常大的图的计算工作。

绝大部分的master的工作,包括输入 ,输出,计算,保存以及从 checkpoint中恢复,都将会在一个叫做barriers的地方终止:Master在每一次操作时都会发送相同的指令到所有的活着的worker,然后等待从每个worker的响应。如果任何一个worker失败了,master便进入4.2节中描述的恢复模式。如果barrier同步成功,master便会进入下一个处理阶段,例如master增加超级步的index,并进入下一个超级步的执行。

Master同时还保存着整个计算过程以及整个graph的状态的统计数据,如图的总大小,关于出度分布的柱状图,处于active状态的顶点个数,在当前超级步的时间信息和消息流量,以及所有用户自定义aggregators的值等。为方便用户监控,Master在内部运行了一个HTTP服务器来显示这些信息。

4.5 Aggregators

每个Aggregator(见3.3节)会通过对一组value值集合应用aggregation函数计算出一个全局值。每一个worker都保存了一个aggregators的实例集,由type name和实例名称来标识。当一个worker对graph的某一个partition执行一个超级步时,worker会combine所有的提供给本地的那个aggregator实例的值到一个local value:即利用一个aggregator对当前partition中包含的所有顶点值进行局部规约。在超级步结束时,所有workers会将所有包含局部规约值的aggregators的值进行最后的汇总,并汇报给master。这个过程是由所有worker构造出一棵规约树而不是顺序的通过流水线的方式来规约,这样做的原因是为了并行化规约时cpu的使用。在下一个超级步开始时,master就会将aggregators的全局值发送给每一个worker。

参考文献:http://blog.csdn.net/likika2012/article/details/38755069

1 0