YARN源码解析(1)-设计风格

Posted by AlstonWilliams on February 17, 2019

在阅读Hadoop的源码时,看到了一种非常好的设计风格.

今年,我打算设计一个分布式键值系统.在刚开始思考如何编码时,就感到非常头疼.之前阅读过Zookeeper的源码,而我感觉并没有从Zookeeper的编码风格中学到什么.实际上,我感觉他的设计非常混乱.在设计上,唯一一点我还清晰记得的就是在请求处理的时候,采用了类似过滤器的调用链的这样一种方法,感觉蛮不错的.

在没有阅读Hadoop的源码之前,我想在我的代码中,采取类似总线的这样一种模式来编码,即把不同的组件封装好,比如WAL组件,存储状态信息的组件,一致性算法组件等,然后通过一个总线,即一个作为总线的方法,进行不同组件之间的交互.

而这样实际上还是有问题的,即通过这种方法,看起来可能各个模块之间是解耦合的.然而,实际上并不是,因为不同的模块要相互调用对方模块的方法.

而我实际上最头疼的就是这种非常混乱的架构.之前睡觉的时候,不少次曾经因为梦到结构混乱,而烦的我睡不踏实.

这又是我第一次设计一个分布式系统,而且刚好刚到公司实习,研究大数据,对Hadoop有点感兴趣,因为它是一个分布式调度系统,以及一个分布式存储系统,刚好我也在研究这种分布式系统.于是就暂时停止了这个项目,先阅读Hadoop的源码了.

在阅读Hadoop的源码时,看到它的设计风格,我感到非常惊喜,因为这种设计风格正是我想要的.

于是,对于该如何编写那个分布式键值系统,也明白了许多.

那么,在这篇文章中,我会介绍Hadoop中的三种比较好的编码风格:

  • 事件驱动
  • 状态机
  • 细粒度的划分模块

事件驱动

在上面,我提到,不同模块之间的相互调用让我很痛苦.

那么如何解决不同模块之间的相互调用呢?有一种方式,是把它放到消息队列中,每个模块都监控这个消息队列中是否有需要自己处理的消息,如果有的话,就进行相应的操作.

除了这种方式,还有一种更加简单的方式,就是采取总线模式.

这里的总线模式,跟上面我提到的总线不一样.它是这样实现的,有一个事件转发器作为总线,然后不同的模块,将它感兴趣的事件,以及它自己注册到这个事件转发器中.

这样,一个模块如果想要给另一个模块发送数据,那么只需要给这个事件转发器发送一条对应类型的消息.

这种方式,跟Linux中的Select模型其实很像.

这样就能充分解耦不同模块.

它跟消息队列方式的不同之处在于,在消息队列的方式中,每个模块都需要启动一个线程,来轮询那个消息队列,这样可能会浪费一些CPU资源,而且启动的线程更多,上下文切换的几率就更大.而在这种总线的模式中,不需要模块启动专门的线程来轮询有没有它感兴趣的事件,当然,如果是需要大量时间来处理的事件,为了避免阻塞,你开启一个线程来处理也是可以的,而是由事件转发器直接调用这个它的那个处理事件的方法.

这种总线模式,需要各个模块都实现一个特定的接口,其中包含处理事件的方法,这样事件转发器才能统一进行调用.

在Hadoop中,这个接口是EventHandler.

状态机

对于这个词汇,只要稍微了解编译原理的朋友,都会非常清楚.

上面这张图是我从Wikipedia上面摘过来的.

从状态机的名称中,我们就可以看到,是在各种状态之间进行转换的.

在Hadoop中,好多组件,都是有好几种状态的,比如一个Container,一个Application,都是有好几种状态.

这里我们摘取Container的实现来看:

我们可以看到,在这个状态机的实现中,有好多种状态,并且每一种状态都会通过一个事件转换到另一个状态,并且会触发一个Transition这样一种动作.

有没有感觉这种方式非常优雅?

相对于我们手动处理事件,然后根据不同的事件,进行状态转换,并进行其他的操作,这种方式,不仅代码对于状态之间的转换一目了然,而且代码的编写也清晰了很多.

其实之前研究一致性算法的时候,是有去研究状态机的,但是却没有想到可以把它用到编程中.非常惭愧.

细粒度的划分模块

在我计划那个分布式键值系统的设计时,模块划分的粒度有一些粗.

而看Hadoop中的ResourceManager和NodeManager的实现,可以看到它划分的非常细.

就拿ResourceManager来说,它就有一个专门的Service,来收集NodeManager发送来的状态信息,来更新可用资源,也会有专门的Service,来处理客户端的请求.各个Service都只做一件事情,彼此的职责非常清晰.

总结

从Hadoop的编码风格上,就学到了这么三点.其实正是这三点,让Hadoop的源码非常清晰,直接明了.而不是跟一团乱麻一样.

在我们的编码中,这些风格,这些方式,也会非常有用的.