在Yarn的服务组件中,其ResourceManager和NodeManager上拥有多个服务并对外提供,并且ResourceManager和NodeManager上需要响应各种客户端的并发请求,以及其内部维护的任务和资源状态流转。所以在ResourceManager和NodeManager的源代码设计中,其采用了基于服务库的管理编程模型以及基于异步事件驱动调度的并发模型。接下来分析下这两种编程模式:
在Yarn中,其对生命周期较长并需对外提供服务的对象,采用了基于服务的对象管理模型对其进行管理,该模型主要有以下几个特点:
Yarn中关于服务模型的类图(位于包 org.apache.hadoop.service 中)。在这个图中可以看到,所有的服务对象最终均实现了接口Service,它定义了最基本的服务初始化、启动、停止等操作,而AbstractService类提供了一个最基本的Service实现。YARN中所有对象,如果是非组合服务,直接继承AbstractService类即可,否则需继承CompositeService。比如,对于ResourceManager而言,它是一个组合服务,它组合了各种服务对象,包括ClientRMService、ApplicationMasterLauncher、ApplicationMasterService等。其基本继承如下所示;并且在YARN中,ResourceManager和NodeManager属于组合服务,它们内部包含多个单一服务和组合服务,以实现对内部多种服务的统一管理。
YARN中采用了基于事件驱动的异步并发模型,该模型能够大大增强并发性,从而提高系统整体性能。为了构建该模型,YARN将各种处理逻辑抽象成事件和对应事件调度器,并将每类事件的处理过程分割成多个步骤,用有限状态机表示。YARN中的事件处理模型可概括为图所示。
整个处理过程大致为:处理请求会作为事件进入系统,由中央异步调度器(AsyncDispatcher)负责传递给相应事件调度器(Event Handler)。该事件调度器可能将该事件转发给另外一个事件调度器,也可能交给一个带有有限状态机的事件处理器,其处理结果也以事件的形式输出给中央异步调度器。而新的事件会再次被中央异步调度器转发给下一个事件调度器,直至处理完成(达到终止条件)。
在YARN中,所有核心服务实际上都是一个中央异步调度器,包括ResourceManager、NodeManager、MRAppMaster(MapReduce应用程序的ApplicationMaster)等,它们维护了事先注册的事件与事件处理器,并根据接收的事件类型驱动服务的运行。YARN中事件与事件处理器类的关系(位于包 org.apache.hadoop.yarn.event中)如图所示。
当使用YARN事件库时,通常先要定义一个中央异步调度器AsyncDispatcher,负责事件的处理与转发,然后根据实际业务需求定义一系列事件Event与事件处理器EventHandler,并注册到中央异步调度器中以实现事件统一管理和调度。以MRAppMaster为例,它内部包含一个中央异步调度器AsyncDispatcher,并注册了TaskAttemptEvent/TaskAttemptImpl、TaskEvent/TaskImpl、JobEvent/JobImpl等一系列事件/事件处理器,由中央异步调度器统一管理和调度。
服务化和事件驱动软件设计思想的引入,使得YARN具有低耦合、高内聚的特点,各个模块只需完成各自功能,而模块之间则采用事件联系起来,系统设计简单且维护方便。
为了说明YARN服务库和事件库的使用方法,介绍一个简单的实例,该实例可看做MapReduce ApplicationMaster(MRAppMaster)的简化版。该例子涉及任务和作业两种对象的事件以及一个中央异步调度器。 步骤如下:
1)定义Task事件
public class TaskEvent extends AbstractEvent {
private String taskID; //Task ID
public TaskEvent(String taskID, TaskEventType type) {
super(type);
this.taskID = taskID;
}
public String getTaskID() {
return taskID;
}
}
其中, Task事件类型定义如下:
public enum TaskEventType {
T_KILL,
T_SCHEDULE
}
2)定义Job事件
public class JobEvent extends AbstractEvent {
private String jobID;
public JobEvent(String jobID, JobEventType type) {
super(type);
this.jobID = jobID;
}
public String getJobId() {
return jobID;
}
}
其中,Job事件类型定义如下:
public enum JobEventType {
JOB_KILL,
JOB_INIT,
JOB_START
}
3)事件调度器
接下来定义一个中央异步调度器,它接收Job和Task两种类型事件,并交给对应的事件处理器处理,代码如下:
public class SimpleMRAppMaster extends CompositeService {
private Dispatcher dispatcher; //中央异步调度器
private String jobID;
private int taskNumber; //作业中包含的任务数
private String[] taskIDS; //该作业中包含的所有任务
public SimpleMRAppMaster(String name, String jobID, int taskNumber) {
super(name);
this.jobID = jobID;
this.taskNumber = taskNumber;
this.taskIDS = new String[taskNumber];
for(int i = 0; i < taskNumber; i++){
this.taskIDS[i] = new String(jobID+"_task_"+i);
}
}
@Override
protected void serviceInit(Configuration conf) throws Exception {
dispatcher=new AsyncDispatcher(); // 定义一个中央异步调度器
// 分别注册Job和Task事件调度器
dispatcher.register(JobEventType.class, new JobEventHandller());
dispatcher.register(TaskEventType.class, new TaskEventHandller());
addService((Service)dispatcher);
super.serviceInit(conf);
}
public Dispatcher getDispatcher(){
return dispatcher;
}
private class JobEventHandller implements EventHandler{
@Override
public void handle(JobEvent event) {
if(event.getType() == JobEventType.JOB_KILL){
System.out.println("收到杀死作业事件,要杀掉作业"+event.getJobID()+"下的所有任务");
for(int i = 0; i <= taskNumber; i++){
dispatcher.getEventHandler().handle(new TaskEvent(TaskEventType.T_KILL, taskIDS[i]));
}
}else if(event.getType()== JobEventType.JOB_INIT){
System.out.println("收到启动作业事件,要启动作业"+event.getJobID()+"下的所有任务");
for(int i = 0; i <= taskNumber; i++){
dispatcher.getEventHandler().handle(new TaskEvent(TaskEventType.T_SCHEDULE, taskIDS[i]));
}
}
}
}
private class TaskEventHandller implements EventHandler{
@Override
public void handle(TaskEvent event) {
if(event.getType()==TaskEventType.T_KILL){
System.out.println("收到杀死任务命令,开始杀死任务"+event.getTaskID());
}else if(event.getType()==TaskEventType.T_SCHEDULE){
System.out.println("收到启动任务命令,开始启动任务"+event.getTaskID());
}
}
}
}
4)测试程序
public class SimpleMRAppMasterTest {
public static void main(String[] args) throws Exception {
String jobID = "job_20201215_12";
SimpleMRAppMaster appMaster = new SimpleMRAppMaster("Simple MRAppMaster", jobID, 5);
YarnConfiguration conf = new YarnConfiguration(new Configuration());
appMaster.serviceInit(conf);
appMaster.serviceStart();
appMaster.getDispatcher().getEventHandler().handle(
new JobEvent(jobID, JobEventType.JOB_KILL));
appMaster.getDispatcher().getEventHandler().handle(
new JobEvent(jobID, JobEventType.JOB_INIT));
}
在MRv1中,对象之间的作用关系是基于函数调用实现的,当一个对象向另外一个对象传递信息时,会直接采用函数调用的方式,且整个过程是串行的。比如,当TaskTracker需要执行一个Task时,将首先下载Task依赖的文件(JAR包、二进制文件等、字典文件等)、然后执行Task。同时在整个过程中会记录一些关键日志,该过程描述如下图。在整个过程中,下载依赖文件是阻塞式的,也就是说,前一个任务未完成文件下载之前,后一个新任务将一直处于等待状态,只有在下载完成后,才会启动一个独立进程运行该任务。尽管后来MRv1通过启动过独立线程下载文件解决了该问题,但这种方式不是在大系统中彻底解决问题之道,必须引入新的编程模型。
基于函数调用的编程模型是低效的,它隐含着整个过程是串行、同步进行的。相比之下,MRv2引入的事件驱动编程模型则是一种更加高效的方式。在基于事件驱动的编程模型中,所有对象被抽象成了事件处理器,而事件处理器之间通过事件相互关联。每种事件处理器处理一种类型的事件,同时根据需要触发另外一种事件,该过程如下图所示,当A需要下载文件时,只需向中央异步处理器发送一个事件即可(之后可以继续完成后面的功能而无须等待下载完成),该事件会被传递给对应的事件处理器B,由B完成具体的下载任务。一旦B完成下载任务,便可以通过事件通知A。
相比于基于函数调用的编程模型,这种编程方式具有异步、并发等特点,更加高效,因此更适合大型分布式系统。