storm学习(四)——storm的并发和worker通讯机制

1、Storm程序的并发机制

1.1、概念

storm的并发即为在topology运行的时候存在多线程同时运行的效果,各个线程之间并发的效果可以根据需求在storm文件中配置。

如下图,supervisor和node是一一对应的关系,worker就是process进程,executor就是thread线程,task就是在spout或bolt中定义的函数。

storm学习(四)——storm的并发和worker通讯机制

那这些节点之间的对应关系为:

  1. Workers (JVMs):在一个物理节点上可以运行一个或多个独立的JVM 进程。一个Topology可以包含一个或多个worker(并行的跑在不同的物理机上), 所以worker process就是执行一个topology的子集,并且worker只能对应于一个topology 。
  2. Executors (threads):在一个worker JVM进程中运行着多个Java线程。一个executor线程可以执行一个或多个tasks。但一般默认每个executor只执行一个task。一个worker可以包含一个或多个executor, 每个component (spout或bolt)至少对应于一个executor, 所以可以说executor执行一个compenent的子集, 同时一个executor只能对应于一个component。 
  3. Tasks(bolt/spout instances):Task就是具体的处理逻辑对象,每一个Spout和Bolt会被当作很多task在整个集群里面执行。每一个task对应到一个线程,而stream grouping则是定义怎么从一堆task发射tuple到另外一堆task。你可以调用TopologyBuilder.setSpout和TopBuilder.setBolt来设置并行度 — 也就是有多少个task。 

1.2、配置并行度

1)对于并发度的配置,在storm里面可以在多个地方进行配置,优先级为:

defaults.yaml < storm.yaml < topology-specific configuration

< internal component-specific configuration < external component-specific configuration 

2)worker processes的数目:可以通过配置文件和代码中配置,worker就是执行进程,所以考虑并发的效果,数目至少应该大亍machines的数目 。

3)executor的数目:component的并发线程数,只能在代码中配置(通过setBolt和setSpout的参数), 例如, setBolt("green-bolt", new GreenBolt(), 2) 。

4)tasks的数目:可以不配置,默认和executor1:1,也可以通过setNumTasks()配置,

Topology的worker数通过config设置,即执行该topology的worker(java)进程数。它可以通过 storm rebalance 命令任意调整。 

 storm学习(四)——storm的并发和worker通讯机制

storm学习(四)——storm的并发和worker通讯机制

 3个组件的并发度加起来是10,就是说拓扑一共有10个executor,一共有2个worker,每个worker产生10 / 2 = 5条线程。

绿色的bolt配置成2个executor和4个task。为此每个executor为这个bolt运行2个task。

5)动态的改变并行度

Storm支持在不 restart topology 的情况下, 动态的改变(增减) worker processes 的数目和 executors 的数目, 称为rebalancing. 通过Storm web UI,或者通过storm rebalance命令实现: 

storm学习(四)——storm的并发和worker通讯机制

2、Storm通信机制

Worker间的通信经常需要通过网络跨节点进行,Storm使用ZeroMQ或Netty(0.9以后默认使用)作为进程间通信的消息框架。

Worker进程内部通信:不同workerthread通信使用LMAX Disruptor来完成。

    不同topologey之间的通信,Storm不负责,需要自己想办法实现,例如使用kafka等;

2.1、Worker进程间通信

worker进程间消息传递机制,消息的接收和处理的大概流程见下图

storm学习(四)——storm的并发和worker通讯机制

1)对于worker进程来说,为了管理流入和传出的消息,每个worker进程有一个独立的接收线程(一个worker进程运行一个专用的接收线程来负责将外部发送过来的消息移动到对应的executor线程的incoming-queue中,对配置的TCP端口supervisor.slots.ports进行监听);每个worker存在一个独立的发送线程(transfer-queue的大小由参数topology.transfer.buffer.size来设置,transfer-queue的每个元素实际上代表一个tuple的集合),它负责从worker的transfer-queue(transfer-queue的大小由参数topology.transfer.buffer.size来设置)中读取消息,并通过网络发送给其他worker。

对应的图形为:

spout的生命周期为:Open——nexttuple——WordcountBolt;

Bolt的生命周期为:prepare——execute——outputfield;

storm学习(四)——storm的并发和worker通讯机制

2)每个executor有自己的incoming-queue(executor的incoming-queue的大小用户可以自定义配置)和outgoing-queue(executor的outgoing-queue的大小用户可以自定义配置)。Worker接收线程将收到的消息通过task编号传递给对应的executor(一个或多个)的incoming-queues;

每个executor有单独的线程分别来处理spout/bolt的业务逻辑,业务逻辑输出的中间数据会存放在outgoing-queue中,当executor的outgoing-queue中的tuple达到一定的阀值,executor的发送线程将批量获取outgoing-queue中的tuple,并发送到transfer-queue中。

补充:

根据我们对storm的了解,worker1中的spout发送tuple的时候,不仅仅会发送给本worker的bolt,还会发送给worker2中的bolt,如果spout将数据发送给worker2中的bolt,同样的逻辑,他依然先将数据发送给worker2的accept大缓存,然后worker2根据tuple的类型,分别分发给worker2中的不同bolt,分发时会遵循一定的规则,accept大缓存接收到的数据存在一定的数据结构,而且每个数据包中存在了需要发往的bolt的taskid,accept大缓存接收到这些数据之后,worker2中的bolt根据taskid来获取到对应的值数据包,并进行executor处理,处理完成后,bolt将处理结果射到worker2中的transfer-queue那里,然后transfer-queue将这一数据汇总发送给其他的worker。

对应的输入队列transfer-queue的数据结构为:

storm学习(四)——storm的并发和worker通讯机制

3)每个worker进程控制一个或多个executor线程,用户可在代码中进行配置。其实就是我们在代码中设置的并发度个数。

2.2、Worker进程间通信分析

storm学习(四)——storm的并发和worker通讯机制

  1. 图中的received Thread接收其他worker中传过来的数据,并将数据传输给received-Queue Map,这个map中含有多个环状队列(Disruptor),received Thread中的数据会存储在received-queue map的环状队列中,另外的线程在Worker中,从环状队列中获取到数据,然后根据Tuple中包含的taskId,匹配到对应的executor;然后根据executor找到对应的incoming-queue,将数据存发送到incoming-queue队列中。
  2. 业务逻辑执行现成消费incoming-queue的数据,通过调用Bolt的execute(xxxx)方法,将Tuple作为参数传输给用户自定义的方法。
  3. 业务逻辑执行完毕之后,将计算的中间数据发送给outgoing-queue队列,当outgoing-queue中的tuple达到一定的阀值,executor的发送线程将批量获取outgoing-queue中的tuple,并发送到Worker的transfer-queue中。
  4. Worker发送线程消费transfer-queue中数据,计算Tuple的目的地,连接不同的node+port将数据通过网络传输的方式传送给另一个的Worker。
  5. 另一个worker执行以上步骤1的操作。

2.3、Worker进程间技术(Netty、ZeroMQ)

2.3.1、Netty

Netty是一个NIO client-server(客户端服务器)框架,使用Netty可以快速开发网络应用,例如服务器和客户端协议。Netty提供了一种新的方式来使开发网络应用程序,这种新的方式使得它很容易使用和有很强的扩展性。Netty的内部实现时很复杂的,但是Netty提供了简单易用的api从网络处理代码中解耦业务逻辑。Netty是完全基于NIO实现的,所以整个Netty都是异步的。

2.3.2、ZeroMQ

         ZeroMQ是一种基于消息队列的多线程网络库,其对套接字类型、连接处理、帧、甚至路由的底层细节进行抽象,提供跨越多种传输协议的套接字。ZeroMQ是网络通信中新的一层,介于应用层和传输层之间(按照TCP/IP划分),其是一个可伸缩层,可并行运行,分散在分布式系统间。

         ZeroMQ定位为:一个简单好用的传输层,像框架一样的一个socket library,他使得Socket编程更加简单、简洁和性能更高。是一个消息处理队列库,可在多个线程、内核和主机盒之间弹性伸缩。ZMQ的明确目标是“成为标准网络协议栈的一部分,之后进入Linux内核”。

2.4、Worker 内部通信技术(Disruptor)

worker内部通讯的结构为:

storm学习(四)——storm的并发和worker通讯机制

2.4.1、Disruptor的来历

1)一个公司的业务与技术的关系,一般可以分为三个阶段。第一个阶段就是跟着业务跑。第二个阶段是经历了几年的时间,才达到的驱动业务阶段。第三个阶段,技术引领业务的发展乃至企业的发展。所以我们在学习Disruptor这个技术时,不得不提LMAX这个机构,因为Disruptor这门技术就是由LMAX公司开发并开源的。

2)LMAX是在英国注册并受到FSA监管(监管号码为509778)的外汇黄金交易所。LMAX也是欧洲第一家也是唯一一家采用多边交易设施Multilateral Trading Facility(MTF)拥有交易所牌照和经纪商牌照的欧洲*金融公司

3)LAMX拥有最迅捷的交易平台,*技术支持。LMAX交易所使用“(MTF)分裂器Disruptor”技术,可以在极短时间内(一般在3百万秒之一内)处理订单,在一个线程里每秒处理6百万订单。所有订单均为撮合成交形式,无一例外。多边交易设施(MTF)曾经用来设计伦敦证券交易 所(london Stock Exchange)、德国证券及衍生工具交易所(Deutsche Borse)和欧洲证券交易所(Euronext)。

4)2011年LMAX凭借该技术获得了金融行业技术评选大赛的最佳交易系统奖和甲骨文“公爵杯”创新编程框架奖。

2.4.2、Disruptor是什么

Disruptor 是线程内通信框架,用于线程里共享数据。LMAX 创建Disruptor 作为可靠消息架构的一部分并将它设计成一种在不同组件*享数据非常快的方法。 基于 Mechanical Sympathy(对于计算机底层硬件的理解),基本的计算机科学以及领域驱动设计,Disruptor 已经发展成为一个帮助开发人员解决很多繁琐并发编程问题的框架。 很多架构都普遍使用一个队列共享线程间的数据(即传送消息)。

Disruptor主要是将队列变成环形缓冲区和添加了序号管理器,序号逐个递增处理,当有多个productor的时候,需要告诉环形缓冲区存储在哪几个空隙中,然后序号管理器存储哪些数据是空的,哪些是有数据的,每一个productor存储时都会做一个标注,等消费者消费,等完整消费后数据会继续存储数据进入:

storm学习(四)——storm的并发和worker通讯机制

图1 展示了一个在不同的阶段中通过使用队列来传送消息的例子(每个蓝色的圈代表一个线程)。

storm学习(四)——storm的并发和worker通讯机制

图 1

这种架构允许生产者线程(图1中的 stage 1)在 stage2 很忙以至于无法立刻处理的时候能够继续执行下一步操作,从而提供了解决系统中数据拥堵的方法。这里队列可以看成是不同线程之间的缓冲。

在这种最简单的情况下,Disruptor 可以用来代替队列作为在不同的线程传递消息的工具(如图 2 所示)。

storm学习(四)——storm的并发和worker通讯机制

图2

这种数据结构叫着 RingBuffer,是用数组实现的。Stage1 线程把数据放进 RingBuffer,而 Stage2 线程从 RingBuffer 中读取数据。

图2 中,可以看到 RingBuffer 中每格中都有序号,并且 RingBuffer 实时监测值最大(最新)的序号,该序号指向 RingBuffer 中最后一格。序号会伴随着越来越多的数据增加进 RingBuffer 中而增长。

Disruptor 的关键在于是它的设计目标是在框架内没有竞争.这是通过遵守single-writer 原则,即只有一块数据可以写入一个数据块中,而达到的。遵循这样的规则使得 Disruptor 避免了代价高昂的 CAS 锁,这也使得 Disruptor 非常快。

Disruptor 通过使用 RingBuffer 以及每个事件处理器(EventProcessor)监测各自的序号从而减少了竞争。这样,事件处理器只能更新自己所获得的序号。当介绍向 RingBuffer 读取和写入数据时会对这个概念作进一步阐述。

发布到 Disruptor

向 RingBuffer 写入数据需要通过两阶段提交(two-phase commit)。首先,Stage 1 线程即发布者必须确定 RingBuffer 中下一个可以插入的格,如图 3 所示。

storm学习(四)——storm的并发和worker通讯机制

图 3

RingBuffer 持有最近写入格的序号(图3中的18格),从而确定下一个插入格的序号。

RingBuffer 通过检查所有事件处理器正在从 RingBuffe 中读取的当前序号来判断下一个插入格是否空闲。

图 4 显示发现了下一个插入格。

storm学习(四)——storm的并发和worker通讯机制

图 4

当发布者得到下一个序号后,它可以获得该格中的对象,并可以对该对象进行任意操作。你可以把格想象成一个简单的可以写入任意值的容器。

同时,在发布者处理 19 格数据的时候,RingBuffer 的序号依然是 18,所以其他事件处理器将不会读到19格中的数据。

图5表示对象的改动保存进了 RingBuffer。

storm学习(四)——storm的并发和worker通讯机制

图5

最终,发布者最终将数据写入19 格后,通知 RingBuffer 发布 19 格的数据。这时,RingBuffer 更新序号并且所有从 RingBuffer 读数据的事件处理器都可以看到 19 格中的数据。

RingBuffer 中数据读取

Disruptor 框架中包含了可以从 RingBuffer 中读取数据的BatchEventProcessor,下面将概述它如何工作并着重介绍它的设计。

当发布者向 RingBuffer 请求下一个空格以便写入时,一个实际上并不真的从 RingBuffer 消费事件的事件处理器,将监控它处理的最新的序号并请求它所需要的下一个序号。

图5显示事件处理器等待下一个序号。

storm学习(四)——storm的并发和worker通讯机制

图6

事件处理器不是直接向 RingBuffer 请求序号,而是通过 SequenceBarrie r向 RingBuffer 请求序号。其中具体实现细节对我们的理解并不重要,但是下面可以看到这样做的目的很明显。

如图 6 中 Stage2 所示,事件处理器的最大序号是 16.它向 SequenceBarrier 调用 waitFor(17)以获得 17 格中的数据。因为没有数据写入 RingBuffer,Stage2 事件处理器挂起等待下一个序号。如果这样,没有什么可以处理。但是,如图6所示的情况,RingBuffer 已经被填充到 18 格,所以 waitFor 函数将返回18并通知事件处理器,它可以读取包括直到 18 格在内的数据,如图7所示。

storm学习(四)——storm的并发和worker通讯机制

图7

这种方法提供了非常好的批处理功能,可以在 BatchEventProcessor 源码中看到。源码中直接向 RingBuffer 批量获取从下一个序号直到最大可以获得的序号中的数据。

你可以通过实现 EventHandler 使用批处理功能。在 Disruptor 性能测试中有关于如何使用批处理的例子,例如 FizzBuzzEventHandler。

是低延迟队列?

当然,Disruptor 可以被当作低延迟队列来使用。我们对于 Disruptor 之前版本的测试数据显示了,运行在一个 2.2 GHz 的英特尔酷睿 i7-2720 QM 处理器上使用 Java 1.6.0_25 64位的 Ubuntu 的 11.04 三层管道模式架构中,Disruptor 比 ArrayBlockingQueue 快了多少。表1显示了在管道中的每跳延迟。有关此测试的更多详细信息,请参阅 Disruptor 技术文件。

但是不要根据延迟数据得出 Disruptor 只是一种解决某种特定性能问题的方案,因为它不是。

更酷的东西

一个有意思的事是 Disruptor 是如何支持系统组件之间的依赖关系,并在线程之间共享数据时不产生竞争。

Disruptor 在设计上遵守 single-writer 原则从而实现零竞争,即每个数据位只能被一个线程写入。但是,这不代表你不可以使用多个线程读数据,而这正是 Disrupto r所支持的。

Disruptor 系统的最初设计是为了支持需要按照特定的顺序发生的阶段性类似流水线事件,这种需求在企业应用系统开发中并不少见。图 8 显示了标准的 3 级流水线。

storm学习(四)——storm的并发和worker通讯机制

图 8

首先,每个事件都被写入硬盘(日志)作为日后恢复用。其次,这些事件被复制到备份服务器。只有在这两个阶段后,系统开始业务逻辑处理。

按顺序执行上次操作是一个合乎逻辑的方法,但是并不是最有效的方法。日志和复制操作可以同步执行,因为他们互相独立。但是业务逻辑必须在他们都执行完后才能执行。图9显示他们可以并行互不依赖。

storm学习(四)——storm的并发和worker通讯机制

图 9

如果使用 Disruptor,前两个阶段(日志和复制)可以直接从 RingBuffer中读取数据。正如图7种的简化图所示,他们都使用一个单一的Sequence Barrier从RingBuffer 获取下一个可用的序号。他们记录他们使用过的序号,这样他们知道那些事件已经读过并可以使用 BatchEventProcessor 批量获取事件。

业务逻辑同样可以从同一个 RingBuffer 中读取事件,但是只限于前两个阶段已经处理过事件。这是通过加入第二个 SequenceBarrier 实现的,用它来监控处理日志的事件处理器和复制的事件处理器,当请求最大可读的序号时,它返回两个处理器中较小的序号。

当每个事件处理器都使用 SequenceBarrier 来确定哪些事件可以安全的从 RingBuffer 中读出,那么就从中读出这些事件。

storm学习(四)——storm的并发和worker通讯机制

图10

有很多事件处理器都可以从 RingBuffer 中读取序号,包括日志事件处理器,复制事件处理器等,但是只有一个处理器可以增加序号。这保证了共享数据没有竞争。

如果有多个发布者?

Disruptor 也支持多个发布者向 RingBuffer 写入。当然,因为这样的话必然会发生两个不同的事件处理器写入同一格的情况,这样就会产生竞争。Disruptor 提供 ClaimStrategy 的处理方式应对有多个发布者的情况。

 2.4.2、Disruptor主要特点

1)没有竞争=没有锁=非常快。

2)所有访问者都记录自己的序号的实现方式,允许多个生产者与多个消费者共享相同的数据结构。

3)在每个对象中都能跟踪***(ring buffer,claim Strategy,生产者和消费者),加上神奇的cache line padding,就意味着没有为伪共享和非预期的竞争。

2.4.3、Disruptor 核心技术点

Disruptor可以看成一个事件监听或消息机制,在队列中一边生产者放入消息,另外一边消费者并行取出处理。

底层是单个数据结构:一个ring buffer。

每个生产者和消费者都有一个次序计算器,以显示当前缓冲工作方式。

每个生产者消费者能够操作自己的次序计数器的能够读取对方的计数器,生产者能够读取消费者的计算器确保其在没有锁的情况下是可写的。

核心组件:

  • Ring Buffer 环形的缓冲区,负责对通过 Disruptor 进行交换的数据(事件)进行存储和更新。
  • Sequence 通过顺序递增的序号来编号管理通过其进行交换的数据(事件),对数据(事件)的处理过程总是沿着序号逐个递增处理。
  • RingBuffer底层是个数组,次序计算器是一个64bit long 整数型,平滑增长。

storm学习(四)——storm的并发和worker通讯机制

1)接受数据并写入到脚标31的位置,之后会沿着序号一直写入,但是不会绕过消费者所在的脚标。

2)Joumaler和replicator同时读到24的位置,他们可以批量读取数据到30。

3)消费逻辑线程读到了14的位置,但是没法继续读下去,因为他的sequence暂停在15的位置上,需要等到他的sequence给他序号。如果sequence能正常工作,就能读取到30的数据。