以Yarn、HDFS和MapReduce为主要组成的Hadoop,涉及到大量复杂的、交互的事件处理、状态转换,同时,这些事件调度和状态转换又对实时性和效率提出了极高的要求。可以想见,没有一个规整的、通用型良好的调度器,Hadoop代码无论是对读者,还是对开发者,都将变成一场灾难,同时,Hadoop的运行效率也会变得无法忍受。统一的、设计良好的、通用的和共用的调度器,对于Hadoop不同组件的开发者来说是一种解脱,大大降低了Hadoop在事件调度、状态转换的底层出错的可能性,提高了代码稳定性和可读性。这篇文章主要介绍了Hadoop的核心调度器AsyncDispatcher的设计和实现。同Hadoop状态机一样,这个通用调度器设计得十分通用,完美可扩展可重用,我们在自己的项目中完全可以使用Hadoop的调度器实现我们自己的事件调度逻辑。
先抛开代码本身,我们来看一个基于事件的调度器的工作方式:
public AsyncDispatcher() {
this(new LinkedBlockingQueue<Event>());
}
public AsyncDispatcher(BlockingQueue<Event> eventQueue) {
super("Dispatcher");
this.eventQueue = eventQueue;
this.eventDispatchers = new HashMap<Class<? extends Enum>, EventHandler>();
}
在它的构造方法中,有两个核心变量,eventQueue是一个队列,用来存放事件,同时,还构造了一个eventDispatchers,这是一个map对象,用来管理事件和事件处理器之间的关系。AsyncDispatcher同样遵循我一直提到的Yarn的服务化设计,即AsyncDispatcher被抽象为一个service,在完成对象构造以后,会进行服务的初始化和启动:
@Override
protected void serviceInit(Configuration conf) throws Exception {
this.exitOnDispatchException =
conf.getBoolean(Dispatcher.DISPATCHER_EXIT_ON_ERROR_KEY,
Dispatcher.DEFAULT_DISPATCHER_EXIT_ON_ERROR);
super.serviceInit(conf);
}
初始化操作并没有做太多的事情,只是调用父类的serviceInit()方法,用来将配置文件设置进来。
@Override
protected void serviceStart() throws Exception {
//start all the components
super.serviceStart();
eventHandlingThread = new Thread(createThread());
eventHandlingThread.setName("AsyncDispatcher event handler");
eventHandlingThread.start();
}
serviceStart()方法的主要任务是创建了一个名字叫做AsyncDispatcher event handler的独立线程,我们一起来看这个线程的代码:
Runnable createThread() {
return new Runnable() {
@Override
public void run() {
while (!stopped && !Thread.currentThread().isInterrupted()) {
drained = eventQueue.isEmpty();
// blockNewEvents is only set when dispatcher is draining to stop,
// adding this check is to avoid the overhead of acquiring the lock
// and calling notify every time in the normal run of the loop.
if (blockNewEvents) {
synchronized (waitForDrained) {
if (drained) {
waitForDrained.notify();
}
}
}
Event event;
try {
event = eventQueue.take();
} catch(InterruptedException ie) {
if (!stopped) {
LOG.warn("AsyncDispatcher thread interrupted", ie);
}
return;
}
if (event != null) {
dispatch(event);
}
}
}
};
}
线程的大致功能,是不断循环检测事件队列eventQueue中是否有新的事件到来,如果有,则取出这个事件的处理器,一个EventHandler接口的实现类,调用dispatch()方法,对该事件进行处理。
那么问题来了,这个异步调度器会管理多个事件和事件调度器,那么它什么时候开始知道某个事件发生的时候改用什么调度器对象对这个事件进行处理呢?这就是register方法:
/**
* 注册事件分派器
*/
public void register(Class<? extends Enum> eventType,
EventHandler handler) {
/* check to see if we have a listener registered */
EventHandler<Event> registeredHandler = (EventHandler<Event>)
eventDispatchers.get(eventType);
LOG.info("Registering " + eventType + " for " + handler.getClass());
if (registeredHandler == null) {
eventDispatchers.put(eventType, handler);
} else if (!(registeredHandler instanceof MultiListenerHandler)){
/* for multiple listeners of an event add the multiple listener handler */
MultiListenerHandler multiHandler = new MultiListenerHandler();
multiHandler.addHandler(registeredHandler);
multiHandler.addHandler(handler);
eventDispatchers.put(eventType, multiHandler);
} else {
/* already a multilistener, just add to it */
MultiListenerHandler multiHandler
= (MultiListenerHandler) registeredHandler;
multiHandler.addHandler(handler);
}
}
还有一个问题,不同的事件,是怎么放入eventQueue中的呢?这是通过AsyncDispatcher.GenericEventHandler.handle()方法进行的:
class GenericEventHandler implements EventHandler<Event> {
public void handle(Event event) {
if (blockNewEvents) { //如果该标记为置位,说明可能服务正在进行stop操作,无法处理新的请求
return;
}
drained = false;
/* all this method does is enqueue all the events onto the queue */
int qSize = eventQueue.size();
if (qSize != 0 && qSize % 1000 == 0
&& lastEventQueueSizeLogged != qSize) {
lastEventQueueSizeLogged = qSize;
LOG.info("Size of event-queue is " + qSize);
}
int remCapacity = eventQueue.remainingCapacity();
if (remCapacity < 1000) {
LOG.warn("Very low remaining capacity in the event-queue: "
+ remCapacity);
}
try {
eventQueue.put(event);
} catch (InterruptedException e) {
if (!stopped) {
LOG.warn("AsyncDispatcher thread interrupted", e);
}
// Need to reset drained flag to true if event queue is empty,
// otherwise dispatcher will hang on stop.
drained = eventQueue.isEmpty();
throw new YarnRuntimeException(e);
}
};
}
AsyncDispatcher内部维护了一个GenericEventHandler handlerInstance变量,通过z这个handlerInstance.handle()来将新的事件放入到eventQueue中。这个方法名字虽然叫做handle(),但是由于我们的AysncDispatcher是异步调度器,因此并没有立刻进行处理,而是放入队列,由上面提到的eventHandlingThread线程进行处理。
先看AsyncDispatcher.serviceStop()方法:
protected void serviceStop() throws Exception {
if (drainEventsOnStop) {
blockNewEvents = true; //首先阻止新任务的分派,试图优雅停掉当前线程的工作
LOG.info("AsyncDispatcher is draining to stop, igonring any new events.");
long endTime = System.currentTimeMillis() + getConfig()
.getLong(YarnConfiguration.DISPATCHER_DRAIN_EVENTS_TIMEOUT,
YarnConfiguration.DEFAULT_DISPATCHER_DRAIN_EVENTS_TIMEOUT);
synchronized (waitForDrained) {
while (!drained && eventHandlingThread != null
&& eventHandlingThread.isAlive()
&& System.currentTimeMillis() < endTime) {
waitForDrained.wait(1000);
LOG.info("Waiting for AsyncDispatcher to drain. Thread state is :" +
eventHandlingThread.getState());
}
}
}
stopped = true;
if (eventHandlingThread != null) {
eventHandlingThread.interrupt();//防止线程正在处理一个耗时任务导致线程依然没有退出
try {
eventHandlingThread.join();//等待eventHandlingThread执行完毕
} catch (InterruptedException ie) {
LOG.warn("Interrupted Exception while stopping", ie);
}
}
serviceStop()调用一开始,就将blockNewEvents=true置位,这意味着将不再接受新的event提交,然后的主要工作,就是需要等待所有的serviceStop()调用前提交的事件都被执行完毕再真正停止服务,否则会造成任务丢失。waitForDrained.wait(1000);代表着服务关闭的时候会每1s检查我们的eventQueue中的所有事件是否处理完毕,如果没有处理完毕,则继续等待。虽然1s已经非常短暂,但是对于Yarn这样的高并发系统,也必须进行优化。
为了能够在eventQueue清空的时候及时通知serviceStop()所在线程,我们看这段代码:
if (blockNewEvents) {
synchronized (waitForDrained) {
if (drained) {
waitForDrained.notify();
}
}
}
如果blockNewEvents=true,代表该调度器目前正在进行关闭操作,因此,事件处理线程有必要在发现事件队列已经清空的情况下(drained=true),通过waitForDrained锁及时通知到服务关闭线程(waitForDrained.notify())。服务关闭线程收到该通知就能立刻从waitForDrained.wait()中直接唤醒,从而及时完成关闭操作,而不再进行不必要的wait操作。
注意,wait操作与sleep()不同,wait()操作虽然必须在synchronized代码