Storm
原理:
Storm是一个分布式的、高容错的实时计算系统。
Storm对于实时计算的的意义相当于Hadoop对于批处理的意义。Hadoop为我们提供了Map和Reduce原语,使我们对数据进行批处理变的非常的简单和优美。同样,Storm也对数据的实时计算提供了简单Spout和Bolt原语。
Storm适用的场景:
1、流数据处理:Storm可以用来用来处理源源不断的消息,并将处理之后的结果保存到持久化介质中。
2、分布式RPC:由于Storm的处理组件都是分布式的,而且处理延迟都极低,所以可以Storm可以做为一个通用的分布式RPC框架来使用。
基本概念:
1 元组(tuple)
元组是Storm中消息传输的基本单元,是一个命名的值列表(List)。
元组支持所有基本类型、字符串、字节数组作为字段的值,只要实现类型的序列化接口就可以使用该类型的对象。
元组本来应该是一个Key-value的Map,但是由于组件之间传递的元组的字段名称已经事先定义好,所以只需要按照顺序,将值填入List即可。
2 流(Stream)
流是一个无序的元组序列。每个流被声明后都会赋予一个id,用于唯一识别这个流。
由于单个流的Spout和Bolt很多(Spout和Bolt都可以声明输出多个流,但是情况很少),因此,使用OutputFieldsDeclarer可以声明一个不指定id的流,这个时候,流被赋予了一个默认的id值(64位)。
3 喷口(Spout)
Spout是Storm的数据来源,一般Spout会从消息队列中读取数据。
Spout可以是可靠的,也可以是不可靠的。可靠的Spout会重发处理失败的元组,而不可靠的Spout选择遗忘。
nextTuple()是Spout的核心函数,Storm框架会不断调用这个函数,从外部读取tuple。
当一个元组被完全处理后,Storm调用Spout的ack()函数,否则调用fail()函数。
IRichSpout是Spout必须要实验的一个接口。
4 螺栓(Bolt)
Topology中的所有业务处理,都在Bolt中完成,比如数据的过滤、业务处理、存储等。
Bolt的关键函数是execute(),这个函数的主要功能是处理接收到的数据,发射0个或者多个基于当前元组的元组,然后应答输入元组(ack)。
5 拓扑(Topology)
拓扑就是Storm中真正在运行的程序,节点表示Spout或Bolt,完成数据处理;边表示数据的流动方式。
6 主控节点和工作节点
Storm集群中节点有两类:主控节点和工作节点。其中主控节点1个,工作节点多个。
主控节点上运行了一个称为Numbus的守护进程,负责在集群中分发代码,对节点分配任务,并监视主机故障。
工作节点上运行了一个称为Supervisor的守护进程,负责监听其主机上分配的作业,启动和停止已经分配的工作进程。
7 流分组(Stream grouping)
是拓扑定义的一部分,负责确定每个Bolt应该从哪个拓扑节点获取数据。
Storm内置7中流分组方式,此外,也可以通过实现CustomStreamGrouping接口,设计自定义的流分组方式。
8 工作进程(Worker)
worker在物理上,是一个JVM和拓扑中任务子集。
每一台服务器可以部署一个或者多个worker进程,每个 worker进程执行一个Topology中的部分任务。
9 任务(task)
worker中的每一个Spout或者Bolt线程(相当于Topology中的一个节点),称为一个任务。
Storm集群中,每个Spout或者Bolt执行很多任务,可以通过设置拓扑中每个Spout或Bolt的任务数(setNumTasks()),调整他们执行的任务数。
10 执行器(Executor)
Storm0.8之后,task不再和线程对应,多个task可以共享一个线程。
所以一台服务器可以部署多个worker进程;每个worker进程中可以有多个Executor(对应一个线程);每个Executor中执行一个或者多个task。
Executor的个数,可以通过setSpout或者SetBolt中的并行度参数进行调整。
11 可靠性
Storm可以保证每一个Spout元组被完全处理,它跟踪每个Spout元组的元组树,当元组树中创建元组节点或者完成元组节点时,对应的组件(Spout或Bolt)会通知Storm。
当一个Spout元组没有在“消息超时时间”内完成时,Spout认为元组处理失败,并重发元组。
一个Storm集群的基本组件
storm的集群表面上看和hadoop的集群非常像。但是在Hadoop上面你运行的是MapReduce的Job, 而在Storm上面你运行的是Topology。它们是非常不一样的—一个关键的区别是:一个MapReduce Job最终会结束,而一个Topology运永远运行(除非你显式的杀掉他)。
在Storm的集群里面有两种节点:控制节点(master node)和工作节点(worker node)。控制节点上面运行一个后台程序:Nimbus,它的作用类似Hadoop里面的JobTracker。Nimbus负责在集群里面分布代码,分配工作给机器,并且监控状态。
每一个工作节点上面运行一个叫做Supervisor的节点(类似 TaskTracker)。Supervisor会监听分配给它那台机器的工作,根据需要启动/关闭工作进程。每一个工作进程执行一个Topology(类似 Job)的一个子集;一个运行的Topology由运行在很多机器上的很多工作进程 Worker(类似 Child)组成。
Storm VS MapReduce
Nimbus和Supervisor之间的所有协调工作都是通过一个Zookeeper集群来完成。并且,nimbus进程和supervisor都是快速失败(fail-fast)和无状态的。所有的状态要么在Zookeeper里面,要么在本地磁盘上。这也就意味着你可以用kill -9来杀死nimbus和supervisor进程,然后再重启它们,它们可以继续工作,就好像什么都没有发生过似的。这个设计使得storm不可思议的稳定。
序列化(Serialization)
1 元组中可以包含任意的对象,所以,元组在任务之间传递时,需要有序列化和反序列化的过程。
2 Storm使用Kryo序列化,Kryo是一个灵活快速的序列化库。
3 除了使用Kryo,Storm中还可以使用自定义序列化(需要注册)和Java序列化(消耗大量资源)
4 动态类型
Storm将对象放置到字段时,并没有声明字段的类型。Strom会动态的找出字段类型并将其序列化。
使用动态类型的原因:
(1)简化API,动态类型简答且易用。
(2)Bolt方法中的元组可能来自任意流,所以有不同的类型组合。
(3)Storm可以被动态语音更直接的使用(Clojure和Jruby)
容错机制
1 Worker进程死亡
Worker死亡时,对应节点的Supervisor会尝试重启Worker。
如果连续重启失败一定次数,无法发送心跳信息到Nimbus,Nimbus会在另一台主机上重新分配Worker.
2 节点死亡
Nimbus会将当前节点的任务分配给其他节点的主机。
3 Nimbus或Supervisor死亡
Nimbus和Supervisor被设计成快速失败(出现故障时,进程自动毁灭)和无状态的(所有状态保存在Zookeeper或者磁盘上)。
Nimbus和Supervisor应该配合Daemontool或者monit工具监控运行,当他们死亡了,可以马上重启。
由于他们是无状态的,所以重启后会向什么都没发生一样继续工作,并且不影响Worker进程的工作。
4 Nimbus的“单点故障”
虽然失去Nimbus节点,worker节点可以正常工作,但是当worker或者节点死亡后,他们的任务无法被安排到其他节点上。
可靠性机制(保证消息完整处理)
1 消息被“完全处理”的含义
对于任意一个Spout发出的tuple,经过Topology中Bolt的一系列运算,都会形成一个“元组树”。
当这个树创建完成(Anchor锚定),并且树中的每一个元素都已经被处理(ack),那么Storm认为这个Spout的tuple被“完全处理”。
如果在超时时间内(默认30s),元组树中的元组没有被处理完,认为这个Spout的tuple处理失败。
2 Spout的处理过程
(1)Storm调用Spout的nextTuple方法从Spout请求一个元组。
(2)Spout调用open方法,发射一个元组到输出流,并给这个元组提供一个唯一的id。
(3)元组被发送到Bolt,Storm负责跟踪元组的链接已经处理状态。
(4)如果处理成功,Storm调用ack方法,并将Spout的tuple的id传递给ack函数;否则调用fail方法,同样传递id。
3 Storm如何保证可靠性
想要保证可靠性,需要保证两点:一是当在元组树上创建一个新链接时,需要告诉Storm;二是,当一个元组处理完成时,告诉Storm。
(1)保证第一条
建立链接的过程其实就是Bolt发射元组的过程。Bolt通过“锚定”来通知Storm,这里建立的新的数据链接。
具体的方法是,Bolt调用emit方法时,将输入tuple作为emit方法的第一个参数,这样,后续发送出去的tuple就都锚定在了输入tuple上,形成了一个元组树。只有后续的元组都被ack,这个输入元组才被认为是ack的。
一个输出元组也可以锚定多个输入元组,称为“复合锚定”,输出参数时多个输入元组组成的List。这样当下游元组未被成功处理时,将会触发Spout的多个元组的重发。
其实emit也可以不把输入tuple作为第一个参数(即不使用锚定),这个时候,如果下游的节点没有被处理,Storm的机制将无法将对应的Spout元组重发。这取决于用户需要的可靠性级别。因为不使用锚定可以提高一些效率。
(2)保证第二条
当完成元组树中的单个元组操作时,需要调用OutputCollector类的ack或者fail方法,通知Storm。
fail方法会使对应的Spout元组立即失败,这样Spout就不需要等到超时时间重发,速度会更快。
ack方法通知Storm,当前组件的输入tuple处理成功。
需要注意的是,每一个元组必须执行ack或者fail方法,Storm使用内存追踪每个元组,付过不返回ack/fail,那么最终会导致内存耗尽。
4 Storm如何实现可靠性
Storm的可靠性,主要依赖一组Acker任务。对于每一个Spout元组,Acker任务跟踪元组的有向图。
Storm使用哈希取模的方式,将Spout元组的id(64位id)映射到一个Acker任务,对应的Acker任务就负责跟踪这个Spout元组。
当Spout的元组发送到Bolt中时,Spout的id会复制到Bolt产生的新的tuple中,同理,Bolt向下游Bolt转发tuple时,同样会把这个Spout元组的id复制到新的Tuple中。这样,一系列的tuple都可以根据Spout的id,找到他们对应的Acker任务。
在进行计算时,当一个组件ack时,它发送给Acker一个消息,告诉Acker,当前的tuple已经处理完成,后面又锚定了新的tuple。这样,Acker任务就可以实时的追踪Spout元组的元组树以及元组树中元祖的完成情况。
Acker任务默认只有一个,当拓扑规模比较大时,可以通过配置,让storm产生多个Acker任务。
5 Acker的跟踪算法
Storm中有大量的节点,如果Acker显示的跟踪Spout元组的所有后续节点,那将导致内存溢出。
事实上,Acker只存储来自一个Spout元组的一对key-value的map值,key是创建Spout元组的任务的id,value是一个64位的值,称为ack val,它是所以在元组树上创建的元组以及ack的元组的id的异或值(XOR),如果这个值为0,表示这个Spout元组已经被处理成功。
上面的算法有一个问题,由于Spout元组的id是随机生成的,如果元组生成的id就是0,那么将无法判断是否真的完成。但是这种情况的概率非常小,可以忽略不计。
6 Storm面对失败案例如何保证数据不丢失
(1)任务挂了,导致元组没有ack
对应的Spout元组将会超时重发。
(2)acker任务挂了
同样,Spout元组会超时重发。
(3)Spout任务挂了
Spout的ack方法不会被执行,重启后,可以重新从队列中读取未处理的值。
7 调节可靠性
如果用户不要求每条数据都要被处理,而是对效率比较看重,那么可以选择取消可靠性保护机制。有以下三种方法:
(1)设置Config.TOPOLOGY_ACKERS为0,这种情况下,Storm不启动Acker任务,Spout发射一个元组后,它的ack方法会立即被调用。
(2)在Spout的SpoutOutputCollector.emit方法中,忽略消息的spout id,这样,这个Spout发射的元组将不会被追踪,且Spout不会受到任何关于ack或fail方法的回调。
(3)如果不介意下游元组的特定子集无法被处理,可以作为非锚定元组发射,这样,下游的元组不会被追踪。
消息传输机制
1 默认的ZeroMQ
(1)本地化的消息库,过度依赖操作系统。
(2)安装麻烦。
(3)在不同的Storm版本之间,ZeroMQ的稳定性差异大。
2 新引入的Netty
(1)纯Java的消息通信解决方案,不依赖平台。
(2)传输性能是ZeroMQ的两倍。
Storm的并行度
与并行度相关的有三个实体:工作进程(Worker),执行器(Executor,即线程),任务(Task)
1 三者之间的关系
Nimbus将Worker平均分配到服务器集群中的节点上。
将所有Executor平均分配到所有的Worker上。
所有任务平均分配到“其对应的”Executor上。(这里需要对应,原因是创建Topology时,同一个组件的Executor和Tast是对应设置的)
2 设置方式
(1)worker
配置选项:TOPOLOGY_WORKERS
代码:Config.setNumWorkers
(2)Executor
配置选项:无
代码:TopologyBuilder.setSpout/setBolt
(3)Task
配置选项:TOPOLOGY_TASK
代码:ComponentConfigurationDeclarer.setNumTasks
版本:2.2.0
部署:
-
解压
tar xvf apache-storm-2.2.0 -C /data/middleware/storm
-
修改配置文件
vim /data/middleware/storm/conf/storm.yaml
配置文件:
#集群中zookeeper的地址
storm.zookeeper.servers:
- "10.0.3.2"
- "10.0.3.3"
- "10.0.3.4"
#集群中zookeeper的端口
storm.zookeeper.port: 2186
storm.zookeeper.connection.timeout: 60000
storm.zookeeper.retry.interval: 2000
storm.zookeeper.session.timeout: 100000
storm.local.dir: "/data/middleware/storm/apache-storm-2.2.0/StormLocalDir"
storm.log.dir: "/data/middleware/storm/apache-storm-2.2.0/logs"
#集群中nimbus的主机名
nimbus.seeds: ["nn-pp-mw000002","nn-pp-mw000003","nn-pp-mw000004"]
#supervisor节点:
supervisor.slots.ports:
- 17601
- 17602
- 17603
- 17604
- 17605
- 17606
- 17607
- 17608
- 17609
- 17610
nimbus.childopts: "-Xmx2048m"
worker.childopts: "-Xmx4096m"
supervisor.childopts: "-Xmx512m"
topology.receiver.buffer.size: 512
topology.acker.executors: 60
topology.acker.tasks: 60
java.library.path: "/usr/local/lib:/opt/local/lib:/usr/lib:/data/middleware/apache-storm-2.2.0/lib"
storm.messaging.netty.server_worker_threads: 80
storm.messaging.netty.buffer_size: 50971520
storm.messaging.netty.max_retries: 30
storm.messaging.netty.max_wait_ms: 30000
storm.messaging.netty.min_wait_ms: 2000
#storm web的端口
ui.port: 17888
supervisor.worker.timeout.secs: 60
topology.worker.receiver.thread.count: 5
topology.enable.message.timeouts: true
topology.message.timeout.secs: 180
-
启动 nimbus+ui supervisor
#nimbus节点启动nimbus ui 其他启动supervisor
nohup bin/storm nimbus &
nohup bin/storm ui &
nohup bin/storm supervisor &