MapReduce作业的提交与初始化过程,即从用户输入提交作业命令到作业初始化的整个过程。该过程涉及JobClient、JobTracker和TaskScheduler三个组件,它们功能分别是准备运行环境、接收作业以及初始化作业。

作业提交与初始化概述

作业提交过程主要为后续作业执行准备环境,主要涉及创建目录、上传文件等操作;而一旦用户提交作业后,JobTracker端便会对作业进行初始化。作业初始化的主要工作是根据输入数据量和作业配置参数将作业分解成若干Map Task以及Reduce Task,并添加到相关数据结构中,以等待后续被调度执行。

可将作业提交与初始化过程分为四个步骤

  • 步骤1:用户使用Hadoop提供的Shell命令提交作业。
  • 步骤2:JobClient按照作业配置信息将作业运行需要的全部文件上传到JobTracker文件系统的(通常为HDFS)某个目录下。
  • 步骤3:JobClient调用RPC接口向JobTracker提交作业。
  • 步骤4:JobTracker接收到作业后,将其告知TaskScheduler,由TaskScheduler对作业进行初始化。

在步骤2中,将作业配置信息上传到hdfs原因主要是:

  • hdfs是一个分布式文件系统,hadoop集群中任何一个节点上的文件都是共享的。
  • 作业文件是运行Task所必需的。一旦被上传到HDFS上后,任何一个Task只需知道存放路径便可以下载到自己工作目录中使用,因而可以看作是一种简便的文件共享方式。

作业提交过程详解

执行Shell命令

假设用java编写一个MapReduce程序,将其打包成jar,然后通过shell命令提交作业。bin/hadoop脚本根据“jar”命令将作业提交给RunJar类处理。

RunJar类中的main函数经解压jar包和设置环境变量后,将运行参数传递给MapReduce程序,并运行之。

用户的MapReduce程序已经配置了作业运行时需要的各种信息,它最终在main函数中调用JobClient.runJob函数(新MapReduce API则使用job.waitForCompletion(true)函数)提交作业,最后经过以下步骤提交到JobTracker端。

作业文件上传

JobClient将作业提交到JobTracker端之前,需要进行一些初始化工作,这些工作由函数JobClient.submitJobInternal(job)实现。

MapReduce作业文件的上传与下载是由DistributedCache工具完成的。用户只需要在提交作业时指定文件位置,至于文件的分发,完全由DistributedCache工具完成,不需要用户参与。

一个典型的Java MapReduce作业,可能包含以下资源。

  • 程序jar包:用户用Java编写的MapReduce应用程序jar包。
  • 作业配置文件:描述MapReduce应用程序的配置信息(根据JobConf对象生成的xml文件)。
  • 依赖的第三方jar包:应用程序依赖的第三方jar包,提交作业时用参数-libjars指定。
  • 依赖的归档文件:应用程序中用到多个文件,可直接打包成归档文件,提交作业时用参数-archives指定。
  • 依赖的普通文件:应用程序中可能用到普通文件,提交作业时用参数-files指定。

上述所有文件在JobClient端被提交到HDFS上,涉及的父目录如下所示

文件上传完毕后,会将这些目录信息保存到作业配置对象JobConf中,其对应的作业属性如下表

DistributedCache将文件分为两种可见级别,分别是private和public。public级别可以被本节点上的所有作业和用户共享,提高效率。但要成为public级别,需要满足以下两个条件:

  • 该文件/目录对所有用户/用户组均有可读权限。
  • 该文件/目录的父目录、父目录的父目录······对所有用户/用户组均有可执行权限。

作业文件上传HDFS后,可能会有大量节点同时下载文件,造成文件访问热点现象,造成性能瓶颈。为此,JobClient上传文件时会调高副本数以通过分摊负载方式避免产生访问热点。

产生InputSplit文件

用户提交MapReduce作业后,JobClient会调用InputFormat的getSplits方法生成InputSplit相关信息。该信息包括:InputSplit元数据信息和原始InputSplit信息。其中第一部分被JobTracker使用,用以生成Task本地性相关的数据结构;而第二部分则被Map Task初始化时使用,用以获取自己要处理的数据。这两部分信息分别被保存到目录${mapreduce.jobtracker.staging.root.dir}/${usr}/.staging/${JobId}下的文件job.splitjob.splitmetainfo中。

InputSplit相关操作放在包org.apache.hadoop.mapreduce.split中,主要包含三个类JobSplitJobSplitWriterSplitMetaInfoReader,它们关系如下

JobSplit主要包括以下三个:

SplitMetaInfo:描述一个InputSplit的元数据信息,包括以下三项内容:

private long startOffset;//该InputSplit元信息在job.split文件中的偏移量
private long inputDataLength;//该InputSplit的数据长度
private String[] locations;//该InputSplit所在host列表

所有InputSplit对应的SplitMetaInfo将被保存到文件job.splitmetainfo中。该文件内容组织方式如下,内容依次为:一个用于标识InputSplit元数据文件头部的字符串“META-SP”,文件版本号splitVersion(当前值为1),作业对应的InputSplit数目length,最后是length个InputSplit对应的SplitMetaInfo信息。

作业在JobTracker端初始化时,需读取job.splitmetainfo文件创建Map Task。

TaskSplitMetaInfo:用于保存InputSplit元信息的数据结构,包括以下内容:

private TaskSplitIndex splitIndex;//Split元信息在job.split文件中的位置
private long inputDataLength;//InputSplit的数据长度
private String[] locations;//InputSplit所在的host列表

host列表信息是任务调度器判断任务是否具有本地性的最重要因素,而splitIndex信息保存了新任务需处理的数据位置信息在文件job.split中的索引,TaskTracker收到该信息后,便可以从job.split文件中读取InputSplit信息,进而运行一个新任务。

TaskSplitIndex: JobTracker向TaskTracker分配新任务时,TaskSplitIndex用于指定新任务待处理数据位置信息在文件job.split中的索引,主要包括两项内容:

private String splitLocation;//job.split文件的位置(目录)
private long startOffset;//InputSplit信息在job.split文件中的位置

作业提交到JobTracker

JobClient最终调用RPC方法submitJob被作业提交到JobTracker端,然后依次进行以下操作:

  1. 为作业创建JobInProgress对象
    JobTracker会为每个作业创建JobInProgress对象,该对象维护了作业的运行时信息。它在作业运行过程中一直存在,主要用于跟踪正在运行作业的运行状态和进度。
  2. 检查用户是否具有指定队列的作业提交权限
    Hadoop以队列为单位管理作业和资源,每个队列分配有一定量的资源,每个用户属于一个或者多个队列且只能使用所属队列中的资源。
  3. 检查作业配置的内存使用量是否合理
    用户提交作业时,可分别用参数mapred.job.map.memory.mbmapred.job.reduce.memory.mb指定Map Task和Reduce Task占用的内存量;而管理员可通过参数mapred.cluster.max.map.memory.mbmapred.cluster.max.reduce.memory.mb限制用户配置的任务最大内存使用量,一旦用户配置的内存使用量超过系统限制,则作业提交失败。
  4. 通知TaskScheduler初始化作业
    JobTracker收到作业后,会交给调度器,由它按照一定策略对作业进行初始化。

之所以选择调度器而不使用JobTracker,主要有以下原因:

  • 作业一旦初始化后便会占用一定量内存资源,为了防止浪费资源,所以使用调度器。
  • 任务调度器的职责是根据每个节点的资源使用情况对其分配最合适的任务,而只有初始化了才能得到调度,因而将作业初始化嵌到调度器。

Hadoop调度器是可插拔模块,用户通过实现TaskScheduler接口设计自己调度器。默认调度器为JobQueueTaskScheduler,采用的调度策略是先来先服务。

JobTracker采用观察者设计模式,将提交新作业告诉TaskScheduler

JobTracker启动时会根据配置参数mapred.jobtracker.taskScheduler构造相应的任务调度器,并调用它的start()方法进行初始化。在该方法中,调度器会向JobTracker注册JobInProgressListener对象以监听作业的添加/删除/更新等事件。其中EagerTaskInitializationListenerJobQueueJobInProgerssListener分别用于初始化和作业排序。

作业初始化过程详解

调度器调用JobTracker.initJob()函数对新作业进行初始化。作业初始化主要工作是构造Map Task和Reduce Task并对它们进行初始化。

Hadoop将每个作业分解成4种类型,分别是Setup Task、Map Task、Reduce Task和Cleanup Task,它们的运行时信息由TaskInProgress类维护。

以上4种任务作用及创建过程如下:

  • Setup Task:作业初始化标识性任务。该任务运行完后,作业由PREP状态变为RUNNING状态,并开始运行Map Task。该类型任务又被分为Map Setup和Reduce Setup Task两种,且每个作业各有一个。它们运行时分别占用一个Map slot 和Reduce slot。由于这两种任务功能相同,因此有且只有一个能够获得运行的机会。
  • Map Task:Map阶段处理数据的任务。其数目及对应的处理数据分片由应用程序中的InputFormat组件确定。
  • Reduce Task:Reduce阶段处理数据的任务。其数目由用户通过参数mapred.reduce.tasks指定。考虑到Reduce Task能否运行依赖于Map Task的输出结果,于是刚开始只会调度Map Task,直到Map Task完成数目达到一定比例,才开始调度Reduce Task.
  • Cleanup Task:作业结束标志性任务,主要完成一些作业清理工作,一旦任务运行成功后,作业由RUNNING状态变为SUCCEEDED状态。

Hadoop DistributedCache原理分析

DistributedCache是Hadoop为方便用户进行应用程序开发而设计的文件分发工具。其大体流程如下:用户提交作业后,Hadoop将由-files和-archives选项指定的文件复制到JobTracker的文件系统中;之后,当某个TaskTracker收到该作业的第一个Task后,该任务将负责从JobTracker文件系统中将文件下载到本地磁盘进行缓存,这样后续Task就可以直接在本地访问这些文件。DistributedCache还可用于软件自动安装部署。用户可采用DistributedCache将需要安装软件分发到各个节点上,每次运行时,会检查软件是否被改过。如果是,则会自动重新下载。

使用方法介绍

用户编写的MapReduce程序会需要一些外部的资源。这时就可以采用DistributedCache使每个Task初始化时能加载这些文件。

使用DistributedCache通常有两种方法:调用相关API和设置命令行参数。

1.调用相关API

DistributedCache允许用户分发归档文件和普通文件。

分为3个步骤:

  • 步骤1:在HDFS上准备好文件,并按照文件可见级别设置目录/文件的访问权限;
  • 步骤2:调用相关API添加文件信息,这里主要是配置作业的JobConf对象;
  • 步骤3:在Mapper或者Reducer类中使用文件,Mapper或者Reducer开始运行前,各种文件已经下载到本地的工作目录中,直接调用文件读写API即可获取文件内容。

2.设置命令行参数

前提是用户编写MapReduce程序时实现了Tool接口支持常规选项。分为两步,第一步与调用相关API中的步骤1相同,第二步则是使用以下两种Shell命令之一提交作业。

$HADOOP_HOME/bin/hadoop jar XXX.jar \


$HADOOP_HOME/bin/hadoop jar XXX.jar \

工作原理分析

DistributedCache工作原理如下,主要功能是将作业文件分发到各个TaskTracker上,具体流程可分为4步:

  • 步骤1:用户提交作业后,DistributedCache将作业文件上传到HDFS上的固定目录中,
  • 步骤2:JobTracker端的任务调度器将作业对应的任务发派到各个TaskTracker上。
  • 步骤3:收到该作业的第一个任务后,由DistributedCache自动将作业文件缓存到本地目录下,然后开始启动该任务。
  • 步骤4:对于TaskTracker接下来收到的任务,收到该作业的第一个任务后,由DistributedCache自动将作业文件缓存到本地目录下,然后开始启动该任务。不会再重复为其下载文件,而是直接运行。

下图为TaskTrakcer中作业目录组织结构,在TaskTracker本地目录中,public级别文件,保存到公共目录${mapred.local.dir}/taskTracker/distcache中,可被该TaskTracker上所有用户共享。对于private级别目录,被保存到用户私有目录${mapred.local.dir}/taskTracker/${usr}下,在该目录下,将DistributedCache文件和作业运行需要文件分别放到子目录distcache和jobcache中,其中jobcache目录相当于作业的工作目录。

DistributedCache中文件或者目录是由专门的一个线程根据文件大小上限和文件/目录数目上限周期性进行清理。

DistributedCache的实现在包org.apache.hadoop.filecache主要包括DistibutedCaheTaskDistibutedCacheManagerTrackerDistibutedCacheManager三个类,功能如下:

  • DistributedCache类:可供用户直接使用的外部类。提供一系列set和get方法以配置作业需借用DistributedCache分发的只读文件。
  • TaskDistributedCacheManager类:Hadoop内部使用的类,用于管理一个作业相关的缓存文件。
  • TrackerDistributedCacheManager类:Hadoop内部使用的类,用于管理一个TaskTracker上所有缓存文件。只用于缓存public可见级别文件。对于private可见级别文件,则由org.apache.hadoop.mapred包中的JobLocalizer类进行缓存