摘要
许多实际的计算问题都与大型图有关。标准示例包括Web图形和各种社交网络。这些图的规模(在某些情况下为数十亿个顶点,数万亿条边)给它们的有效处理带来了挑战。在本文中,我们提出了适合该任务的计算模型。程序被表示为一系列迭代,在每个迭代中,一个顶点可以接收在先前迭代中发送的消息,将消息发送到其他顶点,并修改其自身的状态以及其输出边或突变图拓扑的状态。这种以顶点为中心的方法足够灵活,可以表达各种算法。该模型是为在数千台商用计算机集群上高效,可伸缩和容错实现而设计的,其隐含的同步性使程序推理更加容易。与分发有关的详细信息隐藏在抽象API的后面。结果是一个用于处理大型图形的框架,该框架具有表现力且易于编程。
介绍
互联网使Web图形成为分析和研究的流行对象。 Web 2.0激发了人们对社交网络的兴趣。常用的算法包括最短路径计算,不同的聚类风格以及页面排名主题的变化。还有许多其他具有实际价值的图形计算问题,例如最小切割和连接的组件。对大图形的有效处理具有挑战性。图算法通常表现出内存访问的局部性差,每个顶点的工作量很少以及在执行过程中并行度的变化。在许多计算机上分布会加剧局部性问题,并增加计算机在计算过程中发生故障的可能性。尽管大图无处不在,并且它们具有商业重要性,但我们知道在大规模分布式环境中,没有可扩展的通用系统可以在任意图表示上实现任意图算法。
实现处理大型图的算法通常意味着在以下选项中进行选择:
1.设计一个定制的分布式基础架构,通常需要为每个新算法或图形表示重复大量的实现工作;
2。依靠现有的分布式计算平台,通常不适用于图形处理。例如,MapReduce 非常适合各种各样的大规模计算问题。有时会使用它来消除大图,但这可能会导致性能欠佳和可用性问题。处理数据的基本模型已扩展为便于聚集[41]和类似SQL的查询[40、47],但是这些扩展通常对于通常更适合消息传递模型的图算法不是理想的。
3.使用单机图形算法库,例如BGL [43],LEDA [35],NetworkX [25],JDSL [20],Stanford GraphBase [29]或FGL [16],限制了可以解决的问题的规模。
4.使用现有的平行图系统。 ParallelBGL [22]和CGMgraph [8]库解决了并行图算法,但是没有解决容错或其他对于超大规模分布式系统很重要的问题。
这些选择都不符合我们的目的。为了解决大规模图形的分布式处理,我们构建了一个具有API的容错平台,该API具有足够的灵活性来表示任意图形算法。本文介绍了所得的系统,称为Pregel1,并报告了我们的经验。
Pregel计算包括一系列迭代,称为超步。在超级步骤中,框架在概念上并行地为每个顶点调用用户定义的函数。该函数指定单个顶点V和单个超级步S的行为。它可以读取在超级步S-1发送给V的消息,将消息发送到将在超级步S+1接收的其他顶点,并修改V的状态。及其向外的边缘。消息通常沿着出站边缘发送,但是消息可以发送到其标识符已知的任何顶点。
以顶点为中心的方法让人想起MapReduce,因为用户专注于局部动作,独立处理每个项目,并且系统将这些动作组合起来,以将计算提升到大型数据集。通过设计,该模型非常适合于分布式实现:它没有公开任何用于检测超级步骤中执行顺序的机制,并且所有通信都是从超级步骤S到超级步骤S + 1。
该模型的同步性使得在实现算法时更容易推理程序语义,并确保Pregel程序固有地没有死锁和异步系统中常见的数据争用。原则上,在有足够的并行松弛的情况下,Pregel程序的性能应与异步系统相竞争
模型计算
Pregel计算的输入是有向图,其中每个顶点由字符串顶点标识符唯一标识。每个顶点都与一个可修改的用户定义值相关联。有向边与它们的源顶点相关联,每个边由可修改的用户定义值和目标顶点标识符组成。典型的Pregel计算由图形初始化时的输入和随后的一系列超级步组成。通过全局同步点,直到算法终止,然后完成输出。
在每个超级步骤中,顶点都是并行计算的,每个顶点执行相同的用户定义功能,以表达给定算法的逻辑。顶点可以修改其状态或输出边缘的状态,接收在上一个超级步骤中发送给它的消息,将消息发送到其他顶点(在下一个超级步骤中接收),或转变图的拓扑结构。
算法终止基于每个停止投票的顶点。在超级步骤0中,每个顶点都处于活动状态;所有活动顶点都参与任何给定超步的计算。顶点通过投票暂停来使其自身失效。这意味着除非在外部触发该顶点,否则无需做进一步的工作,并且Pregel框架将不会在后续的超级步骤中执行该顶点,除非它收到消息。如果通过消息重新激活,则顶点必须再次明确地将其自身禁用。当所有顶点同时处于非活动状态并且没有任何消息在传输时,该算法将整体终止。这个简单的状态机如图1所示。
Pregel程序的输出是顶点明确输出的一组值。它通常是与输入同构的有向图,但这不是系统的必需属性,因为可以在计算过程中添加和删除顶点和边。例如,聚类算法可能会生成从大图中选择的一小组不连续的顶点。图挖掘算法可以简单地输出从图提取的聚合统计信息。
图2用一个简单的例子说明了这些概念:给定一个强连接图,其中每个顶点都包含一个值,它将最大值传播到每个顶点。在每个超级步骤中,任何从其消息中了解到更大价值的顶点都会将其发送给所有邻居。如果没有更多的顶点在超步中变化,则算法终止。
我们选择纯消息传递模型,省略远程读取和模拟共享内存的其他方式,这有两个原因。首先,消息传递具有足够的表现力,因此无需进行远程读取。我们没有找到任何消息传递不足的图算法。其次,此选择对性能更好。在集群环境中,从远程计算机读取值会导致高延迟,而这很容易被隐藏。我们的消息传递模型允许我们通过分批异步传递消息来分摊延迟。
图算法可以编写为一系列链接的MapReduce调用。由于可用性和性能的原因,我们选择了不同的模型。 Pregel将顶点和边缘保留在执行计算的机器上,并且仅将网络传输用于消息。但是,MapReduce本质上是功能性的,因此将图形算法表示为链接的MapReduce时,需要将图形的整个状态从一个阶段传递到下一个阶段-通常需要更多的通信和相关的序列化开销。此外,需要协调链接的MapReduce的步骤,这增加了编程的复杂性,而Pregel在超级步骤上的迭代避免了这种复杂性。
C++API
编写Pregel程序涉及子类化预定义的Vertex类。它的模板参数定义了三种值类型,分别与顶点,边和消息相关。每个顶点都有一个指定类型的关联值。这种统一性似乎是限制性的,但用户可以通过使用诸如协议缓冲区之类的灵活类型进行管理[42]。边缘和消息类型的行为类似。用户将覆盖虚拟Compute()方法,该方法将在每个超级步骤的每个活动顶点处执行。预定义的顶点方法使Compute()可以查询有关当前顶点及其边缘的信息,并将消息发送到其他顶点。 Compute()可以通过GetValue()检查与其顶点关联的值,也可以通过MutableValue()对其进行修改。它可以使用边缘迭代器提供的方法检查和修改边缘的值。这些状态更新立即可见。由于它们的可见性仅限于修改后的顶点,因此在来自不同顶点的并发值访问中没有数据争用。与顶点及其边缘相关联的值是唯一在每个超步中都存在的每个顶点状态。将框架管理的图形状态限制为每个顶点或边沿为单个值,可简化主要计算周期,图形分布和故障恢复。
信息传递
各个顶点之间通过发送消息直接进行通信,每个消息都包含一个消息值和目标顶点的名称。消息值的类型由用户指定为Vertex类的模板参数。一个顶点可以在超步中发送任意数量的消息。在超级步骤S + 1中调用V的Compute()方法时,可以通过迭代器使用在超级步骤S中发送给顶点V的所有消息。没有保证消息在迭代器中的顺序,但是可以保证消息将交付,并且不会重复。
常见的使用模式是顶点V在其出站边缘上进行迭代,向每个边缘的目标顶点发送一条消息,如图4中的PageRank算法所示。
顶点可以从较早接收到的消息中学习非邻居的标识符,或者可以隐式知道顶点标识符。例如,该图可能是一个具有众所周知的顶点标识符V1到Vn的集团,在这种情况下,甚至不需要在图中保留明确的边。当任何消息的目标顶点不存在时,我们执行用户定义的处理程序。例如,处理程序可以创建丢失的顶点或从其源顶点移除悬空的边缘。
组合器
发送消息(尤其是发送到另一台计算机上的顶点)会产生一些开销。在某些情况下,可以在用户的帮助下减少这种情况。例如,假设Compute()接收整数消息,并且与各个值相反,只有和才重要。在这种情况下,系统可以将多个针对顶点V的消息合并为一个包含其总和的消息,从而减少了必须传输和缓冲的消息的数量。默认情况下,不启用合并器,因为没有机械方法可以找到与用户的Compute()方法的语义一致的有用的合并函数。为了实现此优化,用户可以重写Combiner类的子类,从而覆盖虚拟的Combine()方法。无法保证合并哪些消息(如果有的话),提供给组合器的分组或组合的顺序,因此应仅对交换器和关联操作启用组合器。对于某些算法,例如单源最短路径(第5.2节),我们已经观察到使用组合器可以将消息流量减少四倍以上。
聚合器
Pregel聚合器是一种用于全球通信,监视和数据的机制。每个顶点都可以为超级步骤S中的聚合器提供一个值,系统使用归约运算符组合这些值,并且结果值可用于超级步骤S + 1中的所有顶点。Pregel包含许多预定义的聚合器,例如min ,最大或求和运算符对各种整数或字符串类型进行运算。
聚合器也可以用于全局协调。例如,对于超级步骤,可以执行Compute()的一个分支,直到and聚合器确定所有顶点都满足某个条件为止,然后可以执行另一个分支,直到终止为止。顶点ID上的最小或最大聚合器可用于选择在算法中发挥显著作用的顶点。
为了定义一个新的聚合器,用户可以对预定义的聚合器类进行子类化,并指定如何从第一个输入值初始化聚合值,以及如何将多个部分聚合的值减少为一个。聚合运算符应该是可交换的和关联的。
默认情况下,聚合器仅减少单个超级步骤的输入值,但是也可以定义使用所有超级步骤的输入值的粘性聚合器。例如,这对于维护仅在添加或删除边缘时才调整的全局边缘计数有用。
更高级的使用是可能的。例如,可以使用一个聚集器为Δ步进最短路径算法实现一个分布式优先级队列[37]。每个顶点根据其暂定距离分配给优先级桶。在一个超级步骤中,顶点将其索引贡献给最小聚合器。最小值将在下一个超级步骤中广播给所有工作者,最低索引的存储桶中的顶点将松弛边缘。
拓扑变异
一些图形算法需要更改图形的拓扑。例如,聚类算法可能会用单个顶点替换每个聚类,而最小生成树算法可能会删除除树边缘以外的所有边缘。就像用户的Compute()函数可以发送消息一样,它也可以发出添加或删除顶点或边的请求。多个顶点可能在同一超步中发出冲突的请求(例如,两个添加顶点V的请求,不同初始值)。我们使用两种机制来实现确定性:部分排序和处理程序。
与消息一样,在发出请求后,突变在此步骤中变得有效。在该超级步骤中,首先执行删除操作,然后在顶点删除之前先进行边缘删除,因为删除顶点会隐式删除其所有边缘。加法在删除之后,顶点加法在边加法之前,并且所有突变都在对Compute()的调用之前。对于大多数冲突,这种部分排序产生了确定的结果。
其余的冲突由用户定义的处理程序解决。如果存在多个在同一超级步骤中创建同一顶点的请求,则默认情况下,系统仅选择一个,但是有特殊需求的用户可以通过在其Vertex子类中定义适当的处理程序方法来指定更好的冲突解决策略。相同的处理程序机制用于解决由多个顶点删除请求或多个边添加或删除请求引起的冲突。我们将解析委托给处理程序,以使Compute()的代码保持简单,这限制了处理程序与Compute()之间的交互,但实际上并不是一个问题。
全局突变直到应用它们时才需要协调。此设计选择有助于流处理。Pregel还支持纯粹的局部突变,即顶点添加或删除其自身的输出边或删除其自身。局部突变不会引入冲突,并且通过使用更容易的顺序编程语义使冲突立即有效,从而简化了分布式编程。
输入和输出
图形有许多可能的文件格式,例如文本文件,关系数据库中的一组顶点或Bigtable [9]中的行。为了避免强加文件格式的特定选择,Pregel将输入文件解释为图形的任务与图形计算的任务分离了。类似地,可以以任意格式生成输出,并以最适合给定应用程序的形式存储输出。Pregel库为读取器和写入器提供了许多常见的文件格式,但是有特殊需求的用户可以通过将抽象基类Reader和Writer子类化来编写自己的文件。
实施
Pregel是为Google集群体系结构设计的,每个群集由成千上万个商用PC组成,这些PC被组织成具有高机架内带宽的机架。群集是相互连接的,但分布在地理上。我们的应用程序通常在群集管理系统上执行,该系统可调度作业以优化资源分配,有时会杀死实例或将实例移至其他计算机。该系统包括一个名称服务,因此实例可以由逻辑名称来引用,而与它们当前与物理机的绑定无关。持久数据作为文件存储在分布式存储系统GFS 或Bigtable 中,而临时数据(例如缓冲消息)存储在本地磁盘上。
基本架构
Pregel库将图形划分为多个分区,每个分区由一组顶点和所有这些顶点的外边缘组成。将一个顶点分配给一个分区仅取决于顶点ID,这意味着即使该顶点属于另一台机器,或者即使该顶点尚不存在,也可以知道给定顶点属于哪个分区。默认的分区功能只是hash(ID)mod N,其中N是分区数,但是用户可以替换它。在Pregel中,分配顶点到工作机是不透明分布的。一些应用程序可以使用默认分配很好地工作,但是某些应用程序可以从定义自定义分配功能中受益,以更好地利用图形中固有的局部性。例如,Web图采用的一种典型启发式方法是将表示同一站点页面的顶点并置。
在没有错误的情况下,Pregel程序的执行包括以下几个阶段:
- 用户程序的许多副本开始在计算机群集上执行。这些副本之一充当主版。它没有分配给图表的任何部分,但负责协调工人的活动。工作人员使用群集管理系统的名称服务来发现主服务器的位置,然后向主服务器发送注册消息。
- 主机确定图形将具有多少个分区,并为每台工作机分配一个或多个分区。该数目可以由用户控制。每个工作人员拥有一个以上的分区允许分区之间的并行性和更好的负载平衡,并且通常可以提高性能。每个工作人员负责维护其图形部分的状态,在其顶点上执行用户的Compute()方法,并管理往返其他工作人员的消息。每个工人都被分配了所有工人的完整作业。
- 主机将用户输入的一部分分配给每个工作人员。输入被视为一组记录,每个记录包含任意数量的顶点和边。输入的划分与图本身的划分正交,并且通常基于文件边界。如果某个工作人员加载的顶点属于该图形的该工作人员部分,则将立即更新适当的数据结构(第4.3节)。否则,工作程序会将消息排队到拥有顶点的远程对等方。输入完成加载后,所有顶点都标记为活动顶点。
- 主机指示每个工人执行一个超级步骤。工作者在其活动顶点之间循环,为每个分区使用一个线程。工作程序为每个活动顶点调用Compute(),传递在上一个超级步骤中发送的消息。消息是异步发送的,以实现计算,通信和批处理的重叠,但是消息是在超级步骤结束之前发送的。工作者完成后,它将响应主机,告诉主机在下一个超级步骤中将激活多少个顶点。
只要任何顶点处于活动状态或任何消息在传输中,就重复此步骤。 - 在计算停止后,主控器可以指示每个工作人员保存其图形部分。
容错
容错通过检查点实现。在超级步骤开始时,主服务器指示工作人员将其分区状态保存到持久性存储中,其中包括顶点值,边值和传入消息。主服务器单独保存聚合器值。使用主服务器向工作人员发出的定期“ ping”消息检测到工作人员失败。如果工作线程在指定的时间间隔后未收到ping消息,则工作进程终止。如果主服务器未收到工作人员的回音,则主服务器将该工作人员进程标记为失败。当一个或多个工作程序出现故障时,分配给这些工作程序的分区的当前状态将丢失。主节点将图分区重新分配给当前可用的工作组,并且它们都在超级步骤S的开头从最新的可用检查点重新加载其分区状态。该检查点可能比最新的超级步骤S0早几个超级步骤。由故障之前的任何分区完成,要求恢复重复丢失的步骤。我们根据平均故障时间模型[13]选择检查点频率,在检查点成本与预期恢复成本之间取得平衡。
限制恢复正在开发中,以提高恢复的成本和延迟时间。除了基本的检查点外,工作人员还可以在图形加载和超级步骤期间记录来自分配分区的传出消息。然后将恢复限制在丢失的分区中,这些分区将从检查点恢复。系统使用健康分区中的已记录消息和恢复分区中的重新计算消息来重新计算丢失的超级步长,直至S0。这种方法仅通过重新计算丢失的分区即可节省恢复期间的计算资源,并且由于每个工作人员可能会节省恢复时间恢复更少的分区。保存传出消息会增加开销,但是典型的计算机具有足够的磁盘带宽以确保I / O不会成为瓶颈。
worker实现
工作机在内存中维护其图形部分的状态。从概念上讲,这可以看作是从顶点ID到每个顶点状态的映射,其中每个顶点的状态包括其当前值,其出站边的列表(边的目标的点ID和边的当前值),包含传入消息的队列以及指定顶点是否处于活动状态的标志。当工作程序执行超级步骤时,它将遍历所有顶点并调用Compute(),将当前值,迭代器传递给传入的消息,并将迭代器传递给传出的边缘,并将其传递给它。无法访问传入边缘,因为每个传入边缘都是源顶点拥有的列表的一部分,通常在另一台计算机上。
出于性能方面的考虑,活动顶点标志与传入消息队列分开存储。此外,虽然仅存在一个顶点和边值的副本,但存在两个活动顶点标志和传入消息队列的副本:一个副本用于当前超步,一个副本用于下一个超步。当工作进程在超级步骤S中处理其顶点时,它同时在另一个线程中从执行同一超级进程的其他工作进程接收消息。由于顶点接收在上一个超级步骤中发送的消息(请参见第2节),因此超级步骤S和S + 1的消息必须保持分开。类似地,到达顶点V的消息意味着V将在下一个超级步骤中处于活动状态,而不必在当前超级步骤中处于活动状态。
当Compute()请求将消息发送到另一个顶点时,辅助进程首先确定目标顶点是由远程工作器计算机拥有还是由拥有发送方的同一工作者拥有。在远程情况下,该消息被缓冲以传递给目标工作者。当缓冲区大小达到阈值时,最大的缓冲区将被异步刷新,并将每个缓冲区作为单个网络消息传递给其目的地工作者。在本地情况下,可以进行优化:将消息直接放置在目标顶点的传入消息队列中。如果用户提供了合并器(第3.2节),则在将消息添加到传出消息队列时以及它们在传入消息队列中被接收。后者不会减少网络使用率,但是会减少存储消息所需的空间。
master实现
master主要负责协调工人的活动。每个工人在注册时都会分配一个唯一的标识符。船长会维护一个清单,列出所有当前已知还活着的工人,包括该工人的唯一标识符,其地址信息以及已分配给图形的哪一部分。master数据结构的大小与分区的数量成正比,而不是与顶点或边的数量成正比,因此单个master甚至可以协调非常大的图形的计算。master还会维护有关计算进度和图形状态的统计信息,例如图形的总大小。
聚合器
聚合器(第3.3节)通过将聚合函数应用于用户提供的一组值来计算单个全局值。每个工作程序维护一个聚合器实例的集合,这些集合由类型名称和实例名称标识。当工作线程对图形的任何分区执行超级步骤时,工作线程会将提供给聚合器实例的所有值组合到一个局部值中:一个聚合器,该聚合器会在分区中所有工作器顶点上部分减少。在超级步骤的最后,工作人员形成一棵树,将部分缩减的聚合器减少为全局值,并将其交付给主节点。我们使用基于树的还原(而不是使用工人链进行流水线化)来在还原期间并行使用CPU。在下一个超级步骤开始时,主服务器将全局值发送给所有工作人员。
应用
本节介绍了四个示例,这些示例是Pregel用户开发的用于解决实际问题的简化算法:pagerank,最短路径,二分匹配和半聚类算法。
实验
我们在300个多核商用PC的群集上使用5.2节的单源最短路径(SSSP)实现进行了各种实验。我们使用各种图的大小(所有边的权重都隐式设置为1)来报告二叉树的运行时间(以研究缩放特性)和对数正态随机图(以研究更现实的环境中的性能)。群集,在内存中生成测试图以及验证结果不包括在测量中。由于所有实验都可以在相对较短的时间内运行,因此失败几率很低,并且禁用了检查点。作为Pregel如何根据工作人员任务进行扩展的指示,图7显示了具有十亿个顶点的二叉树的最短路径运行时间(并且,因此,十亿减去一个边缘)时,Pregel工人的数量从50变为800。使用16倍的工人从174秒减少到17.3秒,意味着加速了大约10倍。
在这里,从17.3秒增加到702秒表明,对于平均得分较低的图,运行时间在图大小上呈线性增长。尽管先前的实验表明Pregel如何在工人和图大小上进行缩放,但是二叉树显然不能代表在实践中遇到的图表。因此,我们还对随机图进行了实验,这些随机图使用度的对数正态分布,
图9显示了此类图形的最短路径运行时,其大小从1000万到十亿个顶点不等(因此超过了1270亿个边),并且在300台多核计算机上计划了800个工作任务。为最大的图形运行最短的路径花费了超过10分钟的时间。
总结和未来工作
本文的贡献是一个适用于大规模图形计算的模型,并描述了其生产质量,可伸缩性,容错实现。基于用户的输入,我们认为我们成功地制作了该模型有用和可用。已经部署了数十个Pregel应用程序,并且正在设计,实施和调整更多应用程序。用户报告说,一旦切换到“像顶点一样思考”的编程模式,该API便变得直观,灵活且易于使用。这并不奇怪,因为我们与从一开始就影响API的早期采用者一起工作。例如,添加了聚合器以消除用户在早期Pregel模型中发现的限制。受用户体验的影响,Pregel的其他可用性方面包括一组状态页面,其中包含有关Pregel程序进度的详细信息,一个单元测试框架以及一个有助于快速原型设计和调试的单机模式。性能,可伸缩性和故障对于具有数十亿个顶点的图形,Pregel的公差已经令人满意。我们正在研究将技术缩放到更大的图形的技术,例如放宽模型的同步性,以避免快速工人不得不在超步间障碍中频繁等待的成本。
向机器分配顶点以最小化机器间的通信是一个挑战。如果拓扑响应消息流量,则基于拓扑对输入图进行分区可能就足够了,但可能不足够。我们想设计动态重新划分机制。Pregel是为稀疏图设计的,在稀疏图中,通信主要发生在边缘上,并且我们不希望焦点发生变化。尽管已经注意支持高扇出和扇入流量,但是当大多数顶点连续向大多数其他顶点发送消息时,性能会受到影响。但是,现实的稠密图很少,稀疏图上的稠密通信算法也很少。可以通过使用组合器,聚集器或拓扑突变等方法,将某些此类算法转换为更具Pregel友好性的变量,当然,这种计算对于任何高度分散的系统都是困难的。面向我们的用户群的生产基础架构。如果没有考虑兼容性,我们将不再具有更改API的自由。但是,我们认为我们设计的编程接口足够抽象和灵活,可以适应基础系统的进一步发展。