最近在看大数据图计算相关的论文,故将图计算的原理简单梳理一遍,参考资料为<<大数据技术原理与应用>>
一、图计算简介
1.1. 图结构:
图形数据结构与一般数据结构不同,它必须要反映数据所对应元素之间的几何关系和拓扑关系。图形数据结构通常是指由若干个图形数据元素按一定关系所组成的有序集,一般称为表。关系的链接实现是指图形数据中的每一个数据项的存放是无规则的,其间的连接是通过数据元素中指示连接单元的指针来实现的。许多大数据都是以大规模图或网络的形式呈现,非图结构的大数据,也常常会被转换为图模型后进行分析。
1.2. 图计算:
“图计算”是以“图论”为基础的对现实世界的一种“图”结构的抽象表达,以及在这种数据结构上的计算模式。在图计算中,基本的数据结构通常包含三部分,即顶点、边及边上的权重。其中顶点表达的是客观世界中的实体,边是实体之间的相互联系,权重往往是量化关系轻重的数值表达。
二、BSP模型解析
针对大型图的计算,主要是基于BSP模型实现的并行图处理系统。
BSP: 整体同步并行计算模型(Bulk Synchronous Parallel Computing Model ),又称“大同步”模型,是由美国哈佛大学的 L.G.Valiant教授在 1992 年作为一种并行计算模型提出的。BSP模型迭代执行每一个超步直到满足终止条件或者达到一定的超步数强制终止。每一个超步由本地计算,全局通信和路障同步三个阶段组成:
-
本地计算;计算发生在每一个参与计算的节点上,每个节点只使用存储在本地存储器上的数据,该些数据是在程序启动时加载数据阶段或上一个超级步的全局通信时放在本地存储器上的数据。每台处理器上的计算是独立的,从这个意义上看节点间的执行是异步的。
-
全局通信:某个处理器向其他处理器发出通信请求,每个处理器发送或等待消息。消息在节点之间进行交换。这种交换是单方面的以点对点的方式进行的。处理器通常需要输入队列及输出队列存储信息。
-
路障同步:当一个任务执行完本地计算到达同步点时,它会等待其他所有的任务完成计算。本超步中所有的数据通信在本次同步后生效,并提供给下一个超步使用。当所有的通信结束后,确保每个处理器均执行完当前超步中所有的计算和通信,并且通信过程中各处理器间的数据交换均已完成,然后进入到下一轮迭代。
Pregel:谷歌公司推出的一种基于BSP模型实现的并行图处理系统。为了解决大型图的分布式计算问题,Pregel搭建了一套可扩展的、有容错机制的平台,该平台提供了一套非常灵活的接口,可以描述各种各样的图计算。主要用于求解最短路径、网页排序等问题。
3.1. 有向图 :
Pregel计算模型以有向图作为输入,有向图的每个顶点都有一个顶点ID,每个顶点都有一个自定义值,每条有向边都和其源顶点关联,并记录了其目标顶点ID,每条边上有一个数值作为权重。
在每个超步S中,图中的所有顶点都会并行执行相同的compute函数.
3.2.消息通信:
每个顶点可以接收前一个超步(S-1)中发送给它的消息,修改其自身及其出射边的状态,并发送消息给其他顶点,甚至是修改整个图的拓扑结构。在这种计算模式中,边上面不会运行相应的计算,只有顶点才会执行compute函数进行相应计算。
3.3.计算过程:
Pregel的计算模型为BSP模型,是由一系列被称为“超步”的迭代组成的:
-
在每个超步中,每个顶点上面都会并行执行compute函数,该函数描述了一个顶点V在一个超步S中需要执行的操作。
-
Compute函数会读取前一个超步(S-1)中其他顶点发送给顶点V的消息,执行相应计算后,修改顶点V及其出射边的状态,然后沿着顶点V的出射边发送消息给其他顶点,而且,一个消息可能经过多条边的传递后被发送到任意已知ID的目标顶点上去。
-
这些消息将会在下一个超步(S+1)中被目标顶点接收,然后像上述过程一样开始下一个超步(S+1)的迭代过程。
在Pregel计算过程中,一个算法什么时候可以结束,是由所有顶点的状态决定的:
-
在第0个超步,所有顶点处于活跃状态,都会参与该超步的计算过程。
-
当一个顶点不需要继续执行进一步的计算时,就会把自己的状态设置为“停机”状态。
-
一旦一个顶点进入非活跃状态,后续超步中就不会再在该顶点上执行计算,除非其他顶点给该顶点发送消息把它再次激活。
-
当一个处于非活跃状态的顶点收到来自其他顶点的消息时,Pregel计算框架必须根据条件判断来决定是否将其显式唤醒进入活跃状态。
-
当图中所有的顶点都已经标识其自身达到“非活跃(inactive)”状态,并且没有消息在传送的时候,算法就可以停止运行。
四、Pregel代码探讨
4.1. C++ API:
Pregel已经采用C++预先定义好了一个Vertex基类,在Vertex类中,定义了三个值类型参数,分别表示顶点、边和消息。编写Pregel程序时,需要继承Vertex类,并且覆写Vertex类的虚函数Compute。
template <typename VertexValue, typename EdgeValue, typename MessageValue>
class Vertex {
public:
//Compute为虚函数,需要用户在其子类中实现
virtual void Compute(MessageIterator* msgs) = 0;
//表示顶点的id值
const string& vertex_id() const;
//表示迭代次数,即超步数
int64 superstep() const;
//获取顶点的数值,面向对象的封装特性
const VertexValue& GetValue();
//改变顶点的数值
VertexValue* MutableValue();
//获取顶点出边的迭代器,用于后续消息传递
OutEdgeIterator GetOutEdgeIterator();
//发送消息函数
void SendMessageTo(const string& dest_vertex,const MessageValue& message);
//顶点进入“停机”状体
void VoteToHalt();
};
Pregel执行计算过程时,每个超步中都会并行调用每个顶点上定义的Compute()函数,允许Compute()方法查询当前顶点及其边的信息,以及发送消息到其他的顶点:
-
Compute()方法可以调用GetValue()方法来获取当前顶点的值。
-
调用MutableValue()方法来修改当前顶点的值。
-
通过由出射边的迭代器OutEdgeIterator提供的方法来查看、修改出射边对应的值。
-
被修改顶点的状态是能够立即被看见的,但对于其他顶点而言是不可见的。
-
不同顶点并发进行的数据访问是不存在竞争关系的。
整个过程中,唯一需要在超步之间持久化的顶点级状态,是顶点和其对应的边所关联的值,因而,Pregel计算框架所需要管理的图状态就只包括顶点和边所关联的值,这种做法大大简化了计算流程,同时,也有利于图的分布和故障恢复。
4.2. 消息传递机制:
顶点之间的通讯是借助于消息传递机制来实现的,每条消息都包含了消息值和需要到达的目标顶点ID:
-
在一个超步S中,一个顶点可以发送任意数量的消息,这些消息将在下一个超步(S+1)中被其他顶点接收。
-
在超步(S+1)中,当Pregel计算框架在顶点V上执行用户自定义的Compute()方法时,所有在前一个超步S中发送给顶点V的消息,都可以通过一个迭代器来访问到。
-
迭代器不能保证消息的顺序,不过可以保证消息一定会被传送并且不会被重复传送。
-
一个顶点V通过与之关联的出射边向外发送消息,消息要到达的目标顶点并不一定是与顶点V相邻的顶点。
-
一个消息可以连续经过多条连通的边到达某个与顶点V不相邻的顶点U,U可以从接收的消息中获取到与其不相邻的顶点V的ID。
4.3. Combiner:
在大数据分布式并行处理框架MapReduce中,每一个map都会产生大量的本地输出,Combiner的作用就是对map输出的结果先做一次合并,以减少的map和reduce节点中的数据传输量,Combiner的存在就是提高当前网络IO传输的性能,也是MapReduce的一种优化手段。Pregel计算框架中Combiner的作用与之类似:
-
Pregel计算框架在消息发出去之前,Combiner可以将发往同一个顶点的多个整型值进行求和得到一个值,只需向外发送这个“求和结果”,从而实现了由多个消息合并成一个消息,大大减少了传输和缓存的开销。
-
在默认情况下,Pregel计算框架并不会开启Combiner功能,因为,通常很难找到一种对所有顶点的Compute()函数都合适的Combiner。
-
当用户打算开启Combiner功能时,可以继承Combiner类并覆写虚函数Combine()。
-
Pregel计算框架无法保证哪些消息会被合并,也无法保证消息传递给 Combine()的顺序和合并操作执行的顺序,因此通常只对那些满足交换律和结合律的操作才可以去开启Combiner功能。
4.4. Aggregator:
Aggregator提供了一种全局通信、监控和数据查看的机制
在超步S中,每个顶点都可以向Aggregator提供一个数据,Pregel计算框架会对这些值进行聚合操作产生一个值,在下个超步(S+1)中,图中的所有顶点都可以看见这个值:
-
Aggregator的聚合功能可以执行最大值、最小值、求和操作,比如,可以定义一个“Sum” Aggregator来统计每个顶点的出射边数量,最后相加可以得到整个图的边的数量。
-
Aggregator还可以实现全局协同的功能,比如,可以设计“and” Aggregator来决定在某个超步中Compute()函数是否执行某些逻辑分支,只有当“and” Aggregator显示所有顶点都满足了某条件时,才去执行这些逻辑分支。
4.5. 拓扑改变:
Pregel计算框架允许用户在自定义函数Compute()中定义操作,修改图的拓扑结构,比如在图中增加(或删除)边或顶点:
-
对于全局拓扑改变,Pregel采用了惰性协调机制,在改变请求发出时,Pregel不会对这些操作进行协调,只有当这些改变请求的消息到达目标顶点并被执行时,Pregel才会对这些操作进行协调,这样,所有针对某个顶点V的拓扑修改操作所引发的冲突,都会由V自己来处理。
-
对于本地的局部拓扑改变,不会引发冲突,顶点或边的本地增减能够立即生效,很大程度上简化了分布式编程。
4.6. 输入和输出:
在Pregel计算框架中,图的保存格式多种多样,包括文本文件、关系数据库或键值数据库等,在Pregel中,“从输入文件生成得到图结构”和“执行图计算”这两个过程是分离的,从而不会限制输入文件的格式。同时,Pregel也可以通过多种方式输出结果。
五、图计算模型体系结构
Pregel计算框架采用分布式主从架构,主节点Master管理者多个Worker从节点,图结构数据拆成多个分区并发送到各个Worker节点上进行运算。Master只负责协调多个Worker执行任务,系统不会把图的任何分区分配给它。Worker借助于名称服务系统可以定位到Master的位置,并向Master发送自己的注册信息。
5.1. Pregel的执行过程:
Pregel计算框架中,一个大型图数据会被划分成多个分区,每个分区都包含了一部分顶点以及以其为起点的边:
-
一个顶点应该被分配到哪个分区上,是由一个函数决定的,系统默认函数为hash(ID) mod N,其中,N为所有分区总数,ID是这个顶点的标识符。
-
无论在哪台机器上,都可以简单根据顶点ID判断出该顶点属于哪个分区,即使该顶点可能已经不存在了。
在理想的情况下,一个Pregel用户程序的执行过程如下:
-
选择集群中的多台机器执行图计算任务,每台机器上运行用户程序的一个副本,其中,有一台机器会被选为Master,其他机器作为Worker。
-
Master把一个图分成多个分区,并把分区分配到多个Worker。一个Worker会领到一个或多个分区,每个Worker知道所有其他Worker所分配到的分区情况。每个Worker负责维护分配给自己的那些分区的状态(顶点及边的增删),对分配给自己的分区中的顶点执行Compute()函数,向外发送消息,并管理接收到的消息。
-
Master会把用户输入划分成多个部分,通常是基于文件边界进行划分。划分后,每个部分都是一系列记录的集合,每条记录都包含一定数量的顶点和边。
-
Master会为每个Worker分配用户输入的一部分。如果一个Worker从输入内容中加载到的顶点,刚好是自己所分配到的分区中的顶点,就会立即更新相应的数据结构。否则,该Worker会根据加载到的顶点的ID,把它发送到其所属的分区所在的Worker上。
-
当所有的输入都被加载后,图中的所有顶点都会被标记为“活跃”状态。
-
Master向每个Worker发送指令,Worker收到指令后,开始运行一个超步。Worker会为自己管辖的每个分区分配一个线程,对于分区中的每个顶点,Worker会把来自上一个超步的、发给该顶点的消息传递给它,并调用处于“活跃”状态的顶点上的Compute()函数,在执行计算过程中,顶点可以对外发送消息,但是,所有消息的发送工作必须在本超步结束之前完成。
-
当所有这些工作都完成以后,Worker会通知Master,并把自己在下一个超步还处于“活跃”状态的顶点的数量报告给Master。上述步骤会被不断重复,直到所有顶点都不再活跃并且系统中不会有任何消息在传输,这时,执行过程才会结束。
-
计算过程结束后,Master会给所有的Worker发送指令,通知每个Worker对自己的计算结果进行持久化存储。
5.2. Master节点:
Master主要负责协调各个Worker执行任务,每个Worker会借助于名称服务系统定位到Master的位置,并向Master发送自己的注册信息,Master会为每个Worker分配一个唯一的ID:
-
Master维护着关于当前处于“有效”状态的所有Worker的各种信息,包括每个Worker的ID和地址信息,以及每个Worker被分配到的分区信息。
-
虽然在集群中只有一个Master,但是,它仍然能够承担起一个大规模图计算的协调任务,这是因为Master中保存这些信息的数据结构的大小,只与分区的数量有关,而与顶点和边的数量无关。
一个大规模图计算任务会被Master分解到多个Worker去执行,在每个超步开始时,Master都会向所有处于“有效”状态的Worker发送相同的指令,然后等待这些Worker的回应:
-
如果在指定时间内收不到某个Worker的反馈,Master就认为这个Worker失效。
-
如果参与任务执行的多个Worker中的任意一个发生了故障失效,Master就会进入恢复模式。
-
在每个超步中,图计算的各种工作,比如输入、输出、计算、保存和从检查点中恢复,都会在路障之前结束。
-
如果路障同步成功,说明一个超步顺利结束,Master就会进入下一个处理阶段,图计算进入下一个超步的执行。
Master在内部运行了一个HTTP服务器来显示图计算过程的各种信息,用户可以通过网页随时监控图计算执行过程各个细节:
-
图的大小。
-
处于活跃状态的顶点数量。
-
在当前超步的时间信息和消息流量。
-
所有用户自定义Aggregator的值。
5.3 Worker:
在每个Worker中,它所管辖的分区的状态信息是保存在内存中的。分区中的顶点的状态信息包括:
-
顶点的当前值。
-
以该顶点为起点的出射边列表,每条出射边包含了目标顶点ID和边的值。
-
消息队列,包含了所有接收到的、发送给该顶点的消息。
-
标志位,用来标记顶点是否处于活跃状态。
在每个超步中,Worker会对自己所管辖的分区中的每个顶点进行遍历,并调用顶点上的Compute()函数,在调用时,会把以下三个参数传递进去:
-
该顶点的当前值
-
一个接收到的消息的迭代器
-
一个出射边的迭代器
-
一个出射边的迭代器
在Pregel中,为了获得更好的性能,“标志位”和输入消息队列是分开保存的,对于每个顶点而言,Pregel只保存一份顶点值和边值,但是,会保存两份“标志位”和输入消息队列,分别用于当前超步和下一个超步:
-
在超步S中,当一个Worker在进行顶点处理时,用于当前超步的消息会被处理,同时,它在处理过程中还会接收到来自其他Worker的消息,这些消息会在下一个超步S+1中被处理,因此,需要两个消息队列用于存放作用于当前超步S的消息和作用于下一个超步S+1的消息。
-
如果一个顶点V在超步S接收到消息,表示V将会在下一个超步S+1中(而不是当前超步S中)处于“活跃”状态。
当一个Worker上的一个顶点V需要发送消息到其他顶点U时,该Worker会首先判断目标顶点U是否位于自己机器上:
-
如果目标顶点U在自己的机器上,就直接把消息放入到与目标顶点U对应的输入消息队列中。
-
如果发现目标顶点U在远程机器上,这个消息就会被暂时缓存到本地,当缓存中的消息数目达到一个事先设定的阈值时,这些缓存消息会被批量异步发送出去,传输到目标顶点所在的Worker上。
-
如果存在用户自定义的Combiner操作,那么当消息被加入到输出队列或者到达输入队列时,就可以对消息执行合并操作,这样可以节省存储空间和网络传输开销。
5.4 容错性:
Pregel采用检查点机制来实现容错。在每个超步的开始,Master会通知所有的Worker把自己管辖的分区的状态(包括顶点值、边值以及接收到的消息),写入到持久化存储设备:
-
Master会周期性地向每个Worker发送ping消息(心跳机制),Worker收到ping消息后会给Master发送反馈消息。如果Master在指定时间间隔内没有收到某个Worker的反馈消息,就会把该Worker标记为“失效”。同样地,如果一个Worker在指定的时间间隔内没有收到来自Master的ping消息,该Worker也会停止工作。
-
每个Worker上都保存了一个或多个分区的状态信息,当一个Worker发生故障时,它所负责维护的分区的当前状态信息就会丢失。Master监测到一个Worker发生故障“失效”后,会把失效Worker所分配到的分区,重新分配到其他处于正常工作状态的Worker集合上,然后这些分区会从最近的超步S开始时写出的检查点中,重新加载状态信息。
5.5 Aggregator:
每个用户自定义的Aggregator都会采用聚合函数对一个值集合进行聚合计算得到一个全局值:
-
每个Worker都保存了一个Aggregator类的对象实例集,其中的每个实例都是由类型名称和实例名称来标识的。
-
在执行图计算过程的某个超步S中,每个Worker会利用一个Aggregator对当前本地分区中包含的所有顶点的值进行归约,得到一个本地的局部归约值。
-
在超步S结束时,所有Worker会将所有包含局部归约值的Aggregator的值进行最后的汇总,得到全局值,然后提交给Master。
-