Hadoop+MapReduce实现原理(二)

第二部分主要介绍,MapReduce的生命周期及其内部实现
主要包括作业提交初始化(JobClient–>JobTracker)、作业执行(JobTracker–>TaskScheduler–>TaskTracker–>Task)

注:文中出现的有序列表代表有步骤顺序(题目除外,题目都是有序号的),其他情况下使用无序列表

一、作业提交及初始化

在这里插入图片描述
如上图所示,作业提交及初始化过程如下:

  1. 用户使用Hadoop的提供的Shell命令提交作业
  2. JobClient按照JobConf将需要的文件都上传到HDFS的某个目录下
  3. JobClient调用RPC接口向JobTracker提交作业
  4. JobTracker将其告知TaskScheduler,TaskScheduler对作业初始化

下面对步骤二、三详细介绍

1. 文件上传

一个作业需要的文件包括程序jar包、程序依赖的第三方jar包、xml作业配置文件及依赖的普通文件等,文件默认是在本地,也可以是事先已经上传到HDFS上了(hdfs://…)

上传文件的时候可以存储到Hadoop的任意节点,因为Hadoop节点间可以共享文件,执行任务的节点可以从任意节点上读取文件下载到自己的工作目录里使用

  1. JobClient会为每个job在hdfs上创建一个目录:
    在这里插入图片描述

  2. 文件上传,并将文件的目录信息保存到JobConf中
    文件分为private级别(只能当前用户使用)和public级别(所有用户可使用),public级别的文件会在每个节点都存一份,提高访问效率。对于经常被访问的文件,也可以提高副本数(默认是10),缓解热访问现象

  3. 产生InputSplit文件:
    JobClient会调用InputFormat中的方法(在第一篇文章介绍过)将数据分片,产生分片后数据和分片元数据,元数据包括分片版本号,分片数目,每个分片的元数据(包括分片长度、分片的host列表(产生方式第一篇文章介绍过))
    这些信息会传给JobTracker,JobTracker再传给TaskTracker

2. JobClient向JobTracker提交作业

  1. JobTracker为每个job创建一个JobInProgress对象
  2. 检查用户是否有向该队列提交作业的权限
  3. 检查用户设置的Map Task和Reduce Task内存是否超过允许的最大内存
  4. 通知TaskScheduler初始化作业:如果是JobTracker自己做初始化,初始化后作业如果还要排队等待时会占很大内存(初始化产生的)
    TaskScheduler是可插拔的,管理员可以自己实现,Hadoop默认调度器是JobQueueTaskScheduler, 调度策略是FIFO

3. TaskScheduler初始化作业

Hadoop将一个作业分为:Setup Task、Map Task、Reduce Task、 Cleanup Task
第一个和最后一个任务没有实际的数据处理,主要完成更改任务状态,清理文件等工作,0.21.0版本后已经改成可选的了

初始化作业主要是将作业分解为若干个Map Task和Reduce Task,并添加到相关数据结构,等待调度(Reduce Task是到Map Task完成数到达一定比例默认5%才开始调度),对任务的创建实际上是创建TaskInProgress对象

4. Hadoop DistributedCache介绍

DistributedCache可以将用户的只读文件在Hadoop的各个节点上做缓存,加快任务执行速度。当一个Task调度到某个TaskTracker时,TaskTracker会在本地把需要的文件存到磁盘上,后续同一个job的Task再调度到这个TaskTracker就可以直接使用了

用户想要使用DistributedCache可以调用相关API或设置命令行参数,public和private级别的文件都可以使用DistributedCache,各节点缓存之后,可访问权限不同





作业在运行过程中,相关的组件主要有JobTracker、TaskScheluder、TaskTracker,我们将通过介绍这三个组件的工作原理和提供的主要功能,来介绍作业的运行过程:

二、JobTracker(作业控制)

JobTracker在作业执行期间负责“总管大局”,且只有一个实例,实际上是一个服务进程,是唯一知道全局信息且负责全局管理(作业管理、资源管理、状态监控、任务调度)的角色
在这里插入图片描述
JobTracker工作原理:

  • JobTracker分为作业控制(图的左边)和资源管理(图的右边)
  • 图最下面:JobTracker进程启动之后就一直监听TaskTracker发送的心跳信息,并存储到自己的数据结构里
  • 图的中间:JobTracker使用“三层多叉树”存储各个作业的状态
    • JIP是JobInProgress,描述一个作业整体状态
    • TIP是TaskInProgress,描述一个任务整体状态,所有任务成功即作业成功
    • TA是Task Attempt,描述一个“任务尝试”的状态,一个任务可能尝试多次,一次成功即任务成功
  • 图的右边:调度器根据TaskTracker所在DataNode的数据分布、剩余资源(slot)、作业优先级、作业提交时间分配任务

1. JobTracker的启动过程

通过JobTracker的main函数,介绍JobTracker的启动过程,包含创建各个对象和启动各个服务

//创建JobTracker对象
JobTracker tracker=startTracker(new JobConf());
// 该过程对一些重要对象进行初始化 
JobTracker startTracker(JobConf jobConf) {
	// 安全管理相关类
	DelegationTokenSecretManager secretManager;
	// 作业级别和队列级别的访问控制:检查用户是否可以提交作业、设置作业的查看和修改权限等
	ACLsManager aclsManager;
	// 调度器
	TaskScheduler taskScheduler;
	// RPC服务
	Server interTrackerServer;
	// 将job、task、task tracker相关信息显示到WEB端的HTTP服务器(封装了web服务器Jetty)
	HttpServer infoServer;
	// JobTracker启动时,恢复到上次停止的状态
	RecoveryManager recoveryManager;
	// 查看作业历史信息的服务
	JobHistoryServer jobHistoryServer;
	/** 将IP或DNS名称映射成网络位置
	** 网络位置表示:数据中心/机架/节点 e.g. /dcX/rackX/nodeX
	** 用户可以编写一个脚本定义转换规则,在jobConf中设置脚本位置
	**/
	DNSToSwitchMapper dnsToSwitchMapper;
}


//启动各个服务线程
tracker.offerService();
void offerService() {
	// 线程清理死掉的TaskTracker(10min没有汇报心跳),清除TaskTracker的相关数据结构,并把它正在处理的任务标为KILLED_UNCLEAN
	expireTrackersThread.start();
	/** 线程将(驻留较长时间默认超过24小时||存储达到上限默认100各)&(已完成)作业信息转移到过期队列里,JobTracker会将已经完成的作业信息放到内存方便外部查询,占用大量内存
	** 过期队列作业抄过1000个(默认)时,作业信息会从内存彻底删除
	**/
	retireJobsThread.start();
	// 线程检查:任务分配给TaskTracker后,10min内未汇报进度,任务该任务分配失败,状态标为FAILED
	expireLaunchingTaskThread.start();
	// 线程将已经完成的作业信息存到HDFS上,便于JobTracker重启恢复和永久查询作业信息,默认该线程不会启用
	completedJobsStoreThread.start();
}

2. JobTracker的心跳接收与应答

JobTracker在运行过程中主要是–心跳接收与应答
在第一篇文文章的RPC通信中简要介绍过一次

/**
* method:TaskTracker向JobTracker汇报节点使用情况和任务运行情况
* param:TaskTrackerStatus:封装了所在节点的资源使用情况(物理内存和虚拟内存总量 和使用量,CPU个数以及利用率等)和任务运行情况(每个任务运行进度,状态以及所处的阶段等)
* param:restarted表示TaskTracker是否刚启动,是的话JobTracker会把它标注为健康的,否则会检查它是否健康
* param:initialContact表示TaskTracker是否初次连接JobTracker
* param:responseId表示心跳响应编号,防止心跳重复发送,每次心跳后加1
* return:HeartbeatResponse.TaskTrackerAction[]:包含了JobTracker向TaskTracker传达的各种命令,主要分为以下几种类型:
*	❑CommitTaskAction: 运行完成,提交其产生的结果。 
*	❑ReinitTrackerAction:重新对自己(TaskTracker)初始化。 
*	❑KillJobAction:杀死某个作业,并清理其使用的资源。 
*	❑KillTaskAction:杀死某个任务。 
*	❑LaunchTaskAction:启动一个新任务。
* return:HeartbeatResponse.heartbeatInterval下次心跳间隔
* return:HeartbeatResponse.recoveredJobs恢复未完成作业列表
*/

// 很重要的一个通信,而且TaskTracker只能通过心跳获取任务
HeartbeatResponse heartbeat(TaskTrackerStatus status, boolean restarted,boolean initialContact,boolean acceptNewTasks,short responseId) throws IOException {
	// 检查TaskTracker
	if(!acceptTaskTracker(status)){ // 检查是否允许该TaskTracker连接JobTracker,当一个TaskTracker在host list(由参数mapred.hosts指定)中,但不在exclude list(由参数mapred.hosts.exclude指定)中时,可接入JobTracker
		throw new DisallowedTaskTrackerException(status);
	}
 	 if(restarted) { 	
 		faultyTrackers.markTrackerHealthy(status.getHost());// 如果该TaskTracker被重启了,则将之标注为健康的TaskTracker,并从黑名单或者灰名单中清除
	} else {
		faultyTrackers.checkTrackerFaultTimeout(status.getHost(),now); // 否则,启动TaskTracker容错机制以检查它是否处于健康状态
	}
 	short newResponseId=(short)(responseId+1); // 响应编号加1 
	status.setLastSeen(now); // 记录心跳发送时间,
	
	// 更新状态
	updateTaskStatuses(trackerStatus); // 更新Task状态信息 
	updateNodeHealthStatus(trackerStatus, timeStamp); // 更新节点健康状态     
	
	// 下达任务
	...
}

	// 计算心跳间隔
	// 心跳间隔大会导致JobTracker无法获取最新的TaskTracker的资源空闲情况,造成资源浪费
	// 心跳间隔小会给JobTracker带来处理压力,JobTracker需要根据TaskTracker规模设置合理的心跳间隔,而且是动态变化的
	// clusterSize是集群数量,集群每增加NUM_HEARTBEATS_IN_SECOND(默认100,最小为1)个节点,心跳间隔增加HEARTBEATS_SCALING_FACTOR秒(默认为1,最小0.01),HEARTBEAT_INTERVAL_MIN为300豪秒
	int heartbeatInterval = Math.max( (int)(1000*HEARTBEATS_SCALING_FACTOR* Math.ceil((double)clusterSize/NUM_HEARTBEATS_IN_SECOND)), HEARTBEAT_INTERVAL_MIN); 

下达各种任务的情况:
ReinitTrackerAction:让TaskTracker重新对自己初始化,因为JobTracker和TaskTracker的状态不一致(JobTracker找不到上次对该TaskTracker的心跳应答||JobTracker找不到TaskTracker状态),TaskTracker需要清理磁盘、初始化各种服务等

LaunchTaskAction:JobTracker为TaskTracker分配任务,优先分配辅助型任务(job-cleanup task、task-cleanup task、job-setup task)、其次由调度器分配一个或多个计算型任务(map、reduce)

KillTaskAction:让TaskTracker杀掉某些任务并清理工作目录、释放slot,杀死任务的原因主要有:用户命令||推测机制的另一个attempt先完成了||作业运行失败需要杀死作业下所有任务||TaskTracker一段时间内没有汇报心跳,标注上面所有task为死亡

KillJobAction:让TaskTracker清理作业的工作目录,原因主要有:用户命令||任务完成||作业运行失败(即作业下任务失败数超过一定比例)

CommitTaskAction:让TaskTracker提交任务,在推测开启情况下,一个task可能被多个attempt同时执行,他们分别写入不同的临时文件,哪个attempt先完成,就把临时文件写入最终存储的文件,最后这个写入过程叫做“任务提交”

3. JobTracker存储的作业状态

上文介绍过JobTracker使用“三层多叉树”存储各个作业的状态,JobInProgress,TaskInProgress和Task Attempt

JobInProgress
jobId = “job_202105201789_0005”job表示这个id是描述作业的,202105201789是作业提交时间,5表示这是这个JobTracker启动的第5个作业
JobInProgress包含静态信息(提交时确定)和动态信息(运行时信息)
静态信息:

//map task, reduce task, cleanup task和setup task对应的TaskInProgress 
TaskInProgress maps[]=new TaskInProgress[0];
TaskInProgress reduces[]=new TaskInProgress[0];
TaskInProgress cleanup[]=new TaskInProgress[0];
TaskInProgress setup[]=new TaskInProgress[0];
int numMapTasks=0;//Map Task个数
int numReduceTasks=0;//Reduce Task个数
final long memoryPerMap;//每个Map Task需要的内存量
final long memoryPerReduce;//每个Reduce Task需要的内存量
volatile int numSlotsPerMap=1;//每个Map Task需要的slot个数
volatile int numSlotsPerReduce=1;//每个Reduce Task需要的slot个数 
/*允许每个TaskTracker上失败的Task个数,默认是4,通过参数mapred.max.tracker.failures 设置。当该作业在某个TaskTracker上失败的个数超过该值时,会将该节点添加到该作业的黑名单中,调度器便不再为该节点分配该作业的任务*/
final int maxTaskFailuresPerTracker;
......
private static float DEFAULT_COMPLETED_MAPS_PERCENT_FOR_REDUCE_SLOWSTART=0.05f; //当有5%的Map Task完成后,才可以调度Reduce Task
int completedMapsForReduceSlowstart=0;//多少Map Task完成后开始调度Reduce Task
......
//允许的Map Task失败比例上限,通过参数mapred.max.map.failures.percent设置
final int mapFailuresPercent;
//允许的Reduce Task失败比例上限,通过参数mapred.max.reduce.failures.percent设置
final int reduceFailuresPercent;
......
JobPriority priority=JobPriority.NORMAL;//作业优先级

动态信息:

int runningMapTasks=0;//正在运行的Map Task数目
int runningReduceTasks=0;//正在运行的Reduce Task数目
int finishedMapTasks=0;//运行完成的Map Task数目
int finishedReduceTasks=0;//运行完成的Reduce Task数目
int failedMapTasks=0;//失败的Map Task Attempt数目
int failedReduceTasks=0;//失败的Reduce Task Attempt数目
......
int speculativeMapTasks=0;//正在运行的备份任务(MAP)数目
int speculativeReduceTasks=0;//正在运行的备份任务(REDUCE)数目
int failedMapTIPs=0;/*失败的TaskInProgress(MAP)数目,这意味着对应的输入数据将被丢弃,不会产生最终结果*/
int failedReduceTIPs=0;//失败的TaskInProgress(REDUCE)数目
private volatile boolean launchedCleanup=false;//是否已启动Cleanup Task
private volatile boolean launchedSetup=false;//是否已启动Setup Task
private volatile boolean jobKilled=false;//作业是否已被杀死
private volatile boolean jobFailed=false;//作业是否已失败 
//节点与TaskInProgress的映射关系,即TaskInProgress输入数据位置与节点对应关系
Map<Node, List<TaskInProgress>>nonRunningMapCache; 
//节点及其上面正在运行的Task映射关系
Map<Node, Set<TaskInProgress>>runningMapCache;
/*不需要考虑数据本地性的Map Task,一个Map Task的InputSplit Location为空的话,调度时就不需考虑本地性*/
final List<TaskInProgress>nonLocalMaps;
//按照失败次数进行排序的TIP集合
final SortedSet<TaskInProgress>failedMaps;
//未运行的Map Task集合
Set<TaskInProgress>nonLocalRunningMaps;
//未运行的Reduce Task集合
Set<TaskInProgress>nonRunningReduces;
//正在运行的Reduce Task集合
Set<TaskInProgress>runningReduces;
//待清理的Map Task列表,比如用户直接通过命令“bin/hadoop job-kill”杀死的Task
List<TaskAttemptID>mapCleanupTasks=new LinkedList<TaskAttemptID>(); 
List<TaskAttemptID>reduceCleanupTasks=new LinkedList<TaskAttemptID>(); 
long startTime;//作业提交时间
long launchTime;//作业开始执行时间
long finishTime;//作业完成时间

TaskInProgress
taskId = "task_202105201789_0005_m_000000",task表示这个id是描述任务的,后面继承jobId的一部分,m表示任务类型(m还是r),0表示任务编号(000000-999999)
TaskInProgress维护的信息:

private final TaskSplitMetaInfo splitInfo;//Task要处理的Split信息 
private int numMaps;//Map Task数目,只对Reduce Task有用
private int partition;//该Task在task列表中的索引
private JobTracker jobtracker;//JobTracker对象,用于获取全局时钟 
private TaskID id;//task ID,其后面加下标构成Task Attempt ID 
private JobInProgress job;//该TaskInProgress所在的JobInProgress 
private final int numSlotsRequired;//运行该Task需要的slot数目 
private int successEventNumber=-1;
private int numTaskFailures=0;//Task Attempt失败次数 
private int numKilledTasks=0;//Task Attempt被杀死次数 
private double progress=0;//任务运行进度
private String state="";//运行状态
private long startTime=0;//TaskInProgress对象创建时间
private long execStartTime=0;//第一个Task Attempt开始运行时间
private long execFinishTime=0;//最后一个运行成功的Task Attempt完成时间
private int completes=0;//Task Attempt运行完成数目,实际只有两个值:0和1
private boolean failed=false;//该TaskInProgress是否运行失败
private boolean killed=false;//该TaskInProgress是否被杀死
private boolean jobCleanup=false;//该TaskInProgress是否为Cleanup Task
private boolean jobSetup=false;//该TaskInProgress是否为Setup Task 
//该TaskInProgress的下一个可用Task Attempt ID
int nextTaskId=0;
//使得该TaskInProgress运行成功的那个Task ID
private TaskAttemptID successfulTaskId;
//第一个运行的Task Attempt的ID
private TaskAttemptID firstTaskId;
//正在运行的Task ID与TaskTracker ID之间的映射关系
private TreeMap<TaskAttemptID, String>activeTasks=new TreeMap<TaskAttemptID, String>();
//该TaskInProgress已运行的所有TaskAttempt ID,包括已经运行完成的和正在运行的
private TreeSet<TaskAttemptID>tasks=new TreeSet<TaskAttemptID>();
//Task ID与TaskStatus映射关系
private TreeMap<TaskAttemptID, TaskStatus>taskStatuses=new TreeMap<TaskAttemptID, TaskStatus>();
//Cleanup Task ID与TaskTracker ID映射关系
private TreeMap<TaskAttemptID, String>cleanupTasks = new TreeMap<TaskAttemptID, String>();
//所有已经运行失败的Task所在的节点列表
private TreeSet<String> machinesWhereFailed=new TreeSet<String>();
//某个Task Attempt运行成功后,其他所有正在运行的Task Attempt保存在该集合中
private TreeSet<TaskAttemptID> tasksReportedClosed=new TreeSet<TaskAttemptID>(); 
//待杀死的Task列表
private TreeMap<TaskAttemptID, Boolean> tasksToKill=new TreeMap<TaskAttemptIDBoolean>();
//等待被提交的Task Attempt,该Task Attempt最终使得TaskInProgress运行成功
private TaskAttemptID taskToCommit;

Task Attempt
attemptId = "attempt_202105201789_0005_m_000000_0",最后一个0表示是该任务的第0次尝试

job状态转换
在这里插入图片描述

PREP→RUNNING:作业的Setup Task(job-setup Task)成功执行完成
RUNNING→SUCCEEDED:作业的Cleanup Task(job-cleanup Task)执行成功
PREP→FAILED/KILLED:人为使用Shell命令杀死作业
RUNNING→FAILED:多种情况可导致该状态转移,包括人为使用Shell命令杀死作业,作业的Cleanup/Setup Task运行失败和作业失败的任务数超过了一定比例
RUNNING→KILLED:人为使用Shell命令杀死作业

task状态转换
在这里插入图片描述
UNASSIGNED→RUNNING:任务初始化状态为UNASSIGNED,当JobTracker将任务分配给某个TaskTracker后,该TaskTracker会为它准备运行环境并启动它,之后该任务进入RUNNING状态。
RUNNING→COMMIT_PENDING:该状态转换存在于产生最终结果的任务(Reduce Task或者map-only类型作业的Map Task)中,当任务处理完最后一条记录后进入COMMIT_PENDING状态,以等待JobTracker批准其提交最后结果。RUNNING→SUCCEEDED:该状态转换只存在于Map Task(且这些Map Task的结果将被Reduce Task进一步处理)中,当Map Task处理完最后一条记录后便意味着任务运行成功
RUNNING/COMMIT_PENDING→KILLED_UNCLEAN:TaskTracker收到来自JobTracker的KillTaskAction命令后,会将对应任务由RUNNING/COMMIT_PENDING状态转化为KILLED_UNCLEAN状态,通常产生的场景是人为杀死任务,同一个TIP的多个 同时运行的Task Attempt中有一个成功运行完成而杀死其他Task Attempt, TaskTracker因超时导致其上所有任务状态变为 KILLED_UNCLEAN等
RUNNING/COMMIT_PENDING→FAILED_UNCLEAN:多种情况下会导致该状态转移,包括本地文件读写错误、Shuffle阶 段错误、任务在一定时间内未汇报进度(从而被TaskTracker杀掉)、内存使用量超过期望值或者其他运行过程中出现的错误
UNASSIGNED→FAILED/KILLED:人为杀死任务。KILLED_UNCLEAN/FAILED_UNCLEAN→FAILED/KILLED:一旦任务进入KILLED_UNCLEAN/FAILED_UNCLEAN状态,接下来必然进入FAILED/KILLED状态,以清理已经写入HDFS上的部分结果
SUCCEEDED→KILLED:一个TIP已有一个Task Attempt运行完成,而备份任务也汇报成功,则备份任务将被杀掉或者用户人为杀死某个Task,而TaskTracker刚好汇报对应Task执行成功
SUCCEEDED/COMMIT_PENDING→FAILED:Reduce Task从Map Task端远程读取数据时,发现数据损坏或者丢失,则将对应Map Task状态标注为FAILED以便重新得到调度

4. JobTracker的错误处理

1)对JobTracker的错误恢复

JobTracker存储着重要且独一无二的全局信息,包括节点信息和作业运行信息
一旦JobTracker因故障重启,节点的健康信息和资源分布可以通过心跳及时恢复,作业运行时信息有三种恢复粒度:作业级别、任务级别和记录级别

恢复粒度版本实现原理优缺点
作业级别Hadoop 0.21.0之后重启后未完成的作业重新提交至Hadoop运行实现简单,浪费多
任务级别Hadoop 1.0.0之前使用事务型日志记录作业及任务状态,重启后对于未开始或运行中的任务重新运行实现复杂,特殊情况多,浪费少
记录级别未实现从失败作业的第一条未处理的记录开始恢复一个任务浪费少

2)对TaskTracker容错机制

Hadoop提供了三种TaskTracker的容错机制:超时机制、灰名单与黑名单、Exclude list与Include list

超时机制

  1. TaskTracker第一次汇报心跳时,JobTracker把它放到过期队列trackerExpiryQueue中
  2. TaskTracker之后每次汇报心跳时,JobTracker都会记录最近心跳时间
  3. 线程expireTrackersThread周期扫描过期队列里的所有TaskTracker,超过一定时间(默认10min)没有汇报心跳,就从集群中移除,移除之前要杀死该TaskTracker上的以下任务,以便调度到其他节点上运行:
    1. 作业状态是等待或运行&&任务是map类型&& 未完成
    2. 作业状态是等待或运行&&任务是map类型&& 已完成 && 下面有reduce task
    3. 作业状态是等待或运行&&任务是reduce类型&& 未完成

灰名单与黑名单
JobTracker会通过算法推断出有问题的TaskTracker,并加入灰名单或黑明单
加入灰名单,一段时间后可以恢复正常
加入黑名单,直至监控脚本发现TaskTracker正常后才可以恢复

灰名单:
一个作业在运行过程中发现同一个TaskTracker上失败的attempt次数超过限制(默认是4)时,会把TaskTracker加入作业黑名单。JobTracker发现某个TaskTracker被n个作业加黑(默认是n=4)且n超过各个TaskTracker被作业加黑平均值的0.5倍(默认)且现有灰名单数目小于TaskTracker总数的50%,会把该TaskTracker加入JobTracker灰名单

实现方式(没看懂,待完善):

JobTracker存储每一个n不为0的TaskTracker被加黑的次数,一个典型的环行桶数据结构(具体参考类JobTracker.FaultInfo)如图6-6所示。默认情况下,它维护了最近 mapred.jobtracker.blacklist.fault-timeout-window(默认是3小时)时间内某个TaskTracker对应的#blacklist值。为了便于计算,环形桶被 分成若干个等时间片(由参数mapred.jobtracker.blacklist.fault-bucket-width配置,默认是15分钟)长度的桶,所有桶的#blacklist值由 整型数组numFaults[]维护,同时由指针lastRotated指向最近一次更新所在桶的第1个毫秒位置,具体操作如下:
❑初始化操作:
lastRotated=(time/bucketWidth)*bucketWidth;/其中,time为当前时间,bucketWidth为桶宽度,经初始化后,lastRotated是bucketWidth的整数倍/
❑checkRotation操作:
将lastRotated到某个新时间点timeStamp之间的桶计数器(#blacklist)清零,同时将lastRotated移动到新时间点对应的桶第一毫秒所在位置。
void checkRotation(long timeStamp){
long diff=timeStamp-lastRotated;
while(diff>bucketWidth){
//lastRotated指向时间最久的桶(它即将成为最新的桶)第一个毫秒的位置
lastRotated+=bucketWidth;
//取得桶下标
int idx=(int)((lastRotated/bucketWidth)%numFaultBuckets);
//清空桶计数器,为写入新值做准备
numFaults[idx]=0;
diff-=bucketWidth;
}
}
❑incrFaultCount操作:将某个时间点对应的桶计数器加1,对应代码如下。
void incrFaultCount(long timeStamp){
checkRotation(timeStamp);
//将lastRotated~timeStamp时间段内桶计数器清零
++numFaults[bucketIndex(timeStamp)];
}
int bucketIndex(long timeStamp){
return(int)((timeStamp/bucketWidth)%numFaultBuckets);
}

黑名单:
使用健康监测脚本(用户编写)检测TaskTracker是够健康(TaskTracker可能还活着可以发送心跳,但是处于无资源、关键服务挂掉等不健康状态),心跳汇报时TaskTracker把脚本的检测结果发送给JobTracker,如果不健康,JobTracker会把TaskTracker加入黑名单,且不再分配任务(上面正在运行的任务可以正常运行结束)直至脚本检测结果为健康

Exclude list和Include list
Exclude list是非法节点列表,列表中的节点无法与JobTracker连接,上面正在运行的任务无法完成

Include list是合法节点列表,只有列表中的节点次啊可以向JobTracker发起连接请求
管理员可以配置两个列表,默认情况下,两个列表均为空,表示任何节点都可以接入JobTracker

灰/黑名单是指TaskTracker,非/合法列表是指host,一个host上可以有多个TaskTracker

3)对Job的错误处理

对于一些场景比如搜索引擎日志分析、网络处理等,数据量巨大,一小部分数据不影响最终结果,因此Hadoop支持用户通过mapred.max.map.failures.percent和mapred.max.reduce.failures.percent两个参数配置允许失败的Map任务数和Reduce任务数占总数的百分比,默认情况下,两个参数为0,只要有一个任务失败则整个作业失败

4)对Task的容错

一个task任务可以尝试运行多次,当超过尝试运行最大次数(mapred.max.map.failures.percent和mapred.max.reduce.failures.percent,默认均为4)后,任务会转为FAILED状态

总结

任务状态产生原因是否会换节点运行是否有次数上限
KILLED1. 用户用命令杀死
2. 磁盘或内存不够时,会按顺序(Reduce Task->进度最慢的Task)依次杀死任务
3. TaskTracker一段时间内没有汇报心跳,会杀死上面所有的任务
不一定,调度器可能还是分配到这个节点上没有
FAILED1. 用户用命令失败
2. 本地文件读写错误
3. reduce task从map task读取数据错误
4. 用户自定义的Counter活Counter Group数目唱过系统要求上限
5. 任务一段时间内(mapred.task.timeout,默认是10min)没有汇报进度
6. 使用内存超过用户设置的预期最大值
7. 任务运行过程中的其他错误,如初始化等

5)对Record的容错

MapReduce运行时是把数据解析成一个个key/value迭代处理的,有些作业可以忽略一些坏记录,MapReduce提供支持跳过坏记录的运行方式,它会分析识别坏记录,并记录下来,下次在运行时直接跳过,
以一个具体例子说明该过程,使用Hadoop Streaming运行程序:

HADOOP_HOME=/opt/dongxicheng/hadoop-1.0.0 
$HADOOP_HOME/bin/hadoop jar\ 
$HADOOP_HOME/contrib/streaming/hadoop-streaming-1.0.0.jar\ 
-D mapred.job.name="Skip-Bad-Records-Test"\
-D mapred.map.tasks=1\         
-D mapred.reduce.tasks=0\
-D mapred.skip.map.max.skip.records=1\             Map任务最多允许跳过的记录数目,默认0
-D mapred.skip.attempts.to.start.skipping=2\       任务失败到多少次才允许启用跳过功能,默认2
-D mapred.map.max.attempts=6\                      任务最多可以尝试运行的次数,一般要比mapred.skip.map.max.skip.records大,不然就没有办法启用跳过功能
-input"/test/input/skip-bad-records-test.txt"\ 
-output"/test/output"\
-mapper"mapper.awk"\
-file"mapper.awk"

假设输入文件中只在361行(从第0行开始计算)中有一条坏记录。根据跳过坏记录算法,仅有的一个Map Task需要尝试运行 5次才会最终运行成功,过程如下:

  1. 前两个Task Attempt尝试处理该文件,但每次到361行均异常退出,导致任务运行失败。
  2. 从第三个Task Attempt开始进入skip mode。该Task Attempt在处理数据过程中,会不断将接下来的数据处理区间汇报给TaskTracker,再由TaskTracker汇报给JobTracker,当处理到第361行时出现错误,此时,JobTracker最后收到的数据处理区间是 Range[361,2] ([数据偏移量,区间长度])
  3. 由于数据处理区间长度超过1(一次最多可跳过坏记录条数为1),JobTracker采用二分法将该区间分裂成两段,分别是 Range[361,1]和Range[362,1],并将第四个Task Attempt作为测试任务,指定其数据处理区间为Range[361,1],即跳过区间 Range[0,361]和Range[362,∞],只处理第361行记录。
  4. 第四个Task Attempt仍然运行失败,此时JobTracker可推断出Range[361,1]为坏记录所在区间,同时将Range[362,1]标注 为正常数据区间,并将该信息传递给第五个Task Attempt。
  5. 第五个Task Attempt在运行过程中跳过坏记录区间Range[361,1],最终运行成功。

6)对磁盘的容错

在作业运行过程中,需要频繁的对写磁盘,Map Task需要写本地磁盘,Reduce需要写HDFS磁盘,磁盘故障率明显高于其他硬件(CPU、内存等),所以对于磁盘不足或出错的情况要及时预测、检测并及时避免

用户最好设置多个目录,以提高写效率,TaskTracker在写数据的时候,可以负载均衡的方式写到这些磁盘:轮询、随机、剩余空间最多优先等,Hadoop选择轮询策略,可以最大程度降低写热点

JobTracker在Map Task运行超过1/10时,会通过运行进度预估作业需要的磁盘空间(按比例预估,结果乘2),并结合Reduce Task的数量预估每个Reduce Task的输入量,如果Task的数据量超过TaskTracker剩余磁盘空间,则不会将Task分给TaskTracker
(没懂,待补充)??不是已经开始运行之后才会预估吗,运行到一半再转移吗

TaskTracker同时自己也要保证自己剩余空间满足mapred. local.dir.minspacestart才可以接受新任务,并周期性检查剩余空间是否低于mapred. local.dir.minspacekill,低于的话按一定策略杀掉一些任务释放空间;
TaskTracker刚启动需要检查mapred.local.dirs目录,这是用户设置的Map Task的中间结果写目录,运行的时候也会周期性(mapred.disk.healthChecker.interval,默认60s)检查这些目录,因为这些都是在本地磁盘,没有备份,一旦损失要重新计算,如果发现目录出现异常(如目录属性变成已读),就会对自己重新初始化,初始化过程相当于JobStracker向TaskTracker下发ReinitTrackerAction命令(前文介绍过)
TaskTracker对磁盘的具体使用可以看这里

5. 任务推测执行

在分布式任务中,同一个作业的不同任务在不同节点执行,而节点间的资源、节点的任务负载,任务本身计算量不同,所以运行速度不一致,运行慢的任务会拖慢整个作业的完成时间,因此Hadoop采用推测执行机制(Speculative Excution)机制,推测出速度慢的任务,并为这些任务启动备份,与原始任务同时执行,使用最先成功的任务的计算结果作为最终结果

用户可分别通过参数mapred.map.tasks.speculative.execution和mapred.reduce.tasks.speculative.execution控制是否对Map Task和Reduce Task启用推测执行功能,默认都开启

1)推测计算假设

  • 每个节点计算能力一致
  • 任务执行进度随时间线性增加
  • 启动备份任务代价忽略不计
  • 同一个job中,相同类型的任务工作量一致
  • 对于Map Task,progress = M/N,M是已读数据量,N是总数据量
  • 对于Reduce Task,progress = 1/3*(K+M/N),K = 0(Shuffle阶段), 1(Sort阶段), 2(Reduce阶段),假设每个阶段所需时间相同

2)1.0.0版本推测算法

若一个任务满足以下条件,就会启动一个备份:

  • 任务没有开启skip mode,skip mode会导致任务变慢
  • 任务没有正在运行的备份(目前Hadoop只允许一个任务同时启动两个attempt)
  • 任务运行超过60s,且任务进度落后平均进度(正在运行的任务)的20%,是进度直接加减20%,不是1.2倍的意思

该算法缺点:

  • 如果其他任务都是100%,这个任务一旦进度超过80%,就不可能开启备份了
  • 备份任务速度要快于原始任务,否则备份没有意义了
  • 20%、60s这些参数不可以配置

3)0.21.0版本推测算法(LATE算法)

为解决上面第2个缺点,提供mapreduce. job.speculative.slownodethreshold参数(默认是1),如果一个TaskTracker上运行任务的速度与所有任务的速度(都是取已经完成的任务的速度来计算)的差值,超过标准方差✖️这个参数,则不会在这个TaskTracker上启动备份任务

为解决上面第3个问题,提供mapreduce. job.speculative.slowtaskthreshold取代20%的固定值,若任务速度慢于所有速度的标准方差✖️这个参数,则为任务启动备份,参数默认是1

提供mapreduce. job.speculative.speculativecap参数,默认0.1,表示一个作业启动备份的任务数不能超过正在运行的任务数的10%

该算法缺点:

  • 任务进度和任务剩余时间估算不准确:这会导致部分正常任务被误认为是“拖后腿”任务,从而造成资源浪费
  • 未针对任务类型对节点分类:尽管LATE算法可通过任务执行速度识别出慢节点,但它未分别针对Map Task和Reduce Task做出更细粒度的识别。而实际应用中,一些节点对于Map Task而言是慢节点,但对Reduce Task而言则是快节点

4)2.0版本推测算法

MapReduce2.0指的是YARN(Yet Another Resource Negotiator)

使用的方法与上面两种完全不一样,上面两种是先找出进度慢的节点,换一个快节点执行,但是快节点重新执行的时间不一定比慢节点继续需要的时间短,有可能备份只是浪费资源

YARN直接计算快节点需要时间(已经完成的任务的平均运行时间)和慢节点剩下时间(按比例计算),两者差最短的任务,为其启动备份(没看懂,“已经完成的任务的平均运行时间”中的任务是指同一个作业的还是同一个节点的任务,待补充)

与前两个版本一样,需要限制备份上限

三、TaskScheduler(任务调度)

TaskScheduler实际上时JobTracker的一部分,在Hadoop中,任务调度是一个可插拔模块,管理员可以自己根据需求实现,在配置文件通过参数制定,JobTracker在初始化的时候会加载该调度器,管理员实现主要是继承TaskScheduler类,实现调度逻辑(assignTasks函数)。

1. Hadoop资源管理

Hadoop将资源以“槽位”slot为单位来表示,一个节点上的资源(CPU、内存和磁盘等)分成若干个Map slot和Reduce slot,数量分别由参数mapred.tasktracker.map.tasks.maximum和mapred.tasktracker.reduce.tasks.maximum指定,Map slot只能被Map Task使用Reduce slot被Reduce Task使用。

一个Task可以使用多少slot由调度器决定,当前大部分调度器只支持一个Task占用一个slot,比如FIFO和Fair Scheduler;而Capacity Scheduler则可根据Task内存需求为其分配多个slot。

Hadoop资源管理优化:

首先,每个节点上的slot总数是在启动时固定的,无法修改,但每个节点资源利用情况不一样,所以可以采取动态调整slot数目的方案,在每个节点上安装SlotsAdjuster,让他根据节点上的资源利用率动态调整slot的数目
(如果想让每个节点上作业负载均衡,不应该是调度器应该负责的事情吗)

其次,Hadoop区分了map专用的slot和reduce专用的slot也不够动态,如果map任务和reduce任务在不同时间数量不一样,slot的分类比例是开始时配置好的,又无法动态调整。对应的解决方案就是只有一种slot,map和reduce都可以用

最后,Hadoop将资源划分的slot资源完全相同,这种划分就不够细粒度,如果一个slot是2G内存+1个CPU,如果任务只需要1G内存就会浪费,需要3G内存就需要抢占其他任务资源(因为有的调度器限制一个任务只能用1个slot),解决方案就是直接让调度器按照内存和CPU进行分配(YARN、Corona等)

2. TaskScheduler与JobTracker的交互

作业提交和调度过程中,JobTracker和TaskScheduler交互如图所示:
在这里插入图片描述

  1. 用户把作业提交给JobTracker
  2. JobTracker把作业发给TaskScheduler,让它把作业初始化
  3. TaskTracker向JobTracker汇报心跳
  4. 如果TaskTracker可以接收新任务,JobTracker就调用TaskScheduler的assignTasks函数
  5. TaskScheduler为按照调度策略为TaskTracker选一个合适的任务列表,返回给JobTracker
  6. JobTracker以心跳应答的方式把任务发给TaskTracker(没懂,最后是发列表还是发一个任务:按照TaskTracker的资源决定发送task的数目)
  7. TaskTracker启动该任务

作业提交和调度过程中,JobTracker和TaskScheduler之间的通信如图所示:
(感觉这个图很清楚)
在这里插入图片描述

3. TaskScheduler内部调度原理

任务调度的时候要根据各个节点的资源情况和作业的优先级综合调度,是一个多目标优化的NP问题(不确定在多项式时间是否有解,但可以在多项时间内验证答案是否正确),TaskScheduler采用三级调度模式,队列–>作业–>任务:
在这里插入图片描述
在Hadoop的调度器当中,不同调度器选择队列和作业的逻辑不同,而在作业中选择任务的逻辑基本相同,都是根据数据本地性选择,具体逻辑是由JobInProgress类中的obtainNewMapTask和obtainNewReduceTask方法实现,里面用到的数据结构也都是JobInProgress类中维护的,一个作业一个,下面介绍obtainNewMapTask和obtainNewReduceTask方法细节:

obtainNewMapTask

调度Map Task时要尽量将任务调度到数据所在节点,数据节点表示:数据中心/机架/节点,上文介绍过,node- local:输入数据与计算资源同节点;rack-local:输入数据与计算资源同机架;off-switch:输入数据与计算资源跨机架。Map Task选择顺序:运行失败的任务–>按照本地性原则选择为运行的–>查找“慢速”任务,为其启动备份:

  1. 合法性检查:如果一个作业在某个节点上失败太多或者节点剩余磁盘容量不足,不再分配给节点任何任务(没懂,不是给TaskTrcker分配任务吗,为什么要检查节点而不检查TaskTrcker,TaskTrcker不是自己会检查自己剩余磁盘容量吗)
  2. failedMaps列表中选择任务:SortedSet<TIP> failedMaps列表保存了失败的Map任务,失败次数越多越靠前,被调度概率越大,这时候不再考虑数据本地性
  3. nonRunningMapCache列表中选任务:Map<Node, List <TIP>> nonRunningMapCache列表保存了节点和未运行TIP的对应关系,按照node- local、rack-local、off-switch的顺序选择任务
  4. nonLocalMaps列表中选择任务:List<TIP> nonLocalMaps列表保存了没有输入数据(即InputSplit为空)且未运行的Map任务,有可能是计算密集型任务,如计算Pi(第一篇文章介绍过)
  5. runningMapCache列表中选择任务:Map<Node, Set<TIP>> runningMapCache保存了节点与正在运行Map任务的映射关系,查找是否有“慢速”任务,如果有则为其启动备份
  6. nonLocalRunningMaps列表中选择任务:List<TIP> nonLocalRunningMaps保存了正在运行的没有输入的Map任务,查找是否有“慢速”任务,如果有则为其启动备份

步骤2~6中任意一个步骤查找到合适的任务就会直接返回

obtainNewReduceTask

Reduce Task没有本地数据可言(没懂,他的输入数据不是map任务的结果吗),调度时只考虑未运行任务和备份任务的调度顺序

  1. 合法性检查,和挑选Map Task一样,检查节点的可靠性和磁盘空间
  2. nonRunningReduces列表中选任务:List<TIP> nonRunningReduces列表保存了未运行的Reduce任务,且没有在对应节点失败过的第一个任务
  3. runningReduces列表中选择任务:List<TIP> runningReduces保存了正在运行的Reduce任务,查找是否有“慢速”任务,如果有则为其启动备份
    (不需要为优先调度失败任务了吗?)

上面只是介绍了选择任务的最后一步——从作业中选择任务,下面完整的介绍一下Scheduler选择任务的过程,因为关于选择任务的前两步,每个调度器实现不一样,我们这里以Hadoop的默认调度器FIFO为例来介绍:

  1. 计算可用的slot数目,FIFO要均衡的使用每个TaskTracker上的资源
    availableSlots = loadFactor ✖️ trackerCapacity - trackingRunningTasks
    loadFactor = min{(numTasks - finishedTasks) / totalSlots, I}
    trackerCapacity是TaskTracker上的slot总数
    trackingRunningTasks是TaskTracker上正在运行的任务数
    numTasks是系统总任务数
    finishedTasks是系统已完成任务数
    totalSlots是系统的slot总数

  2. 对作业初始化、并维护作业的调度顺序,实现方法:调度器会向JobTracker注册两个监听器
    EagerTaskInitializationListener:监听作业的提交、删除、更新等+对作业初始化
    JobQueueJobInProgressListener:监听作业的提交、删除、更新等+维护作业的调度顺序
    初始化和维护的调度顺序都是先按作业优先级、再按提交顺序进行排序

  3. 选择任务,按照第2步维护的调度顺序,选取作业,再按上文介绍的obtainNewMapTask和obtainNewReduceTask方法从作业中选取availableSlots个任务

四、TaskTracker(通信枢纽)

TaskTracker与JobTracker一样也是以组件的形式,是JobTask和Task之间的“沟通桥梁”,是负责任务执行(具体是让下面的Task线程执行)和汇报节点状态(虽然可以一个节点多个TaskTracker,通过端口号区分,但一般都是一个),具体如下图所示:
在这里插入图片描述
TaskTracker启动新任务的时候,会给每个任务创建一个单独的JVM

1. TaskTracker的启动过程

TaskTracker服务是由TaskTracker这个类里面的main函数启动的

public class TaskTracker implements MRConstants, TaskUmbilicalProtocolRunnable, TaskTrackerMXBean{
	......//成员变量定义和成员函数实现
	public static void main(String argv[])throws Exception {
		......
		JobConf conf=new JobConf();
		......
		TaskTracker tt=new TaskTracker(conf); //创建TaskTracker对象 	
		MBeans.register("TaskTracker""TaskTrackerInfo",tt); //注册MXBean,方便外部监控工具获取TaskTracker的运行时信息 
		tt.run(); //启动TaskTracker线程
		......
	}
	......
}

重要变量初始化

volatile boolean running=true; //TaskTracker是否正在运行
InterTrackerProtocol jobClient; //RPC Client,用于与JobTracker通信
short heartbeatResponseId=-1; //心跳响应ID
TaskTrackerStatus status=null; //TaskTracker状态信息
Map<TaskAttemptID, TaskInProgress>tasks=new HashMap<TaskAttemptIDTaskInProgress>(); //该节点上TaskAttemptID与TIP对应关系
Map<TaskAttemptID, TaskInProgress> runningTasks=null;/*该节点上正在运行的TaskAttemptID与TIP对应关系*/ 
//该节点正运行的作业列表。如果一个作业中的任务在节点上运行,则把该作业加入该数据结构 
Map<JobID, RunningJob>runningJobs=new TreeMap<JobID, RunningJob>(); 
boolean acceptNewTasks=true; //是否接受新任务,每次汇报心跳时要将该值告诉JobTracker 
private int maxMapSlots; //TaskTracker上配置的Map slot数目
private int maxReduceSlots; //TaskTracker上配置的Reduce slot数目
private volatile int heartbeatInterval=HEARTBEAT_INTERVAL_MIN; //心跳间隔

重要对象初始化

ACLsManager aclsManager; // 作业权限控制列表
HttpServer server; // 将TaskTracker相关信息显示到web
TaskController taskController; // 用于控制任务的初始化、终结和清理
UserLogManager userLogManager; // 用户日志管理
JvmManager jvmManager; // 管理jvm
Server taskReportServer; // TaskTracker与下面Task通信,是server端
TrackerDistributedCahceManager distributedCacheManager; // 分布式缓存管理
InterTrackerProtocol jobClient; // 与上面的JobTracker通信,是client端
MapEventFetcherThread mapEventsFetcher; // 获取已经完成的Map Task列表,为Reduce Task远程拷贝做准备
TaskMemoryManagerThread taskMemoryManager; // 任务内存监控
TaskLauncher taskLauncher; // 启动任务
Localizer localizer; // 任务本地化,准备任务运行环境
NodeHealthCheckerService healthCheck; // 节点健康状况检查
JettyBugMonitor jettyBugMonitor; // 应对Jetty中存在的bug,临时启动的监控线程

下面主要介绍TaskTracker的两个功能:发送心跳和任务执行

2. TaskTracker发送心跳

TaskTracker发送心跳的流程:
在这里插入图片描述

  • 发送心跳时间:一方面是由上面JobTracker根据各种因素决定并发送给TaskTracker的,另一方面当有某个任务失败或成功时,TaskTracker也会缩短心跳间隔,发送“带外心跳”(是否启用通过mapreduce. tasktracker.outofband.heartbeat设置,默认不启用),心跳间隔 = JobTracker端获取的间隔/(任务成功数*心跳收缩因子+1),心跳收缩因子也是参数设置mapreduce. tasktracker.outofband.heartbeat.damper,默认1000000,基本上就是立刻发送了?
  • 检查TaskTracker和JobTracker的版本一致:版本号是由Hadoop版本号+修订版本号+代码编译用户+校验和 e.g. “1.0.0-dev from 451451 by dongxicheng source checksum e54b3f6cb07ea1cd83 3d1ab0b947ac39”
  • 检查磁盘损坏:这里说的就是mapred.local.dir目录(上面的2.4.6节提到过,用来存放中间结果的)
  • 发送心跳:发送心跳携带的节点信息不是每次都一样,因为不同信息需要汇报的周期不一样,下面会具体介绍,发送的信息内容

状态发送
TaskTracker向JobTracker发送的TaskTrackerStatus包含的主要信息:

String trackerName; // TaskTracker名称
String host; // TaskTracker主机名
int httpPort; // TaskTracker对外的HTTP端口号
int failures; // TaskTracker上已经失败任务总数 
List<TaskStatus> taskReports; // 当前TaskTracker上各个任务运行状态
volatile long lastSeen; // 上次汇报心跳的时间
private int maxMapTasks; // Map slot总数,即允许同时运行的Map Task总数,由参数mapred. tasktracker.map.tasks.maximum设定
private int maxReduceTasks; // Reduce slot总数,即允许同时运行的Reduce Task总数,由参 数mapred.tasktracker.reduce.tasks.maximum设定
private TaskTrackerHealthStatus healthStatus; // TaskTracker健康状态
// TaskTracker资源(内存,CPU等)信息,当askForNewTask为true的时候才需要发送
// askForNewTask为true需要有空闲的(map slot||reduce slot)&剩余磁盘空间大于mapred.local.dir.minspacekill
private ResourceStatus resStatus; 

下面主要介绍其中的taskReports、healthStatus和resStatus
taskReports:各个任务运行状态

public abstract class TaskStatus implements Writable, Cloneable { 
	......
	private final TaskAttemptID taskid;//Task Attempt ID
	private float progress;//任务执行进度,范围为0.0~1.0
	private volatile State runState;/*任务运行状态,包括RUNNING, SUCCEEDED, FAILED,UNASSIGNED, KILLED, COMMIT_PENDING, FAILED_UNCLEAN, KILLED_UNCLEAN*/
	private String diagnosticInfo;//诊断信息,一般为错误信息和运行异常信息
	private String stateString;//字符串形式的运行状态
	private String taskTracker;//该TaskTracker名称,可唯一表示一个TaskTracker,形式如 tracker_mymachine:50010
	private int numSlots;//运行该Task Attempt需要的slot数目,默认值是1
	private long startTime;//Task Attempt开始时间
	private long finishTime;//Task Attempt完成时间
	private long outputSize=-1L;//Task Attempt输出数据量
	private volatile Phase phase=Phase.STARTING;//任务运行阶段,包括STARTING, MAP,SHUFFLE, SORT, REDUCE, CLEANUP
	private Counters counters;//该任务中定义的所有计数器(包括系统自带计数器和用户自定义计数器两种)
	private boolean includeCounters;//是否包含计数器,计数器信息每隔60s发送一次 
	//下一个要处理的数据区间,用于定位坏记录所在区间
	private SortedRanges.Range nextRecordRange=new SortedRanges.Range();
	......
}

healthStatus:节点健康检测结果

static class TaskTrackerHealthStatus implements Writable { 
	private boolean isNodeHealthy; // 节点是否健康
	private String healthReport; // 如果节点不健康,则记录导致不健康的原因 
	private long lastReported; // 上次汇报健康状况的时间
...... }

这个就是上文2.4.2节黑名单介绍中提到的用健康监测脚本(存放在mapred.healthChecker.script.path路径,默认应该是空吧)检测 TaskTracker 是够健康, NodeHealthCheckerService 线程周期性(mapred.healthChecker.interval)调用健康监测脚本并检查其输出,只要 TaskTracker 服务是活着的,该线程会一直运行该脚本,一旦发现节点又变为“healthy”, JobTracker 会立刻将其从黑名单中移除,从而又会为之分配任务
这个机制很方便,可以使用健康检测脚本来检查很多东西,及时发现节点存在的问题,比如让健康检测脚本检查网络、磁盘、文件系统等运行状况,一旦发现特殊情况(网络 拥塞、磁盘空间不足或者文件系统出现问题),通过控制脚本输出暂时让该TaskTracker停止接收新任务以便进行维护

resStatus:当前TaskTracker的使用情况

static class ResourceStatus implements Writable{
	private long totalVirtualMemory; //总的可用虚拟内存量,单位为byte 
	private long totalPhysicalMemory; //总的可用物理内存量
	private long mapSlotMemorySizeOnTT; //每个Map slot对应的内存量
	private long reduceSlotMemorySizeOnTT; //每个Reduce slot对应的内存量 
	private long availableSpace; //可用磁盘空间
	private long availableVirtualMemory=UNAVAILABLE; //可用的虚拟内存量 
	private long availablePhysicalMemory=UNAVAILABLE; //可用的物理内存量 
	private int numProcessors=UNAVAILABLE; //节点总的处理器个数
	private long cumulativeCpuTime=UNAVAILABLE; //运行以来累计的CPU使用时间 
	private long cpuFrequency=UNAVAILABLE; //CPU主频,单位为kHz
	private float cpuUsage=UNAVAILABLE; //CPU使用率,单位为% 
	......
}

获取内存、CPU信息等是由ResourceCalculatorPlugin类实现的,如果是Linux系统,在/proc目录下有meminfo、cpuinfo和stat三个文件夹可以获取到这些信息,如果是其他系统,需要用户自己继承ResourceCalculatorPlugin抽象类实现,并通过参数mapreduce.tasktracker.resourcecalculatorplugin指定
这个信息感觉和上面的健康监测的信息比较相似、都是对内存、cpu、网络等检查

3. TaskTracker命令执行

下面介绍TaskTracker对JobTracker的心跳应答heartbeatResponse的处理,主要有两种作业集合,一种是recoveredJobs,是重启JobTracker后需要恢复的作业;另一部分是需要执行的命令列表的处理,如下所示:

TaskTrackerAction[]actions=heartbeatResponse.getActions(); 
if(reinitTaskTracker(actions)) { //重新初始化
	return State.STALE;
}
if(actions!=null) {
	for(TaskTrackerAction action:actions) {
		if(action instanceof LaunchTaskAction) { // 启动新任务 
			addToTaskQueue((LaunchTaskAction)action);
		} else if(action instanceof CommitTaskAction) { // 提交任务 
			CommitTaskAction commitAction=(CommitTaskAction)action; 	
			if(!commitResponses.contains(commitAction.getTaskID())) { 	
				commitResponses.add(commitAction.getTaskID());
			}
		}else { //杀死任务或者作业
			tasksToCleanup.put(action);
		}
	}
}

下发的任务主要包括启动新任务、提交任务、杀死任务、杀死作业和重新初始化:

启动新任务
启动新任务的流程如下图所示,大部分函数之前都介绍过
runChild()是TaskTracker创建一个新的jvm启动任务,具体启动任务会在
statusUpdate()是Task将通过心跳将任务的Counter值和进度汇报给TaskTracker
具体的更详细的启动任务的过程会在4.4节介绍
在这里插入图片描述
提交任务
任务提交上文也提到过,任务开启推测机制之后可能有多个实例同时运行,把结果写在临时目录,最后先完成的实例需要把结果从临时目录${mapred.output.dir}/_temporary/_${taskid}到最终目录${mapred.output.dir}上,需要注意的是,只要需要将结果写到HDFS的任务才有这个步骤:Reduce Task或者是map-only的Map Task

Hadoop在任务提交过程使用了两阶段提交协议实现

两阶段提交协议(two-phase commit protocol,2PC)是分布式事务中经常采用的协议。它把分布式事务的某一个代理指定为协调者,所有其他代理称为参与者,同时规定只有协调者才有提交或撤销事务的决定权,而其他参与者各自负责在其本地执行写操作,并向协调者提出撤销或提交子事务的意向。两阶段提交协议把事务提交分成两个阶段:

第一阶段(准备阶段):各个参与者执行完自己的操作后,将状态变为“可以提交”,并向协调者发送“准备提交”请求。
第二阶段(提交阶段):协调者按照一定原则决定是否允许参与者提交,如果允许,则向参与者发出“确认提交”请求,参与者收到请求后,把“可以提交”状态改为“提交完成”状态,然后返回应答;如果不允许,则向参与者发送“提交失败”请求,参与者收到该请求后,把“可以提交”状态改为“提交失败”状态,然后退出。

提交任务流程图:
在这里插入图片描述

  1. Task处理完最后一条记录后会向TaskTracker报告,并把状态由RUNNING转为COMMIT_PENDING
  2. TaskTracker会使用带外心跳报告JobTracker
  3. JobTracker会检查这个Task Attempt是不是TaskInprogress里第一个变成COMMIT_PENDING的,是的话通知TaskTracker准许任务提交
  4. TaskTracker将该Task Attempt加入可提交列表commitResponse
  5. Task通过canCommit()轮询自己是否在commitResponse列表里(书里没说,自己猜测的),发现是的话就提交,完成后告诉TaskTracker任务提交完成
  6. TaskTracker会把对应对应任务的状态转为SUCCESSED,并汇报JobTracker

杀死任务
上文介绍了杀死任务的各种原因,但是基本上杀死任务的流程都是基本相同的,下面以用户使用Shell命令杀死任务为例介绍:
在这里插入图片描述

  1. JobTracker收到JobClient的请求后将任务添加到待杀死列表tasksToKill中
  2. JobTracker等心跳来了把杀死任务的命令KillTaskAction封装并返回给TaskTracker
  3. TaskTracker收到KillTaskAction命令后,将任务从列表runningJobs清除,通知directorCleanupThread线程清理本地工作目录,释放占用的slot,同时把任务从RUNNING态转为KILL_UNCLEAN状态,使用带外心跳告诉JobTracker
  4. JobTracker收到之后,将这个Task Attempt改为task-cleanup Task,任务ID和以前一样,添加到清理任务队列mapCleanupTasks/reduceCleanupTasks,并把任务封装成LaunchTaskAction中下次心跳返回给TaskTracker
  5. TaskTracker收到后启动JVM(或重用已有的JVM)执行,主要是清理之前被杀死的Task Attempt写到HDFS的数据,然后将该Cleanup Task变为SUCCESS,并通过心跳告诉JobTracker
  6. JobTracker收到后,检查它是否处于tasksToKill列表(因为Cleanup Task和之前被杀死的Task使用的是同一个ID),如果是的话就把状态改为KILLED,同时修改相应数据结构

为什么被杀死的任务和清理任务要使用同一个任务ID呢,状态先变为KILL_UNCLEAN,再变为SUCCESS,再变为KILLED

杀死作业
杀死作业的命令经常使用,每个作业运行成功都会向各个TaskTracker广播(是JobTracker下的所有TaskTracker)让他们清空该作业的工作目录,下面以用户使用Shell命令杀死作业为例,介绍杀死作业的流程:

在这里插入图片描述
大致流程就是JobTracker先向各个TaskTracker下发关于这个作业下的Setup、Map和Reduce所有任务的KillTaskAction命令(清理任务占的目录和槽位),同时向某个TaskTracker下发关于这个作业的job-cleanup 任务的LaunchTaskAction命令(不知道这个命令在干什么,清理写到HDFS上的数据?),最后向各个TaskTracker下发关于这个作业的KillJobAction命令(清理作业占的目录和相关结构)

  1. JobTracker收到用户命令,作业的JonInProgress对象把jobKilled置为true
  2. 之后第一个向JobTracker汇报心跳的TaskTracker称为TaskTracker1,JobTracker给他返回相关的KillTaskAction(关于setup、map和reduce任务的)和LaunchTaskAction(关于job-cleanup任务的)命令
  3. 之后再向JobTracker汇报心跳的TaskTracker成为TaskTracker2,JobTracker给他返回相关的KillTaskAction
  4. TaskTracker1和TaskTracker2收到命令后执行,KillTaskAction命令的执行过程上面第三步介绍过,最后把任务状态改为KILLED_UNCLEAN;LaunchTaskAction命令没有输入数据,很快完成把任务状态置为SUCCESSED。完成后通过心跳发送给JobTracker
  5. JobTracker收到TaskTracker汇报的是KILLED_UNCLEAN状态的话,且所在的作业的jobKilled为true,则将作业任务置为KILLED态;若JobTracker收到TaskTracker汇报的是SUCCESSED,则向各个TaskTracker广播KillJobAction命令,清理作业占的目录和相关结构(如runningJobs)

重新初始化
重新初始化的原因上文介绍过了,过程与TaskTracker启动一样

4. 任务启动过程

上文只是简单介绍了一下任务的启动过程,下面详细介绍具体过程
主要流程如下图所示:
在这里插入图片描述
作业本地化

作业的第一个任务来的话需要首先进行作业本地化,创建作业目录、文件,之后就是任务的本地化,就是把任务的相关程序、jar包等文件从HDFS上下载到本地。本地化的文件可能比较大,所以不同任务之间采用多线程下载,一个线程负责一个任务的本地化:

void startNewTask(final TaskInProgress tip) { 
	Thread launchThread=new Thread(new Runnable() {
		public void run() {
			......
			RunningJob rjob=localizeJob(tip); // 作业本地化 
			tip.getTask().setJobFile(rjob.getLocalizedJobConf().toString()); 
			launchTaskForJob(tip, new JobConf(rjob.getJobConf()),rjob);
			......
		} 
	} 
}

每个任务初始化前都会先进行作业初始化,为防止多个任务对同时对同一个作业初始化,需要job加锁,但是不止初始化线程会对job加锁,MapEventsFetcherThread线程、TaskTracker.getMap-CompletionEvents()函数都会对他加锁,所以为了减少上锁时间,维护两个变量localized和localizing来表示是否有线程正在本地化

synchronized(rjob) { 
	if(!rjob.localized) { //该作业尚未完成本地化工作 
		while(rjob.localizing){ //另外一个任务正在进行作业本地化 
			rjob.wait(); //等待作业本地化结束
		} //释放rjob锁
		if(!rjob.localized){ //没有任务进行作业本地化 
			rjob.localizing=true; //让当前任务对该作业进行本地化
		}
	}
} 
if(!rjob.localized){ //运行到此,说明当前没有任务进行作业本地化 
	initializeJob(rjob); //进行作业本地化工作
}

作业本地化过程:

  1. 将凭据文件jobToken和作业描述文件job.xml下载到TaskTracker私有文件目录
  2. TaskController.initializeJob函数创建作业目录和相关文件,TaskController类使用与控制任务的初始化、终结和清理等工作的,如下图所示:

在这里插入图片描述
启动JVM:

  1. TaskTracker调用MapTaskRunner或ReduceTaskRunner
  2. TaskRunner.的un()方法,让JvmManager对象启动一个JVM
  3. JvmManager使用mapJvmManager和reduceJvmManager启动一个JVM
  4. JVM选择:
    1. 如果JVM数低于上限(Map slot或Reduce slot),直接启动JVM
    2. 如果JVM数达到上限(Map slot或Reduce slot),查找当前已经启动的JVM,如果满足 当前状态空闲&&复用次数为未超过上限&&与要启动的任务属于一个作业,则复用该JVM
    3. 如果没有找到合适的JVM,则杀掉 (复用次数为达到上限&&与要启动的任务属于一个作业)|| (当前处于空闲&&与要启动的任务不属于一个作业)的JVM
  5. JvmRunner线程调用TaskController的launchTask方法
// org.apache.hadoop.mapred.Child类
// 对应任务启动流程图的下半部分
public static void main(String[]args)throws Throwable{ 
	//创建RPC Client,启动日志同步线程
	......
	while(true){ //不断询问TaskTracker,以获取新任务
		JvmTask myTask=umbilical.getTask(context); //获取新任务 
		if(myTask.shouldDie()){ //JVM所属作业不存在或者被杀死 
			break;
		}else {
			if(myTask.getTask()==null){ //暂时没有新任务 
				//等待一段时间继续询问TaskTracker
				......
        		continue;
			}
		} 
		//有新任务,进行任务本地化 
		......
		taskFinal.run(job, umbilical); //启动该任务
		......
		//如果JVM复用次数达到上限数目,则直接退出
		if(numTasksToExecute>0&&++numTasksExecuted==numTasksToExecute){ 
			break;
		} 
	}
}

任务本地化

  1. 将任务配置添加到作业配置JobConf,两个配置参数重复以任务配置为准,形成最终的任务下的作业配置
  2. 形成任务目录,下图是整个作业目录+任务目录,用虚线框圈起来的是一个任务目录
    在这里插入图片描述

5. 内存监控

TaskMemoryManagerThread线程每隔一段时间(由参数mapred.tasktracker.taskmemorymanager.monitoring-interval指定,默认5 s)
扫描所有正在运行的任务,并按照以下步骤检查它们使用的内存量是否超过上限值:

  1. 构造进程树:通过读/proc/<pid>/stat 文件构造出以该任务进程为根的进程树,因为一个任务不一定只占用一个进程,可能在运行过程中创建其他子进程,例如非JAVA语言编写的MR程序至少有C++和JAVA两个进程

  2. 判断单个任务内存使用是否超过内存最大值:/proc/<pid>/stat 包含进程pid的运行信息,获取出的内存信息单位是page,再通过shell命令获取page对应的内存量,单位是B

    getConf PAGESIZE
    

    不能仅凭内存量来判断该任务是否超内存,还要结合时间,因为JVM是采用“fork()+exec()”模型,进程创建后会复制一份父进程的内存空间,在这一小段时间内存使用会翻倍,所以进程刚启动的时候,把他的“年龄”设为1,TaskMemoryManagerThread每更新一次,各个进程年龄+1

    因此,如果一个任务中 所有“年龄” > 0 的进程使用内存总量超过用户设置的最大值的两倍,或者所有“年龄” > 1 的进程使用内存总量超过用户设置的最大值,则认为该任务内存使用过量,将其杀死,状态置为FAILED

  3. 判断该TaskTracker上所有任务内存使用总量是否超过可用内存总量:按照步骤2计算各个任务使用的内存,如果超过可用内存(slot数与slot对应内存乘积),TaskTracker会选择任务进度最慢的并杀死,直到内存总量在可用总量以下

五、Task(任务运行)

这一节介绍Map Task和Reduce Task内部实现原理,将Map Task分解成Read、Map、Collect、Spill和Combine 五个阶段,将Reduce Task分解成Shuffle、Merge、Sort、Reduce和Write五个阶段
在这里插入图片描述
图中左半部分是Map Task,右半部分是Reduce Task:

Map Task首先调用用户提供的InputFormat中的方法将数据分片成InputSplit,解析成一个个key/value,依次交给map()函数处理,如图是将输入分成3个InputSplit,分别交给3个Map task处理;然后按指定的partition()分片,决定每个key/value交给哪个Reduce Task处理;之后将数据交给用户定义的Combiner进行一次本地规约(用户如果没有规定则直接跳过);最后将处理结果保存到本地磁盘

Reduce Task首先向各个Map Task请求数据,拷贝对应的数据分片,再议key为关键字对数据排序,将key相同的数据分为一组交给对应的reduce()函数处理,将最终结果直接写到HDFS上

中间数据格式
因为输入数据和输出数据都由组件InputFormat和OutputFormat定义了具体的格式,但是Map Task计算的中间数据格式没有定义且需要传输至Reduce节点,所以Hadoop内部设计了一种支持行压缩的数据存储格式——IFile,每条数据记录格式是:

<key-len, value-len, key, value>

可以使用Zlib、BZip2等压缩算法,用户可以通过mapred. compress.map.output配置是否支持中间结果的压缩,默认false

数据排序
在Map Task和Reduce Task中都有对key的排序操作,这是Hadoop的默认行为:

在Map Task中,会将结果暂时放在缓冲区,到达阈值时对这些数据进行排序,再以IFile的形式写到磁盘,当所有数据处理完,对磁盘数据做一次排序,合并成有序的大文件

在Reduce Task中,从每个Map Task处读取文件,如果文件超过一定阈值,则放到磁盘上,否则直接放到内存里。如果磁盘的文件数超过一定阈值,则合并成一个文件,如果内存的文件大小或数目超过一定阈值,则合并放入磁盘。当所有数据数据拷贝完后,Reduce Task对所有数据再进行一次合并。

缓冲区数据排序使用Hadoop实现的快排,IFile文件合并使用基于堆的优先队列:

快排中,Hadoop选择将序列的首尾和中间元素的中位数作为枢纽;使用左右两个索引i和j分别从左右两端进行扫描,i扫描到>=枢纽元素停止,j扫描到<=枢纽元素停止,交换两个元素继续扫描,直至索引相遇;等于枢纽的元素直接放到中间,不再参与后面的递归;当子序列元素数目小于13时,直接使用插入排序

优先队列中,所有文件以小顶堆的方式存储,取堆顶元素,即选取最小的前io.sort.factor(默认是10)个文件,把他们合并成1个文件并插入小顶堆,再迭代进行该步骤,直到所有的文件数小于io.sort.factor个,如下图所示,最小的3个文件是0 3 4,合并成0,最小的3个文件是5 2 6,合并成5:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值