Storm整理(上)
一,Storm认识
Storm是个实时的,分布式以及具备高容错的计算框架。
主要有两个特点:Storm进程常驻内存,Storm数据不经过磁盘,在内存中处理。
应用:
双十一实时更新成交额:
QQ实时在线人数统计:
Storm架构简单介绍,后面有详细解释
架构:
-
Nimbus
-
Supervisor
-
Worker
编程模型:
- DAG(Topology)
- Spout
- Bolt
数据传输:
- ZMQ
ZeroMQ开源的消息传递框架,并不是一个MessageQueue
- Netty
Netty是基于NIO的网络框架,更加高效(之所以Storm0.9版本之后没用ZMQ,而是使用了Netty,只要原因是ZMQ的licese和Storm的licese不兼容)。
高可靠性:
- 异常处理
- 消息可靠性保障机制
可维护性:
- StormUI图形化监控接口
Storm - 流式处理:
- 流式处理(异步)
客户端提交数据进行结算,并不会等待数据计算结果
- 逐条处理
例如:ETL
- 统计分析
例:计算 PV,UV,访问热点以及某些数据的聚合,加和,平均等
客户端提交数据之后,计算完成结果存储到Redis,Mysql,Hbase…
客户端并不关心最终结果是多少。
Storm - 实时请求
- 实时请求应答服务(服务)
客户端提交数据后,立刻取得结果结果并返回给客户端
- Drpc
- 实时请求处理
例:图片特征提取
Storm和MapReduce对比:
Storm: 进程,线程常驻内存中运行,数据不进入到磁盘,数据通过网络传递。
MapReduce: 为TB,PB级别数据设计的批处理计算框架。
Storm和Spark Streaming 对比
Storm:纯流式处理
- 专门为流式处理设计
- 数据传输模式更为简单,很多地方也更为高效
- 并不是不能做批处理,他也可以做微量批处理,来提高吞吐
Spark Streaming :微批处理
- 将RDD做的很小来用小的批处理来接近流式处理
- 基于内存和DAG
二,概念加深,实例
2.1 Storm计算模型
- Topology - DAG有向无环图的实现
---- 对于Storm实时计算逻辑的封装
---- 即,由一系列通过数据流相互关联的Spout, Bolt 所组成的拓扑结构
---- 声明周期:此拓扑只要启动就会一直在集群中运行,直到手动将其kill,否则不会终止(区别与Mapreduce当中的Job,MR当中的Job在计算执行完成就会终止)
- Tuple - 元组
---- Stream中最小的数据组成单元
- Stream - 数据流
---- 从Spout中源源不断传递数据给Bolt,以及上一个Bolt传递给下一个Blot.所形成的这些数据通道叫做Stream
---- Stream声明时需给其指定一个id(默认为Default),实际开发场景中,多使用单一数据流,此时不需要单独指定StreamId
- Spout - 数据源
---- 拓扑中数据流的来源。一般会从指定外部的数据源读取元组(Tuple)发送到拓扑中(Topology)
---- 一个Spout可以发送多个数据流(Stream)
可以通过OutPutFieldsDeclare中的declare方法声明定义的不同数据流,发送数据时通过SpoutOutPutCollector中的emit指定数据流id(StreamId)参数将数据发送出去。
---- Spout中最核心的方法是nextTuple,该方法会被Storm线程不断调用,主动从数据源拉取数据,再通过emit方法将数据生成元组(Tuple)发送给之后的Bolt计算。
- Bolt - 数据流处理组件
---- 拓扑中数据处理均由Bolt处理。对于简单的任务或者数据流转换,单个Blot可以简单实现,更加复杂的场景需要多个Bolt分多个步骤完成。
---- 一个Bolt可以发送多个数据流(Stream)
可先通过OuPutFieldsDeclare的declare方法声明定义的不同数据流,发送数据时通过SpoutOutPutCollector中的emit方法指定数据流id(StreamId)参数将数据发送出去。
---- Bolt中最核心的方法是execute方法,该方法负责接收到一个元组(Tuple)数据,真正实现核心的业务的逻辑。
2.2 Strom 数据累加
先看MyTopology.java
package com.shsxt.api;
import backtype.storm.Config;
import backtype.storm.LocalCluster;
import backtype.storm.generated.StormTopology;
import backtype.storm.topology.TopologyBuilder;
public class MyTopology {
public static void main(String args[]){
//实例化 TopologyBuilder 对象
TopologyBuilder topologyBuilder = new TopologyBuilder();
//设置,Spout相关信息
topologyBuilder.setSpout("myspout",new MySpout());
//设置,Bolt相关信息
topologyBuilder.setBolt("mybolt",new MyBolt()).shuffleGrouping("myspout"); //shuffleGrouping 设置负责策略是随机分组,同时绑定Spout和Bolt的关系
//通过 TopologyBuilder 对象 构建StormTopology对象
StormTopology topology = topologyBuilder.createTopology();
Config config = new Config();
LocalCluster cluster = new LocalCluster();
//本地提交
cluster.submitTopology("sum",config,topology);
}
}
MySpout.java
package com.shsxt.api;
import backtype.storm.spout.SpoutOutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.IRichSpout;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichSpout;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Values;
import java.util.Map;
public class MySpout extends BaseRichSpout {
private SpoutOutputCollector spoutOutputCollector;
int sum = 0;
/**
* 初始化方法,框架在执行任务的时候,会先执行此方法
* @param map 得到spout的配置
* @param topologyContext 上下文环境
* @param spoutOutputCollector 往下游发送数据
*/
@Override
public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {
this.spoutOutputCollector = spoutOutputCollector;
}
/**
* 此方法是Spout的核心方法
* 框架会一直调用这个方法,每当调用此方法时,往下游发送数据
*/
@Override
public void nextTuple() {
sum++;
spoutOutputCollector.emit(new Values(sum));
try {
//发送一个Tuple,停一秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("spout发送......"+sum);
}
/**
* 当需要往下游发送数据时,就要声明字段个数和字段名字
* @param outputFieldsDeclarer
*/
@Override
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
outputFieldsDeclarer.declare(new Fields("number"));
}
}
MyBolt.java
package com.shsxt.api;
import backtype.storm.task.OutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.IBasicBolt;
import backtype.storm.topology.IRichBolt;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichBolt;
import backtype.storm.tuple.Tuple;
import java.util.Map;
public class MyBolt extends BaseRichBolt {
int sum = 0;
/**
* bolt初始化方法
* @param map
* @param topologyContext
* @param outputCollector
*/
@Override
public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {
}
/**
* blot最核心的方法
* storm框架会一直调用该方法,每次调用就传一个数据进来
* @param tuple
*/
@Override
public void execute(Tuple tuple) {
Integer count = tuple.getInteger(0);
sum+=count;
System.out.println("exexute...:"+count+" sum:"+sum);
}
@Override
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
}
}
运行效果:
不手动停止的话,会一直跑下去…
2.3 Storm WordCount
先看下图,
可以看到流程中两个Bolt,一个负责切分数据,并把数据传递给下一个Bolt,接下来看代码实现
WcTopology.java
package com.shsxt.storm;
import backtype.storm.Config;
import backtype.storm.LocalCluster;
import backtype.storm.StormSubmitter;
import backtype.storm.generated.AlreadyAliveException;
import backtype.storm.generated.InvalidTopologyException;
import backtype.storm.generated.StormTopology;
import backtype.storm.topology.TopologyBuilder;
import backtype.storm.tuple.Fields;
public class WcTopology {
public static void main(String args[]){
TopologyBuilder topologyBuilder = new TopologyBuilder();
topologyBuilder.setSpout("wcspout",new WcSpout());
topologyBuilder.setBolt("spiltBolt",new SpiltBolt(),4).shuffleGrouping("wcspout").setNumTasks(8);
//接收SpiltBolt发送出去的数据,指定并发数,和根据字段值分组
topologyBuilder.setBolt("countBolt",new CountBolt(),5).fieldsGrouping("spiltBolt",new Fields("word"));
StormTopology topology = topologyBuilder.createTopology();
Config config = new Config();
config.setNumWorkers(3);
//如果不传参数,就是在本地运行,传参数,就是在集群运行
if (args.length>0){
try {
StormSubmitter.submitTopology(args[0],config,topology);
} catch (AlreadyAliveException e) {
e.printStackTrace();
} catch (InvalidTopologyException e) {
e.printStackTrace();
}
}else {
LocalCluster cluster = new LocalCluster();
cluster.submitTopology("mytopology",config,topology);
}
}
}
WcSpout.java
package com.shsxt.storm;
import backtype.storm.spout.SpoutOutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.IRichSpout;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichSpout;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Values;
import java.util.Map;
import java.util.Random;
public class WcSpout extends BaseRichSpout {
private SpoutOutputCollector collector;
//随机设置些数据
String [] lines = {
"i like play",
"i not like study",
"not eat ",
"day day up"
};
Random random = new Random();
@Override
public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {
this.collector = spoutOutputCollector;
}
@Override
public void nextTuple() {
int index = random.nextInt(lines.length);
//发送tuple出去
collector.emit(new Values(lines[index]));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
//指定发送出去的tuple的字段
outputFieldsDeclarer.declare(new Fields("line"));
}
}
SplitBolt.java
package com.shsxt.storm;
import backtype.storm.task.OutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.IBasicBolt;
import backtype.storm.topology.IRichBolt;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichBolt;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Tuple;
import backtype.storm.tuple.Values;
import java.util.Map;
public class SpiltBolt extends BaseRichBolt {
private OutputCollector collector;
@Override
public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {
this.collector = outputCollector;
}
@Override
public void execute(Tuple tuple) {
//根据上面Spout指定的字段获取tuple,也可以根据下标获取
String line = tuple.getStringByField("line");
String[] split = line.split(" ");
for (String word: split){
//切分每一行数据,发送到下游
collector.emit(new Values(word));
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
//指定发送出去的字段
outputFieldsDeclarer.declare(new Fields("word"));
}
}
CountBolt.java
package com.shsxt.storm;
import backtype.storm.task.OutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.IBasicBolt;
import backtype.storm.topology.IRichBolt;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichBolt;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Tuple;
import java.util.HashMap;
import java.util.Map;
public class CountBolt extends BaseRichBolt {
//定义一个Map 用来记录单词的数量
HashMap<String,Integer> map = new HashMap<>();
@Override
public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {
}
@Override
public void execute(Tuple tuple) {
String word = tuple.getStringByField("word");
//Map中存在就累加,不存在就Put,起始值为1
if (map.containsKey(word)){
map.put(word,map.get(word)+1);
}else {
map.put(word,1);
}
System.out.println(word+"------>"+map.get(word));
}
@Override
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
outputFieldsDeclarer.declare(new Fields("count"));
}
}
运行:
三,深入Storm架构
- Nimbus
---- 资源的调度
---- 任务的分配
---- 接收jar包
- Supervisor
---- 接收zookeeper上Nimbus分配的任务
---- 启动,停止自己管理的worker进程(当前Supervisor上Worker数量由配置文件决定)
- Worker
---- 运行具体处理运算组件的过程(每个Worker对应执行一个Topology的子集)
---- worker任务类型:Spout类型,Bolt类型
---- 启动 executor (executor是Worker JVM中的一个Java进程,一般默认每个executor负责执行一个task任务)
- Zookeeper
3.1 Storm和MapReduce
MapReduce | Storm | |
---|---|---|
主节点 | ResurceManager | Nimbus |
从节点 | NodeManager | supervisor |
应用程序 | JobTracker | Topolopy |
工作进程 | YarnChild | Worker |
计算模型 | Map/Reduce | Spout/Bolt |
3.2 Strom任务提交流程
-
客户端将提交的jar包上传至Nimbus服务器 nimbus/inbox目录下
-
对topology进行一些校验(是否同名),检查Strom的集群状态是否是Active,Spout,Blot的id不能以“__”开头,这种命名方式是系统保留的。
-
建立topology在本地的存放目录nimbus/stormdist/topology-id,该目录下包含三个文件
stormjar.jar --从nimbus/inbox目录下移动来的topology的jar包
stormcode.ser --对topology对象的序列化方法
stormconf.ser --对topology的运行配置
-
nimbus分配任务,即根据代码初始化spout/bolt的task数目,并分配给对应的task-id,最后将这些信息写入到zk的/task节点下
-
nimbus在zk上创建taskbeats节点,监控task的心跳
-
将任务分配信息写入到assignment/topology-id节点中,此时即可认为任务提交完毕
-
在zk的storm/topology-id 节点下存放任务的运行时间,状态等信息
-
Supervisor定期检查zk上的storm节点,是否有新任务提交
-
删除本地不再运行的任务
-
根据Nimbus指定的任务信息,启动该节点上的Worker
-
Worker需要查看执行的task任务信息
-
获取到相应的Task信息,即Spout/Blot任务信息
-
执行具体运算,并根据Ip以及端口发送消息数据
3.3 Storm目录树
3.4 Storm Zk目录树
四,Storm部署
部署Zookeeper集群,可查看本人相关博客
节点:node01,node02,node03
集群规划:node01为nimbus,其余两台为Supervisor
4.1 上传解压
三台集群都需要…
tar -zvxf ....
4.2 在Storm目录中新建logs目录
存放运行的日志
4.3 修改配置文件
- strom.yarm
三台节点配置文件一致
4.4 启动Storm集群
- 启动ZK集群
- 在Node01上启动Nimbus
./bin/storm nimbus >> ./logs/nimbus.out 2>&1 &
-
在node02,node03启动supervisor
./bin/storm supervisor >> ./logs/supervisor.out 2>&1 &
Jps查看进程是否成功启动
4.5 Storm UI启动
在任意一台节点启动即可,我这里选的是node02
./storm ui >> ./logs/ui.out 2>&1 &
通过 node02:8080 访问
五,Storm Grouping
5.1 Shuffle Grouping
随机分组,随机分派Stream里面的tuple,保证每个bolt task接收到的tuple数目大致相同
轮询,平均分配
5.2 Fields Grouping
按字段分组,比如,按"user-id"这个字段来分组,那么具有同样"user-id"的 tuple 会被分到相同的Bolt里的一个task, 而不同的"user-id"则可能会被分配到不同的task。
5.3 All Grouping
广播发送,对于每一个tuple,所有的Bolt都会收到
5.4 Global Grouping
全局分组,把tuple分配给task id最低的task
5.5 None Grouping
不分组,这个分组的意思是说stream不关心到底怎样分组。目前这种分组和Shuffle grouping是一样的效果。有一点不同的是storm会把使用none grouping的这个bolt放到这个bolt的订阅者同一个线程里面去执行(未来Storm如果可能的话会这样设计)。
5.6 Direct Grouping
指向型分组, 这是一种比较特别的分组方法,用这种分组意味着消息(tuple)的发送者指定由消息接收者的哪个task处理这个消息。只有被声明为 Direct Stream 的消息流可以声明这种分组方法。而且这种消息tuple必须使用 emitDirect 方法来发射。消息处理者可以通过TopologyContext 来获取处理它的消息的task的id(OutputCollector.emit方法也会返回task的id)
5.7 Local or shuffle Grouping
本地或随机分组。如果目标bolt有一个或者多个task与源bolt的task在同一个工作进程中,tuple将会被随机发送给这些同进程中的tasks。否则,和普通的Shuffle Grouping行为一致.
5.8CustomGrouping
自定义,相当于mapreduce那里自己去实现一个partition一样。
六,并发机制
- Worker - 进程
---- 一个Topology拓扑会包含一个或多个Worker(每个Worker进程只能从属于一个Topology)
---- 这些Worker进程会并行跑在集群中不同的服务器上,即一个Topology拓扑其实是由并行在Strom集群中多台服务器上的进程所组成
- Executor - 线程
---- Executor是由Worker进程中生成的一个线程
---- 每个Worker进程中会运行拓扑当中的一个或多个Executor线程
---- 一个Executor线程中可以执行一个或多个Task任务(默认每个Executor只执行一个Task任务),但是这些Task任务都是对应着同一个组件(Spout,Bolt)
- Task
---- 实际执行数据处理的最小单元
---- 每个Task即为一个Spout或者一个Bolt
- Task数量在整个Topology声明周期中保持不变,Executor数量可以变化或手动调整
- 默认情况下,Task数量和Executor是相同的,即每个Executor中运行一个Task任务
- 设置Worker进程数
Config.setNumWorkers(int workers)
- 设置Executor线程数
TopologyBuilder.setSpout(String id, IRichSpout spout, Number parallelism_hint)
TopologyBuilder.setBolt(String id, IRichBolt bolt, Number parallelism_hint)
## 其中, parallelism_hint即为executor线程数
- 设置Task数量
ComponentConfigurationDeclarer.setNumTasks(Number val)
- 例
Config conf = new Config() ;
conf.setNumWorkers(2);
TopologyBuilder topologyBuilder = new TopologyBuilder();
topologyBuilder.setSpout("spout", new MySpout(), 1);
topologyBuilder.setBolt("green-bolt", new GreenBolt(), 2)
.setNumTasks(4)
.shuffleGrouping("blue-spout);
- Rebalance - 再平衡
---- 即,动态调整拓扑的Worker进程数量,以及Executor线程的数量
- 支持两种调整方式
---- 通过Storm UI
---- 通过Strom CLI
- 例
storm rebalance mytopology -n 5 -e blue-spout=3 -e yellow-bolt=10
将mytopology拓扑worker的个数调整为5个
“blue-spout”所使用的线程数量调整为3个
"yellow-bolt"所使用的线程数量调整为10个
七,通信机制
- Worker进程间的数据通信
---- ZMQ
ZeroMQ开源的消息传递框架,并不是一个MessageQueue
---- Netty
Netty是基于NIO的网络框架,更加高效(Storm0.9版本之后使用Netty,因为ZMQ的licenese和Storm的license不兼容)
- Worker内部的数据通信
---- Disruptor
实现了队列的功能
可以理解为一种事件监听或者消息处理机制,即在队列一端生产消息数据,另一边消费者并行取出消息数据来处理
八,容错机制
8.1 集群节点宕机
-Nimbus服务器
- 单点故障
-非Nimbus服务器
- 故障时,该节点上所有Task任务都会超时,Nimbus会将这些Task分配到其他节点上运行
8.2 进程挂掉
-Worker
- 挂掉时,Superivsor会重新启动这个进程。如果启动过程中仍然一直失败,并且无法向Nimbus发送心跳,Nimbus会将Worker分配到其他服务器上
-Superivsor
- 无状态(所有的状态信息都存放在Zk集群中来管理)
- 快速失败(每当遇到任何异常情况,都会自动毁灭)
-Nimbus
- 无状态(所有的状态信息都存放在zk集群中来管理)
- 快速失败(每当遇到任何异常情况,都会自动毁灭)
8.3 消息的完整性
-
从Spout中发成的Tuple,以及基于它所产生的Tuple
-
由这些消息就构成了一颗Tuple树
-
当这棵树发送完成,并且数当中每一条消息都被正确处理,就表明Spout发送的消息被完整处理,即消息的完整性。
-
Acker – 消息完整性的实现机制
---- Storm的拓扑当中特殊的一些任务
---- 负责跟踪每个Spout发出的Tuple的DAG(有向无环图)