DreamWorks

Never say Never。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

MapReduce流程分析

Posted on 2013-07-16 21:54  _Babyface  阅读(561)  评论(0编辑  收藏  举报

MapReduce是一个用于大规模数据处理的分布式计算模型,通过Map函数来处理一个Key/value对以便生成一批中间的Key/Value对,再定义一个reduce函数将所有这些中间的、有着相同的Key的values合并起来。例如:map:(k1,v1) →list(k2,v2)   reduce:(k2,list(v2))→list(k3,v3)

其MapReduce编写的大致模板如下:

MapReduce的创建
1、Map过程的创建
public static class Map extends Mapper<k1,V1,K2,V2>{
    public void map(K1 k1,V1 v1,Context context) throws Exception{
        
    }
}
2、Reduce过程的创建
public static class Reduce extends Reducer<K2,V2,K3,V3>{
    public void reduce(K2 k2,Iterator<V2> v2,Context context) throws Exception{
        
    }
}
3、MapReduce函数的执行
public static void main(String[] args) throws Exception {
    //实例化配置文件
    Configuration conf = new Configuration();
    //添加输入输出路径
    String[] otherArg = new GenericOptionsParser(conf,args);
    if (otherArgs.length != 2) {
      System.err.println("Usage: Java用例 <in> <out>");
      System.exit(2);
    }
    //配置作业
    Job job = new Job(conf,"JobName");
    //将作业创建为jar包存入HDFS文件系统中
    job.setJarByClass(JobClass);
    //将Map过程导入作业中
    job.setMapperClass(MapClass);
    //必要时添加Combiner过程
    job.setCombinerClass(CombinerClass);
    //将Reduce过程导入作业中
    job.setReducerClass(ReduceClass);
    //设置作业中Reduce输出类的Key类型
    job.setOutputKeyClass(WriteableClass);
    //设置作业中Reduce输出类的Value类型
    job.setOutputValueClass(WriteableClass)
    //添加本地文件系统中的文件输入路径
    FileIuputFormat.addInputPath(job,new Path(srcFilePath));
    //添加HDFS中文件的输出路径
    FileOutputFormat.setOutputPath(job,new Path(dstFilePath));
    //执行该作业
    job.waitForCompletion(true);
}
View Code

大致编写的过程都是在Map与Reduce函数中进行。但是看似简单的编程方式却隐藏了大量的技术的细节,包含很多的网络通信协议,分布式技术,设计模式等等;它最好的好处就是使程序员能够不用过于专注于底层的实现细节而更加的注重业务实现方式。然而分布式系统在进行维护,升级或者调优的过程中,必须又得了解其中实现的技术细节,所以本人在此对自己前一段时间学习MapReduce做个小小的总结,主要目的是了解MapReduce在从提交Job到得出相应数据过程所经历的大致的流程,当然鉴于本人的理解能力有限,所以只能说是个人的阶段性总结,也是为以后能够更深入的了解MapReduce实现的各个细节打个基础吧。

首先先看下MapReduce的详细流程图:

1、首先MapReduce都运行于Hadoop中,所以在执行MapReduce之前先要开启Namenode,Datanode,JobTracker,TaskTracker等,在上一篇分析HDFS中已经给出。

2、以上完成后,当你把编写无误的MR程序通过eclipse启动后就是提交Job阶段,此阶段在JobClient类的submitJob中实现,此函数返回一个RunningJob类型的函数submitJobInternal,submitJobInternal方法在真正提交作业前会首先从hadoop分布式文件系统HDFS初始化一块空间,并在这块共享空间里依次上传3个文件:job.jar(作业的程序包,jar包里包含了执行此类任务所需要的类,例如Mappr和Reducer过程中具体实现类等)、job.split(将文件进行分块,默认为64MB,在JobClient中使用默认的FileInputFormat类,调用FileInputFormat.getSplits()方法生成小数据集,这里的分块不是真正的分割,而是记录文件在HDFS中的路径、偏移量和分片大小)、job.xml(作业配置,比如Mapper,Reducer的类型,Key/value的类型,主要由用户程序中的设置生成)。当上传完这三个文件后,在该函数中通过writeNewSplits(writeOldSplits)来得出分片个数,而后会通过RPC调用JobTracker节点上的submitJob来提交作业。其中JobClient类是通过RPC实现的Proxy接口调用JobTracker上的submitJob方法,而JobTracker必须实现JobSubmissionProtocol接口。JobTracker创建job成功后会返回一个JobStatus对象,该对象记录作业的状态信息,包括调度信息、错误信息以及Map和Reduce任务执行的比例等。JobClient根据这个JobStatus对象创建一个NetworkedJob对象,用于监控JobTracker上的执行过程并打印统计数据到用户的控制台。

3、当在JobTracker提交作业的时候创建JobInprogress来跟踪和调度这个Job,并把它添加到Job队列里。然后通过之前得出的分片个数在initTasks函数中创建相应数目的TaskInprogress来监控和调度相应的MapTask和ReduceTask(一般默认是1个ReduceTask),然后通过createCache函数将为这些未执行的TaskInProgress对象产生一个未执行Map任务缓存nonRunningMapCache(其数据结构是个Map<Node,List<TaskInProgress>>容器,其中Node就是需要进行Map过程的节点,当Slave端的TaskTracker向Master发送心跳时,就可以直接从这个cache中取任务去执行),还有相同的Reduce任务nonRunningReduces.add(reduce[i])(而其数据结构是List<TaskInProgress>容器),然后将分别产生的MapTask和ReduceTask通过序列化写入相应的TaskTracker服务中,最后在TaskTracker的addToTaskQueue中也创建一个TaskInProgress用于监控和调度Task,当每次添加到队列时候就会通过notifyAll()唤醒一个线程,然后取出任务进行MapTask和ReduceTask的处理。

4、之前已经启动过TaskTracker了,而TaskTracker是通过心跳来向JobTracker报告其状态,然后JobTracker根据发送过来的心跳判断TaskTracker是否正常,是否有新的请求,最后回应TaskTracker的心跳传递新的指令。此发生在TaskTracker的run函数通过offerService方法实现心跳的发送,发送心跳的函数是transmitHeartBeat,在该函数中会通过status描述TaskTracker机器状态信息,包括空闲磁盘信息、虚拟和实际内存信息、Map使用内存、Reduce使用内存、可以虚拟和物理内存、累积CPU时间、CPU频率、CPU处理器个数、CPU使用率等,而heartbeat返回TaskTrackerAction数组,其中包括TaskTracker需要做的job或者Task操作,是否开启新的任务。TaskTracker可以从JobTracker取得当前文件系统路径,需要执行Job的jar文件路径等;接收JobTracker返回的心跳指令是heatbeatResponse.getActions[],然后判断是否有新的指令,如果有调用addToTaskQueue方法加入到待执行的队列中,否则加入到tasksToCleanup待清理队列。

5、当JobTracker接收到心跳时候通过heartbeat函数中的prevHeartbeatResponse来判断连接情况,同时创建一个心跳回复实例,再调用processHeartBeat通过返回所包含的的TaskTracker和Task的信息更新JobTracker中相应的数据结构,新建并初始化JobTracker的心跳返回对象,检测将在该TaskTracker上运行的新Task,检测需要被Kill的Task以及需要被kill或者cleanup的Job,更新JobTracker中的trackerID→last sent HeartBeatResponse映射关系,即是trackerToHeartbeatResponseMap结构,最后处理完Heartbeat,删除该TaskTracker上全部“marked”的Tasks,调用方法是把non-running属性的Task标记成“Marked”的Tasks。

6、当JobTrack启动时就在其构造函数中创建一个作业调度器,源代码如下

// Create the scheduler
    Class<? extends TaskScheduler> schedulerClass
      = conf.getClass("mapred.jobtracker.taskScheduler",
          JobQueueTaskScheduler.class, TaskScheduler.class);
    taskScheduler = (TaskScheduler) ReflectionUtils.newInstance(schedulerClass, conf);
View Code

而后在JobQueueTaskScheduler类中创建两个成员变量,一个JobQueueJobInProgressListener,一个EagerTaskInitializationListener;JobQueueJobInProgressListener是JobTracker的一个监听器类,它包含了一个Map类型的jobQueue,用来管理和调度所有JobInProgress,如增加作业、移除作业以及更新作业等。EagerTaskInitializationListener最重要的方法是assignTasks,它实现了工作调度。具体实现过程如下:

  (1)、首先他会计算该作业剩余的Map和Reduce工作量,然后检查每个TaskTracker端还可以承受的平均Map和Reduce任务负载,并继续检查将要派发的任务书是否超出集群的任务平均可负载数。

  (2)、Map任务的分配优先于Reduce任务,如果TaskTracker运行的Map任务数目小于平均工作量,则为此TaskTracker分配一个MapTask。产生Map任务使用JobInProgress的obtainNewMapTask方法,其主要调用JobInProgress的findNewMapTask,根据TaskTracker所在Node从nonRunningMapCache中查找TaskInProgress。在之前初始化时,通过createCache创建未执行任务的缓存时候,会在网络拓扑结构上挂载所有需要执行的TaskInProgress。findNewMapTask方法由近及远分层寻找TaskTracker,首先同一个Node,然后再是同一个机架上的节点,最后再是相同数据中心下的节点,直到maxLevel层结束。应用此寻找机制,在JobTracker给TaskTracker派发任务的时候,可以迅速找到最近的TaskTracker,让其执行。最终生成一个Task类对象,该对象被封装在LanuchTaskAction中,发回给TaskTracker,让其执行任务

  (3)、产生Reduce任务过程与产生Map任务类似,使用obtainNewReduceTask方法,调用JobInProgress的findNewReduceTask方法访问nonRunningReduceCache从中查找TaskInProgress。


这些基本就是一个完成的MR程序所经历的的主要过程,至于其中还有一些细节的实现方法,由于本人也未曾完全清楚的弄明白,毕竟MapReduce这个分布式计算框架所蕴含的技术太多,所以还需日后继续深入的研究。