对Giraph的一些理解
这两天又重新看了一下Giraph源码,对整体架构的理解又有了新的认识和理解,下面逐点来说。
一、 Giraph本质的理解:
大家都知道,Giraph对用户来讲可能是一个基于Pregel模型的图运算项目,但是对于Hadoop来讲,其实它是一个普通的MapReduce任务。因此我们在运行时可以把他看成是一个mapreduce任务,只是这个任务有点特殊和复杂。
特殊在,它没有像普通mapreduce任务一样重写map和reduce函数,而只是写了run函数,所以一切逻辑都在run函数中呈现,map、reduce函数都是空函数,并且这个对于hadoop的job没有reduce任务,只有map任务。
复杂在,它不是简单的map任务,在run函数中实现了一切pregel逻辑,包括数据加载、图数据重分区、图数据的计算等。
二、 Giraph到底利用了hadoop的什么?
首先,用到mapreduce框架,是不是感觉有点蒙圈了?通过第一点的讲解,大家知道了Giraph的本质,但是换个角度想想,它其实只是用到了hadoop的分布式任务启动的功能,也就是说,hadoop给这个特殊的Job启动了N个maptask以后就再也没做什么。
那么问题来了,使用过Giraph的伙伴们都知道,在运行命令里我们可以指定work的数数目(-w N),而且神奇的发现,hadoop确实启动了N+1(多出来的一个是master)个map(worker)任务,可能有人会说,我们在使用hadoop的可以指定map数量,嗯嗯,这个说法没错,难道这么简单就完了吗?仔细想想,用户指定的hadoop数目不一定能起到作用啊,hadoop在确定map任务数量时候是根据它在hdfs上的block数量来启动的,默认每个block启动一个map任务,所以当我们在giraph启动时人工指定的map数目不一定起作用。其实giraph用了一个很聪明的办法,重写InputFormat啊,在giraph中它为hadoop指定的InputFormat是一个“空”的-----BspInputFormat,这个BspInputformat中一个重要的方法getsplits()其实获得的是含有一个空路径的splitlist,但是他的数量是N+1。所以hadoop在确定启动多少个maptask时待用这个方法得到了含有N+1个split的list,启动了N+1个maptask来作为Giraph的worker,但是split中文件路径为空,所以Hadoop没有加载任何数据。
其次,giraph确实用到了hadoop的hdfs,这个没什么说的。
三、 Giraph同步和通信的理解
可能你又会问,Giraph这个框架要实现Pregel必须满足同步机制和通信等功能,如何实现的呢?
首先来说说通信吧,在Giraph中,因为它本质上是map任务,但没有用到mapreduce框架的任何通信功能,它的所有通信都是用的Netty这个apache开源分布式通信工具完成的,也就是不同worker之间的消息(包括节点消息等)都是netty完成的。
其次是同步机制,学习hadoop生态圈的对zookeeper都不会陌生,zookeeper是一个类似于简单分布式文件系统的分布式协同管理工具(具体的内容自己看吧)。在Giraph中,每一个迭代步中worker都会在zookeeper上某个路径下(设为A)建立相应迭代步的znode,并且监听“本步迭代完成标志节点”是否已经创建,master的任务就是不停检查A路径下的znode节点数目是否等于worker数目,若等于则创建“本步迭代完成标志节点”,此时worker会监听到,进入下一个迭代步。
四、 Giraph的这个工作流程的理解
这个本来应该画一个流程图来说明更加直观的,由于一些原因先文字叙述一下,后期再补吧。
通过第二点可以知道,当GiraphJob提交以后,hadoop为Giraph启动了N+1个任务后(此时的任务叫做maptask,因为还没有确定哪个maptask是master哪个是worker),那么Giraph接下来都做了哪些内容呢?
1. GIraph要确定集群上有没有zookeeper服务(用户可以指定),如果有,则进入第2步,如果没有此时要启动zookeeper服务。熟悉zookeeper的小伙伴应该知道,zookeeper中应该有一个主服务器和多个从服务器来为(client)集群服务,因此Giraph首先要启动zookeeper服务器,Giraph中默认启动一个zookeeper服务器。这个过程又是怎样的呢?
1.1 启动好的多个maptask首先在hdfs的指定路径下创建本task的标志(例如,路径B/host1 taskID) 。
1.2 每个task启动一个zookManage(类名可能不正确,详细见源码)对象,这个对象的主要任务就是从这些个maptask中选取一个作为zookeeper服务器,经验证这个始终都是task0,有兴趣的可以自己看下源码。
1.3 Task0将zookeeper服务器启动,并在hdfs上挂出服务器地址和端口,其他task读取地址和端口,所有task进入第2步。
2. Giraph启动了zookeeper服务以后,接下来要做的就是确定每个task的角 色,也就是确定它具体是master还是worker,判断逻辑如下:
a) If not split master, everyone does the everything and/or runningZooKeeper.
b) If split master/worker, masters also run ZooKeeper
c) If split master/worker == true and giraph.zkList is set, the masterwill not instantiate a ZK instance, but will assume a quorum is already activeon the cluster for Giraph to use.
最终的结果就是,作为zookeeper服务器的task0兼任master,这个task不参与具体运算。其他的task为worker,参与具体运算。
3. 接下来,master和worker开始做不同的事情,并且开始进行同步了,这个过程利用了刚刚建好的zookeeper服务来完成。
master要做的事情就是加载用户输入文件的splits信息,这个信息是利用用户定义的InputFormat(注意区分BspInputFormat)来获取的,此时获取的splits才是真正的数据,master会将splits信息以znode节点的形式挂到zookeeper上,同时master根据splits信息创建分区,这里需要说明一下分区数和splits数目,splits数目一般是hdfs上的blocks(这个和hadoop相似)的数量,分区数目则不同,分区数目是通过系统的一个计算公式(partitionCount=PARTITION_COUNT_MULTIPLIER* availableWorkerInfos.size() * availableWorkerInfos.size())来创建,这个数目要比workers数目大,得到这个数目后,会将分区安排到不同的work上,并将该partionToWorker这个信息也以znode的形式挂到zookeeper上。
其他的worker在master生成splits信息和partion信息之前一直处于同步等待阶段,当master将信息挂载到zookeeper后,worker从zookeeper上读取splits信息,并且开启线程进行数据加载,数据加载过程线面会详细讲解。加载完数据以后,各个worker开始将自己load进来的点计算它属于的分区号,并查到属于哪个计算节点,并将其发送到相应计算节点。
4. 最后,当数据都加载到各个worker上以后开始进行迭代计算。
五、 Giraph中涉及到多线程的地方
在Giraph中,主要涉及到多线程的有两个地方,一个是worker在将split中所指的数据加载到内存时使用了线程;另一个是worker在迭代中对顶点计算时候使用了多线程。
先说第一个,当master把splits信息放到zookeeper上以后,每个Worker创建N个InputsCallable线程读取数据。N=Min(NUM_INPUT_THREADS,maxInputSplitThread),其中NUM_INPUT_THREADS默认值为1,maxInputSplitThread=InputSplitSize-1/maxWorkers+1。那么,默认每个worker就是创建一个线程来加载数据。在每个线程中,它们会不停的遍历splits列表上的信息,当遇到一个没有被加载的splits时,首先会在zookeeper上创建相应splits的正在加载标签,然后加载完以后会创建已加载标签。当加载完这个split之后继续进行上述过程,进行加载。
再说第二个,当worker把数据加载到内存并且已经将顶点发送到属于它的worker上以后,每个worker会得到若干个分区(原因在前面已经讲过)。此时开始进行迭代计算,也是pregel的核心内容。具体的,每个worker会开启N个线程来进行计算,此时worker会将本地的partionID存储到一个队列,partionID所对应的数据会放在一个叫做serviceWorker的对象中存储。每个线程中,它会不停地遍历属于本worker上的partion,对每一个partion上的顶点进行遍历运行用户定义的compute函数,直到所有partion完成。
需要说明的是,上述的“N”是可以自定义的,不过一般都是采用默认。