转载自https://www.cnblogs.com/Jack47/p/understanding-storm-internal-message-passing.html
一个Storm拓扑,就是一个复杂的多阶段的流式计算。Storm中的组件(Component)就是对各个阶段的一个抽象,其中的Spout是生产者的角色,它负责源源不断地从Storm外部接收消息,扔给下游的组件处理,下游组件处理完成后,最终输出到外部的存储系统。
本文主要讲解消息在Storm内部的各个组件(Component)之间如何进行传递,本文适用于JStorm 2.1.0以后的版本。对于JStorm各个版本的改进,见这里。
如果读者对Storm的基本组成部分如Spout,Bolt,Worker进程和Task不了解,可以先看下 Storm介绍(一),Storm介绍(二)和“理解Storm拓扑并发”。
下文中使用的"消息"(Messagesage)和“元组"(Tuple)两个词语其实是同一个意思。
Storm中的每个组件,可以用一个三元组来定义
<inputStreamId, boltImpl, outputStreamInfo>
其中inputStreamId定义了这个组件所消费的流的ID; boltImpl是bolt的具体实现的类;outputStreamInfo代表这个组件的输出流的信息,包含两部分:输出流的ID和路由方式。所以当一个组件发射一个消息后,通过这个流的分组策略(Grouping)就可以立马计算出消费它的taskId。
拿下面中的拓扑为例,从图一看到这个拓扑由三个组件构成:蓝色的Spout,消费方是Green Bolt,Green Bolt的消费方是Yellow Bolt。这个拓扑在集群上的运行时的状态如图二所示:
它由两个Worker进程组成,每个Work里面运行4个Task。仔细的读者已经发现了这里没有Executor,这是JStorm和Storm很大的不同。JStorm认为Executor的存在收益比太低,虽然它支持不停机动态扩大Task的数量,但同时增加了理解成本,增加了应用开发人员编程的复杂度,所以JStorm中去掉了Executor。
->
<-
图一 一个实际拓扑组件的构成
->
<-
图二 集群上运行的一个实际拓扑
在Storm拓扑内部,同一个拓扑的多个Worker之间会发生消息传递,比如上图二中的两个Worker进程,他们之间的通信就是进程间的通信了,发送的消息需要经过序列化和反序列化。Storm中,Worker之间使用Netty进行网络通信。
在Storm拓扑的一个Worker进程内部,多个Task之间也会进行通信。比如上图二中的Task 6和Task 3。Storm中Worker进程内部的消息通信依赖于LMAX Disruptor这个高性能线程间通信的消息通信库。
Storm内部的消息传递
JStorm与Storm在内部消息传递机制上的主要差别:
JStorm中独立出一个线程来专门负责消息的反序列化,这样执行线程单独执行,而不是Storm那样,一个线程负责执行反序列化并执行用户的逻辑。相当于是把流水线拆解的更小了。这样对于反序列化时延跟执行时延在同一个数量级的应用性能提升比较明显。
图三:JStorm内部消息队列的概要图
从图里看到队列都是绿色的,这些队列都是某个Worker内部的队列。为了可读性只保留了一个Worker进程(一个storm节点一般都运行多个Worker),而且在这个Worker进程里只画了一个Task,(再次的,在一个Worker进程里通常有多个Task)
详细解释
每个Worker进程有一个NettyServer,它监听在Worker的TCP端口上(通过 supervisor.slots.ports
来配置),其他需要跟它通信的Worker会作为NettyClient分别建立连接。当NettyServer接收到消息,会根据taskId参数把消息放到对应的反序列化队列(DeserializedQueue)里面。 topology.executor.receive.buffer.size
决定了反序列化队列的大小。TaskReceiver中的反序列化线程专门负责消费反序列化队列中的消息:先将消息反序列化,然后放到执行队列(Execute Queue)中去。
执行队列的消费者是BoltExecutor线程,它负责从队列中取出消息,执行用户的代码逻辑。执行完用户的代码逻辑后,最终通过OutputCollect输出消息,此时消息里已经生成了目标task的taskId。topology.executor.receive.buffer.size
决定了执行队列的大小。可以看到JStorm中执行队列跟反序列化队列的大小是同一个配置项,即它们是一致的。
仔细看了图三的同学会发现执行队列的生产者除了TaskReceiver外,还有一个。这种消息的来源就是Worker内部的其他Task的TaskTransfer。
输出的消息通过TaskTransfer来发送,如果目标Task是Worker内部的Task,就直接扔到目标Task的执行队列中去。如果目标Task是在其他Worker上,那就先放到序列化队列中,然后由单独的一个线程专门负责序列化,然后通过NettyClient发送出去。topology.executor.send.buffer.size
决定了序列化队列的大小。
每个Worker进程有多个NettyClient,他们负责与其他的Worker进行网络通信。
延伸阅读
如何配置Storm的内部消息缓存
上面提到的众多配置项都在conf/defaults.yaml里有定义。可以通过在Storm集群的conf/storm.yaml
里进行配置来全局的覆值。也可以通过Storm的Java API backtype.storm.Config 来对单个的Storm拓扑进行配置。
如何配置Storm的并发
Storm消息缓存的正确配置不但是和你的拓扑的负载类型紧密关联的,而且和拓扑的并发度有很大关系。后者的详细信息见理解Storm并发一文。
了解Storm拓扑上发生了什么?
Storm UI是一个用于观察你正在运行的拓扑的关键指标的良好开端。例如,它可以给你展示所谓的Spout/Bolt的“容量”。众多的指标可以帮助你决定对本文中提到的众多缓存相关的配置参数的修改,对你的拓扑的运行效率的影响是正向的还是负向的。更多信息见"运行一个多节点Storm集群"一文。
除此之外还可以注册应用程序自己的指标并使用类似Graphite这类工具来跟踪它们。详细信息见“从Storm发送指标到Graphite"和“通过RPM和Supervisord来安装和运行Graphite"。
性能调优方面的建议
可以看Storm主要作者Nathan Marz的演讲:调优和上线Storm拓扑
看看其他没有提到的参数:
topology.spout.max.batch.size
数据容量(data volume),数据速度(velocity),消息的大小,处理一个消息的计算复杂度。所以需要不断调整才能找到最佳的参数,没有银弹。
参考资料:
Understanding storm internal message buffers