一、原理介绍

Strom的结构

storm 架构设计 storm架构与运行原理_UI

  


Storm与传统关系型数据库  

    传统关系型数据库是先存后计算,而storm则是先算后存,甚至不存  

    传统关系型数据库很难部署实时计算,只能部署定时任务统计分析窗口数据  

    关系型数据库重视事务,并发控制,相对来说Storm比较简陋  

    Storm不Hadoop,Spark等是流行的大数据方案  


与Storm关系密切的语言:核心代码用clojure书写,实用程序用python开发,使用java开发拓扑  

 

topology

    Storm集群中有两种节点,一种是控制节点(Nimbus节点),另一种是工作节点(Supervisor节点)。所有Topology任务的提交必须在Storm客户端节点上进行(需要配置 storm.yaml文件),由Nimbus节点分配给其他Supervisor节点进行处理。 Nimbus节点首先将提交的Topology进行分片,分成一个个的Task,并将Task和Supervisor相关的信息提交到 zookeeper集群上,Supervisor会去zookeeper集群上认领自己的Task,通知自己的Worker进程进行Task的处理。  


    和同样是计算框架的MapReduce相比,MapReduce集群上运行的是Job,而Storm集群上运行的是Topology。但是Job在运行结束之后会自行结束,Topology却只能被手动的kill掉,否则会一直运行下去  


    Storm不处理计算结果的保存,这是应用代码需要负责的事情,如果数据不大,你可以简单地保存在内存里,也可以每次都更新数据库,也可以采用NoSQL存储。这部分事情完全交给用户。  


    数据存储之后的展现,也是你需要自己处理的,storm UI 只提供对topology的监控和统计。  


    总体的Topology处理流程图为:  

storm 架构设计 storm架构与运行原理_zookeeper_02

  

 

zookeeper集群

    storm使用zookeeper来协调整个集群, 但是要注意的是storm并不用zookeeper来传递消息。所以zookeeper上的负载是非常低的,单个节点的zookeeper在大多数情况下都已经足够了, 但是如果你要部署大一点的storm集群, 那么你需要的zookeeper也要大一点。关于如何部署zookeeper,可以看http://zookeeper.apache.org/doc /r3.3.3/zookeeperAdmin.html  

    部署zookeeper有些需要注意的地方:  
    1、对zookeeper做好监控非常重要, zookeeper是fail-fast的系统,只要出现什么错误就会退出, 所以实际场景中要监控,更多细节看http://zookeeper.apache.org/doc/r3.3.3 /zookeeperAdmin.html#sc_supervision  
    2、实际场景中要配置一个cron job来压缩zookeeper的数据和业务日志。zookeeper自己是不会去压缩这些的,所以你如果不设置一个cron job, 那么你很快就会发现磁盘不够用了,更多细节可以查看http://zookeeper.apache.org/doc/r3.3.3 /zookeeperAdmin.html#sc_maintenance  
 

Component

    Storm中,Spout和Bolt都是Component。所以,Storm定义了一个名叫IComponent的总接口  

    全家普如下:绿色部分是我们最常用、比较简单的部分。红色部分是与事务相关的  

storm 架构设计 storm架构与运行原理_zookeeper_03


 

Spout

    Spout是Stream的消息产生源, Spout组件的实现可以通过继承BaseRichSpout类或者其他Spout类来完成,也可以通过实现IRichSpout接口来实现  

public interface ISpout extends Serializable {
void open(Map conf, TopologyContext context, SpoutOutputCollector collector);
void close();
void nextTuple();
void ack(Object msgId);
void fail(Object msgId);
}

    open()方法 -- 初始化方法  
    close() -- 在该spout将要关闭时调用。但是不保证其一定被调用,因为在集群中supervisor节点,可以使用kill -9来杀死worker进程。只有当Storm是在本地模式下运行,如果是发送停止命令,可以保证close的执行。  
    ack(Object msgId) -- 成功处理tuple时回调的方法,通常情况下,此方法的实现是将消息队列中的消息移除,防止消息重放。  
    fail(Object msgId) -- 处理tuple失败时回调的方法,通常情况下,此方法的实现是将消息放回消息队列中然后在稍后时间里重放。  
    nextTuple() -- 这是Spout类中最重要的一个方法。发射一个Tuple到Topology都是通过这个方法来实现的。调用此方法时,storm向spout发出请求, 让spout发出元组(tuple)到输出器(ouput collector)。这种方法应该是非阻塞的,所以spout如果没有元组发出,这个方法应该返回。nextTuple、ack和fail都在spout任务的同一个线程中被循环调用。 当没有元组的发射时,应该让nextTuple睡眠一个很短的时间(如一毫秒),以免浪费太多的CPU。  
继承了BaseRichSpout后,不用实现close、 activate、 deactivate、 ack、 fail 和 getComponentConfiguration 方法,只关心最基本核心的部分。  
通常情况下(Shell和事务型的除外),实现一个Spout,可以直接实现接口IRichSpout,如果不想写多余的代码,可以直接继承BaseRichSpout。  
 

Bolt

    Bolt类接收由Spout或者其他上游Bolt类发来的Tuple,对其进行处理。Bolt组件的实现可以通过继承BasicRichBolt类或者IRichBolt接口等来完成  
    prepare方法 -- 此方法和Spout中的open方法类似,在集群中一个worker中的task初始化时调用。 它提供了bolt执行的环境  
    declareOutputFields方法 -- 用于声明当前Bolt发送的Tuple中包含的字段(field),和Spout中类似  
    cleanup方法 -- 同ISpout的close方法,在关闭前调用。同样不保证其一定执行。  
    execute方法 -- 这是Bolt中最关键的一个方法,对于Tuple的处理都可以放到此方法中进行。具体的发送是通过emit方法来完成的。execute接受一个 tuple进行处理,并用prepare方法传入的OutputCollector的ack方法(表示成功)或fail(表示失败)来反馈处理结果。  
    Storm提供了IBasicBolt接口,其目的就是实现该接口的Bolt不用在代码中提供反馈结果了,Storm内部会自动反馈成功。如果你确实要反馈失败,可以抛出FailedException  
    通常情况下,实现一个Bolt,可以实现IRichBolt接口或继承BaseRichBolt,如果不想自己处理结果反馈,可以实现 IBasicBolt接口或继承BaseBasicBolt,它实际上相当于自动实现了collector.emit.ack(inputTuple)  
 

Topology运行流程

    (1)Storm提交后,会把代码首先存放到Nimbus节点的inbox目录下,之后,会把当前Storm运行的配置生成一个 stormconf.ser文件放到Nimbus节点的stormdist目录中,在此目录中同时还有序列化之后的Topology代码文件  
    (2)在设定Topology所关联的Spouts和Bolts时,可以同时设置当前Spout和Bolt的executor数目和task数目,默认情况下, 一个Topology的task的总和是和executor的总和一致的。之后,系统根据worker的数目,尽量平均的分配这些task的执行。 worker在哪个supervisor节点上运行是由storm本身决定的  
    (3)任务分配好之后,Nimbus节点会将任务的信息提交到zookeeper集群,同时在zookeeper集群中会有workerbeats节点,这里存储了当前Topology的所有worker进程的心跳信息  
    (4)Supervisor节点会不断的轮询zookeeper集群,在zookeeper的assignments节点中保存了所有Topology的任务分配信息、代码存储目 录、任务之间的关联关系等,Supervisor通过轮询此节点的内容,来领取自己的任务,启动worker进程运行  
    (5)一个Topology运行之后,就会不断的通过Spouts来发送Stream流,通过Bolts来不断的处理接收到的Stream流,Stream流是无界的。  
    最后一步会不间断的执行,除非手动结束Topology。  
 

Topology运行方式

    在开始创建项目之前,了解Storm的操作模式(operation modes)是很重要的。 Storm有两种运行方式  
    本地运行的提交方式,例:  

LocalCluster cluster = new LocalCluster();
cluster.submitTopology(TOPOLOGY_NAME, conf, builder.createTopology());
Thread.sleep(2000);
cluster.shutdown();

    分布式提交方式,例:  
StormSubmitter.submitTopology(TOPOLOGY_NAME, conf, builder.createTopology());  
   
    需要注意的是,在Storm代码编写完成之后,需要打包成jar包放到Nimbus中运行,打包的时候,不需要把依赖的jar都打进去,否则如果把依赖的 storm.jar包打进去的话,运行时会出现重复的配置文件错误导致Topology无法运行。因为Topology运行之前,会加载本地的 storm.yaml 配置文件。  

    运行的命令如下: storm jar StormTopology.jar mainclass [args]  
 

storm守护进程的命令

    Nimbus: storm nimbus 启动nimbus守护进程  
    Supervisor: storm supervisor 启动supervisor守护进程  
    UI:storm ui 这将启动stormUI的守护进程,为监测storm集群提供一个基于web的用户界面。 
    DRPC: storm drpc 启动DRPC的守护进程  
 

storm管理命令

    JAR:storm jar topology_jar topology_class [arguments...]  
    jar命令是用于提交一个集群拓扑.它运行指定参数的topology_class中的main()方法,上传topology_jar到nimbus, 由nimbus发布到集群中。一旦提交,storm将激活拓扑并开始处理topology_class 中的main()方法,main()方法负责调用StormSubmitter.submitTopology()方法,并提供一个唯一的拓扑(集群)的名。如果一个拥有该名称的拓扑已经存在于集群中,jar命令将会失败。常见的做法是在使用命令行参数来指定拓扑名称,以便拓扑在提交的时候被命名。  

    KILL:storm kill topology_name [-w wait_time]  
    杀死一个拓扑,可以使用kill命令。它会以一种安全的方式销毁一个拓扑,首先停用拓扑,在等待拓扑消息的时间段内允许拓扑完成当前的数据流。执行kill命令时可以通过-w [等待秒数]指定拓扑停用以后的等待时间。也可以在Storm UI 界面上实现同样的功能  

    Deactivate:storm deactivate topology_name  
    停用拓扑时,所有已分发的元组都会得到处理,spouts的nextTuple方法将不会被调用。也可以在Storm UI 界面上实现同样的功能  

    Activate:storm activate topology_name  
    启动一个停用的拓扑。也可以在Storm UI 界面上实现同样的功能  

    Rebalance:storm rebalance topology_name [-w wait_time] [-n worker_count] [-e component_name=executer_count]...  
    rebalance使你重新分配集群任务。这是个很强大的命令。比如,你向一个运行中的集群增加了节点。rebalance命令将会停用拓扑,然后在相应超时时间之后重分配worker,并重启拓扑  
例:storm rebalance wordcount-topology -w 15 -n 5 -e sentence-spout=4 -e split-bolt=8  

    还有其他管理命令,如:Remoteconfvalue、REPL、Classpath等  
 

新建storm项目注意事项

    为了开发storm项目,你的classpath里面需要有storm的jar包。最推荐的方式是使用Maven,不使用maven的话你可以手动把storm发行版里面的所有的jar包添加到classpath  

    storm-starter项目使用Leiningen作为build和依赖管理工具,你可以下载这个脚本 (https://raw.githubusercontent.com/technomancy/leiningen/stable/bin /lein)来安装Leiningen, 把它加入到你的PATH, 使它可执行。要拉取storm的所有依赖包,简单地在项目的根目录执行 lein deps 就可以了。

二、配置

完整的默认配置文件见下面defaluts.yaml,若需要修改,则在storm.yaml中修改。重要参数如下:
1、storm.zookeeper.servers:指定使用哪个zookeeper集群

storm.zookeeper.servers:
     - "gdc-nn01-test"
     - "gdc-dn01-test"
     - "gdc-dn02-test”

2、nimbus.host:指定nimbus是哪台机器

nimbus.host: "gdc-nn01-test”

3、指定supervisor在哪个端口上运行worker,每个端口可运行一个worker,因此有多少个配置端口,则每个supervisor有多少个slot(即可运行多少个worker)

supervisor.slots.ports:
    - 6700
    - 6701
    - 6702
    - 6703
storm.local.dir: "/home/hadoop/storm/data"

4、jvm设置

#jvm setting
nimbus.childopts:"-4096m”
supervisor.childopts:"-Xmx4096m"
nimubs.childopts:"-Xmx3072m”

除此外,还有ui.childopts,logviewer.childopts

附完整配置文件:defaults.yaml

########### These all have default values as shown
########### Additional configuration goes into storm.yaml
java.library.path: "/usr/local/lib:/opt/local/lib:/usr/lib"

### storm.* configs are general configurations
# the local dir is where jars are kept
storm.local.dir: "storm-local"
storm.zookeeper.servers:
    - "localhost"
storm.zookeeper.port: 2181
storm.zookeeper.root: "/storm"
storm.zookeeper.session.timeout: 20000
storm.zookeeper.connection.timeout: 15000
storm.zookeeper.retry.times: 5
storm.zookeeper.retry.interval: 1000
storm.zookeeper.retry.intervalceiling.millis: 30000
storm.cluster.mode: "distributed" # can be distributed or local
storm.local.mode.zmq: false
storm.thrift.transport: "backtype.storm.security.auth.SimpleTransportPlugin"
storm.messaging.transport: "backtype.storm.messaging.netty.Context"
storm.meta.serialization.delegate: "backtype.storm.serialization.DefaultSerializationDelegate"

### nimbus.* configs are for the master
nimbus.host: "localhost"
nimbus.thrift.port: 6627
nimbus.thrift.max_buffer_size: 1048576
nimbus.childopts: "-Xmx1024m"
nimbus.task.timeout.secs: 30
nimbus.supervisor.timeout.secs: 60
nimbus.monitor.freq.secs: 10
nimbus.cleanup.inbox.freq.secs: 600
nimbus.inbox.jar.expiration.secs: 3600
nimbus.task.launch.secs: 120
nimbus.reassign: true
nimbus.file.copy.expiration.secs: 600
nimbus.topology.validator: "backtype.storm.nimbus.DefaultTopologyValidator"

### ui.* configs are for the master
ui.port: 8080
ui.childopts: "-Xmx768m"
logviewer.port: 8000
logviewer.childopts: "-Xmx128m"
logviewer.appender.name: "A1"

drpc.port: 3772
drpc.worker.threads: 64
drpc.queue.size: 128
drpc.invocations.port: 3773
drpc.request.timeout.secs: 600
drpc.childopts: "-Xmx768m"
transactional.zookeeper.root: "/transactional"
transactional.zookeeper.servers: null
transactional.zookeeper.port: null

### supervisor.* configs are for node supervisors
# Define the amount of workers that can be run on this machine. Each worker is assigned a port to use for communication
supervisor.slots.ports:
    - 6700
    - 6701
    - 6702
    - 6703
supervisor.childopts: "-Xmx256m"
#how long supervisor will wait to ensure that a worker process is started
supervisor.worker.start.timeout.secs: 120
#how long between heartbeats until supervisor considers that worker dead and tries to restart it
supervisor.worker.timeout.secs: 30
#how frequently the supervisor checks on the status of the processes it's monitoring and restarts if necessary
supervisor.monitor.frequency.secs: 3
#how frequently the supervisor heartbeats to the cluster state (for nimbus)
supervisor.heartbeat.frequency.secs: 5
supervisor.enable: true
### worker.* configs are for task workers
worker.childopts: "-Xmx768m"
worker.heartbeat.frequency.secs: 1

# control how many worker receiver threads we need per worker
topology.worker.receiver.thread.count: 1
task.heartbeat.frequency.secs: 3
task.refresh.poll.secs: 10
zmq.threads: 1
zmq.linger.millis: 5000
zmq.hwm: 0
storm.messaging.netty.server_worker_threads: 1
storm.messaging.netty.client_worker_threads: 1
storm.messaging.netty.buffer_size: 5242880 #5MB buffer
# Since nimbus.task.launch.secs and supervisor.worker.start.timeout.secs are 120, other workers should also wait at least that long before giving up on connecting to the other worker. The reconnection period need also be bigger than storm.zookeeper.session.timeout(default is 20s), so that we can abort the reconnection when the target worker is dead.
storm.messaging.netty.max_retries: 30
storm.messaging.netty.max_wait_ms: 1000
storm.messaging.netty.min_wait_ms: 100

# If the Netty messaging layer is busy(netty internal buffer not writable), the Netty client will try to batch message as more as possible up to the size of storm.messaging.netty.transfer.batch.size bytes, otherwise it will try to flush message as soon as possible to reduce latency.
storm.messaging.netty.transfer.batch.size: 262144

# We check with this interval that whether the Netty channel is writable and try to write pending messages if it is.
storm.messaging.netty.flush.check.interval.ms: 10
### topology.* configs are for specific executing storms
topology.enable.message.timeouts: true
topology.debug: false
topology.workers: 1
topology.acker.executors: null
topology.tasks: null
# maximum amount of time a message has to complete before it's considered failed
topology.message.timeout.secs: 30
topology.multilang.serializer: "backtype.storm.multilang.JsonSerializer"
topology.skip.missing.kryo.registrations: false
topology.max.task.parallelism: null
topology.max.spout.pending: null
topology.state.synchronization.timeout.secs: 60
topology.stats.sample.rate: 0.05
topology.builtin.metrics.bucket.size.secs: 60
topology.fall.back.on.java.serialization: true
topology.worker.childopts: null
topology.executor.receive.buffer.size: 1024 #batched
topology.executor.send.buffer.size: 1024 #individual messages
topology.receiver.buffer.size: 8 # setting it too high causes a lot of problems (heartbeat thread gets starved, throughput plummets)
topology.transfer.buffer.size: 1024 # batched
topology.tick.tuple.freq.secs: null
topology.worker.shared.thread.pool.size: 4
topology.disruptor.wait.strategy: "com.lmax.disruptor.BlockingWaitStrategy"
topology.spout.wait.strategy: "backtype.storm.spout.SleepSpoutWaitStrategy"
topology.sleep.spout.wait.strategy.time.ms: 1
topology.error.throttle.interval.secs: 10
topology.max.error.report.per.interval: 5
topology.kryo.factory: "backtype.storm.serialization.DefaultKryoFactory"
topology.tuple.serializer: "backtype.storm.serialization.types.ListDelegateSerializer"
topology.trident.batch.emit.interval.millis: 500
topology.classpath: null
topology.environment: null
dev.zookeeper.path: "/tmp/dev-storm-zookeeper"</span>

三、并行度

(一)storm拓扑的并行度可以从以下4个维度进行设置:

1、node(服务器):指一个storm集群中的supervisor服务器数量。
2、worker(jvm进程):指整个拓扑中worker进程的总数量,这些数量会随机的平均分配到各个node。
3、executor(线程):指某个spout或者bolt的总线程数量,这些线程会被随机平均的分配到各个worker。
4、task(spout/bolt实例):task是spout和bolt的实例,它们的nextTuple()和execute()方法会被executors线程调用。除非明确指定,storm会给每个executor分配一个task。如果设置了多个task,即一个线程持有了多个spout/bolt实例.
注意:以上设置的都是总数量,这些数量会被平均分配到各自的宿主上,而不是设置每个宿主进行多少个进程/线程。详见下面的例子。

(二)并行度的设置方法

1、node:买机器吧,然后加入集群中……
2、worker:Config#setNumWorkers() 或者配置项 TOPOLOGY_WORKERS
3、executor:Topology.setSpout()/.setBolt()
4、task:ComponentConfigurationDeclarer#setNumWorker()

(三)示例

// 创建topology  
TopologyBuilder builder = new TopologyBuilder();  
builder.setSpout("kafka-reader", new KafkaSpout(spoutConf), 5);//设置executor数量为5  
builder.setBolt("filter-bolt", new FilterBolt(), 3).shuffleGrouping(  
        "kafka-reader");//设置executor数量为3  
builder.setBolt("log-splitter", new LogSplitterBolt(), 3)  
        .shuffleGrouping("filter-bolt");//设置executor数量为5  
builder.setBolt("hdfs-bolt", hdfsBolt, 2).shuffleGrouping(  
        "log-splitter");//设置executor数量为2  
  
// 启动topology  
Config conf = new Config();  
conf.put(Config.NIMBUS_HOST, nimbusHost);  
conf.setNumWorkers(3);      //设置worker数量  
StormSubmitter.submitTopologyWithProgressBar(topologyName, conf,  
        builder.createTopology());

1、通过config.setNumWorkers(3)将worker进程数量设置为3,假设集群中有3个node,则每个node会运行一个worker。
2、executor的数量分别为:
spout:5
filter-bolt:3
log-splitter:3
hdfs-bolt:2
总共为13个executor,这13个executor会被随机分配到各个worker中去。
注:这段代码是从kafka中读取消息源的,而这个topic在kafka中的分区数量设置为5,因此这里spout的线程ovtn为5.
3、这个示例都没有单独设置task的数量,即使用每个executor一个task的默认配置。若需要设置,可以:

builder.setBolt("log-splitter", new LogSplitterBolt(), 3)
                .shuffleGrouping("filter-bolt").setNumTasks(5);

来进行设置,这5个task会被分配到3个executor中。

(四)并行度的动态调整
对storm拓扑的并行度进行调整有2种方法:
1、kill topo—>修改代码—>编译—>提交拓扑
2、动态调整
第1种方法太不方便了,有时候topo不能说kill就kill,另外,如果加几台机器,难道要把所有topo kill掉还要修改代码?
因此storm提供了动态调整的方法,动态调整有2种方法:
1、ui方式:进入某个topo的页面,点击rebalance即可,此时可以看到topo的状态是rebalancing。但此方法只是把进程、线程在各个机器上重新分配,即适用于增加机器,或者减少机器的情形,不能调整worker数量、executor数量等
2、cli方式:storm rebalance
举个例子

storm rebalance toponame -n 7 -e filter-bolt=6 -e hdfs-bolt=8

将topo的worker数量设置为7,并将filter-bolt与hdfs-bolt的executor数量分别设置为6、8.
此时,查看topo的状态是rebalancing,调整完成后,可以看到3台机器中的worker数量分别为3、2、2

四、分组

Storm通过分组来指定数据的流向,主要指定了每个bolt消费哪个流,以及如何消费。
storm内置了7个分组方式,并提供了CustomStreamGrouping来创建自定义的分组方式。
1、随机分组 shuffleGrouping
这种方式会随机分发tuple给bolt的各个task,每个task接到到相同数量的tuple。

2、字段分组 fieldGrouping
按照指定字段进行分组,该字段具有相同组的会被发送到同一个task,具体不同值的可能会被发送到不同的task。

3、全复制分组 allGrouping(或者叫广播分组)
每一个tuple都会发送给所有的task,必须小心使用。

4、全局分组 globlaGrouping
将所有tuple均发送到唯一的task,会选取task ID最小的task。这种分组下,设置task的并行度是没有意义的。另外,这种方式很有可能引起瓶颈。

5、不分组 noneGrouping
留作以后使用,目前也随机分组相同。

6、指向型分组 directGrouping(或者叫直接分组)
数据源会调用emitDirect()方法来判断一个tuple应该由哪个storm组件来接收,只能在声明了是指向型的数据流上使用。

7、本地或随机分组 localOrShuffleGrouping
如果接收bolt在同一个进程中存在一个或者多个task,tuple会优先发送给这个task。否则和随机分组一样。相对于随机分组,此方式可以减少网络传输,从而提高性能。

五、可靠性

可靠性:spout发送的消息会被拓扑树上的所有节点ack,否则会一直重发。
导致重发的原因有2个:
(1)fail()被调用
(2)超时无响应。
完整的可靠性示例请参考storm blueprint的chapter1 v4代码,或者P22,或者参考从零开始学storm P102页的例子。
关键步骤如下:

(一)spout

1、创建一个map,用于记录已经发送的tuple的id与内容,此为待确认的tuple列表。
private ConcurrentHashMap<UUID,Values> pending;
2、发送tuple时,加上一个参数用于指明该tuple的id。同时,将此tuple加入map中,等待确认。
UUID msgId = UUID.randomUUID();
this.pending.put(msgId,values);
this.collector.emit(values,msgId);
3、定义ack方法与fail方法。
ack方法将tuple从map中取出
this.pending.remove(msgId);
fail方法将tuple重新发送
this.collector.emit(this.pending.get(msgId),msgId);

对于没回复的tuple,会定时重新发送。

(二)bolt

处理该tuple的每个bolt均需要增加以下内容:
1、emit时,增加一个参数anchor,指定响应的tuple
collector.emit(tuple,new Values(word));
2、确认接收到的tuple已经处理
this.collector.ack(tuple);