Storm源码结构 (来源Storm Github Wiki)

写在前面

本文译自Storm Github Wiki: Structure of the codebase,有助于深入了解Storm的设计和源码学习。本人也是参照这个进行学习的,觉得在理解Storm设计的过程中起到了重要作用,所以也帖一份放在自己博客里。以下的模块分析里没有包括Storm 0.9.0增加的Netty模块,对应的代码包在Storm Github下的storm-netty文件夹内,内容比较简单,关于这块的release note可以参考Storm 0.9.0 Released Netty Transport,这里有一篇Storm 的新消息传输机制也可以参考(这篇文章的博客里有不少分析Storm的文章,博主本人貌似是Storm的Committer,好像在阿里工作)。此外,在Storm Github Wiki Pages里也可以看到不少需要的基础内容。


结构层次

Storm的源码共分为三个不同的层次。
首先,Storm在设计之初就考虑到了兼容多语言开发。Nimbus是一个thrift服务,topologies被定义为Thrift结构体。Thrift的运用使得Storm可以被任意开发语言使用。
其次,Storm的所有接口都是Java语言来定义的。因此,尽管Storm中的很多功能实现都是Clojure代码(说实话,第一次看Clojure代码的时候,第一感觉是这乱七八糟的都是些什么啊!那么多的括号又是什么节奏!),但是使用这些功能都必须通过Java API。这意味着Storm的所有特性对于Java来讲都是可用的。
第三,Storm的很大一部分实现都是Clojure代码。从代码行来看,差不多是一半Java代码,一半Clojure代码。但是由于Clojure在表达能力上更为见长,因此,实际上绝大多数逻辑的实现都是Clojure来做的。
接下来的小节里将会逐个详细解释这三个层次。


Storm.thrift

要理解Storm的代码结构,首先需要看的是 storm.thrift文件。(在storm-core/src下)

Storm使用了从 这里folk出来的Thrift版本来自动生成代码。这个Thrift版本实际上是将所有的Java packages都重命名为"org.apache.thrift7"之后的Thrift 7。除此之外,它与Thrfit 7是完全一样的。之所以单独出这样一个Thrift版本一是考虑到Thrift缺少向后兼容,而是为了避免包名冲突以满足一些用户在他们自己的topologies中用到其他版本的thrift。

一个topology中的任何一个spout或bolt都会被用户指定一个唯一标识,称为"component id"。当描述1个bolt接收其他哪些spout或bolt的输出时需要用到这个"component id"。 StormTopology结构中保存了1个map来保存"component id"到"component"的映射关系,这个映射关系包含所有的component类型(即所有的spout、bolt)。

Thrift对Spout或bolt的定义是相同的,因此我们只需要看一下 bolt的thrift定义。它包含了1个"ComponentObject"结构和1个"ComponentCommon"结构。
[html] view plain copy
  1. union ComponentObject {  
  2.   1: binary serialized_java;  
  3.   2: ShellComponent shell;  
  4.   3: JavaObject java_object;  
  5. }  
"ComponentObject"即是bolt的实现实体。它可以是以下三个类型之一:
  • 1个序列化的java对象(这个对象实现IBolt接口)
  • 1个"ShellComponent"对象,意味着bolt是由其他语言实现的。如果以这种方式来定义1个bolt,Storm将会实例化1个ShellBolt对象来负责处理基于JVM的worker进程与非JVM的component(即该bolt)实现体之间的通讯。
  • 1个"JavaObject"结构,这个结构告诉Storm实例化这个bolt所需要的classname和构造函数参数。这一点在你想用非JVM语言来定义topology时比较有用。这样,在你使用非JVM语言来定义topology时就可以做到既使用基于JVM的spout或bolt,同时又不需要创建并序列化它们的Java对象。
[html] view plain copy
  1. struct ComponentCommon {  
  2.   1: required map<GlobalStreamId, Grouping> inputs;  
  3.   2: required map<string, StreamInfo> streams; //key is stream id  
  4.   3: optional i32 parallelism_hint; //how many threads across the cluster should be dedicated to this component  
  5.   
  6.   // component specific configuration respects:  
  7.   // topology.debug: false  
  8.   // topology.max.task.parallelism: null // can replace isDistributed with this  
  9.   // topology.max.spout.pending: null  
  10.   // topology.kryo.register // this is the only additive one  
  11.     
  12.   // component specific configuration  
  13.   4: optional string json_conf;  
  14. }  
"ComponentCommon"定义了这个component的其他所有属性。包括:
  • 这个component发射什么stream以及stream的元数据(是否是direct stream,stream中field的声明)
  • 这个component接收什么stream(被定义在1个component_id到stream_id的map里,在stream做分组时用到)
  • 这个component的并行度
  • 这个component的配置项configuration
注意,在spout的结构中同样有"ComponentCommon"字段,因此,spout也是可以被声明接收其他的stream输入。然而,Storm Java API并没有提供一种方式指定spout接收什么stream,同时如果你在这里指定1个spout的输入声明,在提交这个topology时将会出现报错信息。之所以这样设计,是因为spout的输入声明不是让用户自己来使用的,而是Storm内部使用的。Storm会在内部自动向topology添加stream和bolt来构造 acking framework,其中的两个stream就是从acker bolt发出给topology中的所有spout节点的。只要1个tuple树被检测到完成了或失败了,acker就会通过这两个stream分别发出"ack"或"fail"消息。将用户提交的topology转换成运行时的topology的代码可参见 这里。

Java接口

Storm的接口定义都是Java接口。主要的接口如下:
  • IRichBolt
  • IRichSpout
  • TopologyBuilder
这样定义这些接口的主要意图在于:
  • 以Java语言来定义接口
  • 基于此接口,可以做到在不同的场合,提供出各自最适合的默认实现基类
这一策略的实际运用可以参考 BaseRichSpout类

Spout和bolt就是按照以上接口描述的方式被序列化到topology的Thrift定义结构中。

值得一提的一个细节是,IBolt、ISpout与IRichBolt、IRichSpout这两对接口是有区别的。它们主要区别是在"Rich"版本里增加了"declareOutputFields"方法。这样设计的原因是所有的输出stream的输出field声明都必须是在Thrift结构里的(这样就可以做到使用任何编程语言来声明了),但是用户又希望能够在自己的class中来声明stream输出field信息。为解决这个问题,"TopologyBuilder"在构造Thrift结构时就是通过调用"declareOutputFields"方法来得到输出field的声明,然后将其转换纳入Thrift结构。这个转换操作可以从"TopologyBuilder"代码中的 这一段里看到。

接口实现

通过将Storm所有的接口都由Java语言来定义确保了Storm的所有功能对于Java来讲都是可使用的。同时,Java接口的使用也使得Java用户在使用Storm时体验更好。

应该说,Storm主要是由Clojure语言实现的。尽管从代码行数上看一半是Java一半是Clojure,但其实里面绝大多数的逻辑实现都是Clojure。有两个值得一提的例外就是 DRPC和 支持事务的topology,它们二者都纯Java实现的。这样做的主要目的是来展示如何基于Storm,实现Storm之上更高层次的抽象。DRPC和支持事务的topology的实现分别位于 backtype.storm.coordination和 backtype.storm.transactional包里。

这里总结了一份主要的Java包和Clojure命名空间的内容列表:

Java

backtype.storm.coordination: 实现了DRPC和事务性topology里用到的基于Storm的批处理功能。这个包里最重要得类是CoordinatedBolt
backtype.storm.drpc: DRPC的更高层次抽象的具体实现
backtype.storm.generated: 自动生成的Thrift代码(利用 这里folk出来的Thrift版本生成的,主要是把org.apache.thrift包重命名成org.apache.thrift7来避免与其他Thrift版本的冲突)
backtype.storm.grouping: 包含了用户实现自定义stream分组类时需要用到的接口
backtype.storm.hooks: 定义了处理storm各种事件的钩子接口,例如当task发射tuple时、当tuple被ack时。关于钩子的手册详见 这里
backtype.storm.serialization: storm序列化/反序列化tuple的实现。在 Kryo之上构建。
backtype.storm.spout: spout及相关接口的定义(例如"SpoutOutputCollector")。也包括了"ShellSpout"来实现非JVM语言定义spout的协议。
backtype.storm.task: bolt及相关接口的定义(例如"OutputCollector")。也包括了"ShellBolt"来实现非JVM语言定义bolt的协议。最后,"TopologyContext"也是在这里定义的,用来在运行时供spout和bolt使用以获取topology的执行信息。
backtype.storm.testing: 包括了storm单元测试中用到的各种测试bolt及工具。
backtype.storm.topology: 在Thrift结构之上的Java层,用以提供一个纯Java API来使用Storm(用户不需要了解Thrift的细节)。"TopologyBuilder"及不同spout和bolt的基类们也在这里定义。稍高一层次的接口"IBasicBolt"也在这里定义,它会使得创建某些特定类型的bolt会更加简洁。
backtype.storm.transactional: 包括了事务性topology的实现。
backtype.storm.tuple: 包括Storm中tuple数据模型的实现。
backtype.storm.utils: 包含了Storm源码中用到的数据结构及各种工具类。


Clojure

backtype.storm.bootstrap: 包括了1个有用的宏来引入源码中用到的所有类及命名空间。
backtype.storm.clojure: 包括了利用Clojure为Storm定义的特定领域语言(DSL)。
backtype.storm.cluster: Storm守护进程中用到的Zookeeper逻辑都封装在这个文件中。这部分代码提供了API来将整个集群的运行状态映射到Zookeeper的"文件系统"上(例如哪里运行着怎样的task,每个task运行的是哪个spout/bolt)。
backtype.storm.command.*: 这些命名空间包括了各种"storm xxx"开头的客户端命令行的命令实现。这些实现都很简短。
backtype.storm.config: Clojure中config的读取/解析实现。同时也包括了工具函数来告诉nimbus、supervisor等守护进程在各种情况下应该使用哪些本地目录。例如:"master-inbox"函数会返回本地目录告诉Nimbus应该将上传给它的jar包保存到哪里。
backtype.storm.daemon.acker: "acker" bolt的实现。这是Storm确保数据被完全处理的关键组成部分。
backtype.storm.daemon.common: Storm守护进程用到的公共函数,例如根据topology的名字获取其id,将1个用户定义的topology映射到真正运行的topology(真正运行的topology是在用户定义的topology基础上添加了ack stream及acker bolt,参见system-topology!函数),同时包括了各种心跳及Storm中其他数据结构的定义。
backtype.storm.daemon.drpc: 包括了DRPC服务器的实现,用来与DRPC topology一起使用。
backtype.storm.daemon.nimbus: 包括了Nimbus的实现。
backtype.storm.daemon.supervisor: 包括了Supervisor的实现。
backtype.storm.daemon.task: 包括了spout或bolt的task实例实现。包括了处理消息路由、序列化、为UI提供的统计集合及spout、bolt执行动作的实现。
backtype.storm.daemon.worker: 包括了worker进程(1个worker包含很多的task)的实现。包括了消息传输和task启动的实现。
backtype.storm.event: 包括了1个简单的异步函数的执行器。Nimbus和Supervisor很多场合都用到了异步函数执行器来避免资源竞争。
backtype.storm.log: 定义了用来输出log信息给log4j的函数。
backtype.storm.messaging.*: 定义了1个高一层次的接口来实现点对点的消息通讯。工作在本地模式时Storm会使用内存中的Java队列来模拟消息传递。工作在集群模式时,消息传递使用的是ZeroMQ。通用的接口在protocol.clj中定义。
backtype.storm.stats: 实现了向Zookeeper中写入UI使用的统计信息时如何进行汇总。实现了不同粒度的聚合。
backtype.storm.testing: 包括了测试Storm topology的工具。包括时间仿真,运行一组固定数量的tuple然后获得输出快照的"complete-topology","tracker topology"可以在集群"空闲"时做更细粒度的控制操作,以及其他工具。
backtype.storm.thrift: 包括了自动生成的Thrift API的Clojure封装以使得使用Thrift结构更加便利。
backtype.storm.timer: 实现了1个后台定时器来延迟执行函数或者定时轮询执行。Storm不能使用Java里的Timer类,因为为了单测Nimbus和Supervisor,必须要与时间仿真集成起来使用。
backtype.storm.ui.*: Storm UI的实现。完全独立于其他的代码,通过Nimbus的Thrift API来获取需要的数据。
backtype.storm.util: 包括了Storm代码中用到的通用工具函数。
backtype.storm.zookeeper: 包括了Clojure对Zookeeper API的封装,同时也提供了一些高一层次的操作例如:"mkdirs"、"delete-recursive"

你可能感兴趣的:(Storm源码结构 (来源Storm Github Wiki))