一、Storm的简介
2013年,Storm进入Apache社区进行孵化, 2014年9月,晋级成为Apache顶级项目。 Storm是一个免费开源、分布式、高容错的实时计算系统。Storm令持续不断的流计算变得容易,弥补了Hadoop批处理所不能满足的实时要求。Storm经常用于在实时分析、在线机器学习、持续计算、分布式远程调用和ETL等领域。Storm的部署管理非常简单,而且,在同类的流式计算工具,Storm的性能也是非常出众的。
总结:Storm是个实时的、分布式以及具备高容错的计算系统,Storm进程常驻内存
1.1、Storm的优点
- 编程简单:开发人员只需要关注应用逻辑,而且跟Hadoop类似,Storm提供的编程原语也很简单
- 高性能,低延迟:可以应用于广告搜索引擎这种要求对广告主的操作进行实时响应的场景。
- 分布式:可以轻松应对数据量大,单机搞不定的场景
- 可扩展: 随着业务发展,数据量和计算量越来越大,系统可水平扩展
- 容错:单个节点挂了不影响应用
- 消息不丢失:保证消息处理
1.2、Storm的通信机制
Worker进程间的数据通信
数据传输方面,ZeroMQ 提供了可扩展环境下的传输层高效消息通信,一开始Storm的内部通信使用的是ZeroMQ,后来作者想把Storm移交给Apache开源基金会来管理,而ZeroMQ的许可证书跟Apache基金会的政策有冲突。在Storm中,Netty比ZeroMQ更加高效,而且提供了worker间通信时的验证机制,所以在Storm0.9中,就改用了Netty。
Worker内部的数据通信
Disruptor 实现了“队列”的功能。 可以理解为一种事件监听或者消息处理机制,即在队列当中一边由生产者放入消息数据,另一边消费者并行取出消息数据处理。
Worker内部的消息传递机制
1.3、流式计算一般架构图
- 其中flume用来获取数据,Kafka用来临时保存数据,Strom用来计算数据,Redis是个内存数据库,用来保存数据。
二、Storm的架构
与Hadoop主从架构一样,Storm也采用Master/Slave体系结构,分布式计算由Nimbus和Supervisor两类服务进程实现,Nimbus进程运行在集群的主节点,负责任务的指派和分发,Supervisor运行在集群的从节点,负责执行任务的具体部分。
如图所示:
- Nimbus:负责资源分配和任务调度。
- Supervisor:负责接受nimbus分配的任务,启动和停止属于自己管理的worker进程。
- Worker:运行具体处理组件逻辑的进程。
- Task:worker中每一个spout/bolt的线程称为一个task。同一个spout/bolt的task可能会共享一个物理线程,该线程称为executor。
1、Storm 架构设计与Hadoop架构对比
2、Storm,Sparkstreaming,Mapreduce相关概念比较
Storm:(实时处理)
- 专门为流式处理设计
- 数据传输模式更为简单,很多地方也更为高效
- 并不是不能做批处理,它也可以来做微批处理,来提高吞吐。
Spark Streaming:微批处理
- 将RDD做的很小来用小的批处理来接近流式处理
- 基于内存和DAG可以把处理任务做的很快。
MapReduce:
- Storm:进程、线程常驻内存运行,数据不进入磁盘,数据通过网络传递。
- MapReduce:为TB、PB级别数据设计的批处理计算框架。
三、Storm相关概念
1、拓扑(Topologies)
一个Storm拓扑打包了一个实时处理程序的逻辑。一个Storm拓扑跟一个MapReduce的任务(job)是类似的。主要区别是MapReduce任务最终会结束,而拓扑会一直运行(当然直到你杀死它)。一个拓扑是一个通过流分组(stream grouping)把Spout和Bolt连接到一起的拓扑结构。图的每条边代表一个Bolt订阅了其他Spout或者Bolt的输出流。一个拓扑就是一个复杂的多阶段的流计算。
资源
- TopologyBuilder: 使用这个类来在Java中创建拓扑
- 在生产集群中运行拓扑
- 本地模式: 通过阅读这篇可以学习到如何在本地模式下进行拓扑的开发和测试
2、数据流(Streams)
从Spout中源源不断传递数据给Bolt、以及上一个Bolt传递数据给下一个Bolt,所形成的这些数据通道即叫做Stream Stream声明时需给其指定一个Id(默认为Default) 实际开发场景中,多使用单一数据流,此时不需要单独指定StreamId
资源:
- 元组:流由元组组成
- OutputFieldsDeclarer:用于声明流及其模式
- 序列化:有关Storm动态键入元组和声明自定义序列化的信息
3、数据源(Spouts)
拓扑中数据流的来源。一般会从指定外部的数据源读取元组(Tuple)发送到拓扑(Topology)中 一个Spout可以发送多个数据流(Stream) 可先通过OutputFieldsDeclarer中的declare方法声明定义的不同数据流,发送数据时通过SpoutOutputCollector中的emit方法指定数据流Id(streamId)参数将数据发送出去 Spout中最核心的方法是nextTuple,该方法会被Storm线程不断调用、主动从数据源拉取数据,再通过emit方法将数据生成元组(Tuple)发送给之后的Bolt计算
资源:
- IRichSpout:这是spouts必须实现的接口。
- 保证消息处理
4、数据流处理组件(Bolt)
拓扑中数据处理均有Bolt完成。对于简单的任务或者数据流转换,单个Bolt可以简单实现;更加复杂场景往往需要多个Bolt分多个步骤完成 一个Bolt可以发送多个数据流(Stream) 可先通过OutputFieldsDeclarer中的declare方法声明定义的不同数据流,发送数据时通过SpoutOutputCollector中的emit方法指定数据流Id(streamId)参数将数据发送出去
Bolt中最核心的方法是execute方法,该方法负责接收到一个元组(Tuple)数据、真正实现核心的业务逻辑
资源:
- IRichBolt:这是螺栓的通用接口。
- IBasicBolt:这是一个方便的界面,用于定义执行过滤或简单功能的螺栓。
- OutputCollector:bolt使用此类的实例将元组发送到其输出流
- 保证消息处理
5、流分组(Stream groupings)
数据分发策略,Storm中有八个内置流分组,您可以通过实现CustomStreamGrouping接口来实现自定义流分组:
- 随机分组:元组随机分布在螺栓的任务中,使得每个螺栓都能保证获得相同数量的元组。
- 字段分组:流按分组中指定的字段进行分区。例如,如果流按“user-id”字段分组,则具有相同“user-id”的元组将始终执行相同的任务,但具有不同“user-id”的元组可能会执行不同的任务。
- 部分密钥分组:流按分组中指定的字段进行分区,如字段分组,但在两个下游螺栓之间进行负载平衡,这可在传入数据偏斜时提供更好的资源利用率。本文对其工作原理及其提供的优势进行了很好的解释。
- 所有分组:流被复制到所有bolt任务中。小心使用此分组。
- 全局分组:整个流转到了一个bolt的任务。具体来说,它转到id最低的任务。
- 无分组:此分组指定您不关心流的分组方式。目前,没有任何分组相当于随机分组。最终,Storm会按下没有分组的螺栓,在与他们订购的螺栓或喷口相同的螺纹中执行(如果可能的话)。
- 直接分组:这是一种特殊的分组。以这种方式分组的流意味着元组的生产者决定消费者的哪个任务将接收该元组。直接分组只能在已声明为直接流的流上声明。发送到直接流的元组必须使用[emitDirect]之一(javadocs / org / apache / storm / task / OutputCollector.html#emitDirect(int,int,java.util.List)方法发出。一个bolt可以得到通过使用提供的TopologyContext或跟踪OutputCollector中的
emit
方法输出(返回元组发送到的任务ID)来消费者的任务ID。 - 本地或随机分组:如果目标螺栓在同一工作进程中有一个或多个任务,则元组将被洗牌到只有那些进程内任务。否则,这就像一个普通的shuffle分组。
资源:
- TopologyBuilder:使用此类来定义拓扑
-
InputDeclarer:
setBolt
调用on 时返回此对象TopologyBuilder
,用于声明bolt的输入流以及如何对这些流进行分组
6、可靠性(Reliability)
Storm保证每个spout元组都将由拓扑完全处理。它通过跟踪每个spout元组触发的元组树并确定元组树何时成功完成来实现。每个拓扑都有一个与其关联的“消息超时”。如果Storm未能检测到在该超时内已完成一个spout元组,则它会使元组失败并在以后重播。
要利用Storm的可靠性功能,必须在创建元组树中的新边时告诉Storm,并在完成处理单个元组时告诉Storm。这些是使用OutputCollector对象完成的,该对象用于发出元组。锚定在emit
方法中完成,并声明您已使用该ack
方法完成元组。
在保证消息处理中更详细地解释了这一点。
7、任务(Tasks)
每个喷口或螺栓在整个集群中执行任意数量的任务。每个任务对应一个执行线程,流分组定义如何将元组从一组任务发送到另一组任务。您可以在TopologyBuilder的setSpout
and和setBolt
方法中为每个喷口或螺栓设置并行度。
8、工作节点(worker)
拓扑在一个或多个工作进程中执行。每个工作进程都是物理JVM,并执行拓扑的所有任务的子集。例如,如果拓扑的组合并行度为300且分配了50个工作线程,则每个工作线程将执行6个任务(作为工作线程中的线程)。Storm试图在所有工作人员之间平均分配任务。
资源:
- Config.TOPOLOGY_WORKERS:此配置设置为执行拓扑而分配的工作器数
9、元组(Tuple)
Stream中最小数据组成单元
四、Storm的计算模型
Storm实现了一个数据流(data flow)的模型,在这个模型中数据持续不断地流经一个由很多转换实体构成的网络。一个数据流的抽象叫做流(stream),流是无限的元组(Tuple)序列。元组就像一个可以表示标准数据类型(例如int,float和byte数组)和用户自定义类型(需要额外序列化代码的)的数据结构。每个流由一个唯一的ID来标示的,这个ID可以用来构建拓扑中各个组件的数据源。
如下图所示,其中的水龙头代表了数据流的来源,一旦水龙头打开,数据就会源源不断地流经Bolt而被处理。图中有三个流,用不同的颜色来表示,每个数据流中流动的是元组(Tuple),它承载了具体的数据。元组通过流经不同的转换实体而被处理。
Storm对数据输入的来源和输出数据的去向没有做任何限制。像Hadoop,是需要把数据放到自己的文件系统HDFS里的。在Storm里,可以使用任意来源的数据输入和任意的数据输出,只要你实现对应的代码来获取/写入这些数据就可以。典型场景下,输入/输出数据来是基于类似Kafka或者ActiveMQ这样的消息队列,但是数据库,文件系统或者web服务也都是可以的。
五、代码示例
5.1、Storm 数据累加
5.1.1、定义Spout类
package com.lxk.storm.first;
import java.util.List;
import java.util.Map;
import backtype.storm.spout.SpoutOutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichSpout;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Values;
import ch.qos.logback.classic.pattern.Util;
public class NumSumSpout extends BaseRichSpout{
private Map conf;
private TopologyContext context;
private SpoutOutputCollector collector;
int i= 0;
@Override
public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
this.conf =conf;
this.context=context;
this.collector=collector;
}
/**
* 1+2+3+4+...
*/
@Override
public void nextTuple() {
i++;
List tuple =new Values(i);
this.collector.emit(tuple);
System.err.println("spout------------"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declare) {
declare.declare(new Fields("num"));
}
}
5.1.2、定义Bolt类
package com.lxk.storm.first;
import java.util.Map;
import backtype.storm.spout.SpoutOutputCollector;
import backtype.storm.task.OutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichBolt;
import backtype.storm.tuple.Tuple;
public class NumSumBolt extends BaseRichBolt {
private Map conf;
private TopologyContext context;
private OutputCollector collector;
int sum = 0;
@Override
public void prepare(Map conf, TopologyContext context, OutputCollector collector) {
this.conf = conf;
this.context = context;
this.collector = collector;
}
@Override
public void execute(Tuple input) {
// 获取数据
int i = input.getIntegerByField("num");
// 求和累加
sum += i;
System.out.println("sum*************" + sum);
}
@Override
public void declareOutputFields(OutputFieldsDeclarer arg0) {
// 已经是结果,不需要继续定义
}
}
5.1.3、建造者模式创建测试类
package com.lxk.storm.first;
import backtype.storm.Config;
import backtype.storm.LocalCluster;
import backtype.storm.topology.TopologyBuilder;
public class TopologyTest {
public static void main(String[] args) {
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("numSumSpout", new NumSumSpout(), 1);
builder.setBolt("numSumBolt", new NumSumBolt(), 1).shuffleGrouping("numSumSpout");
// 放入本地集群
LocalCluster localCluster = new LocalCluster();
localCluster.submitTopology("mytopology", new Config(), builder.createTopology());
}
}
结果:
5.2、经典示例wordCount
5.2.1、定义Spout类--读取文件,以行发送
package com.lxk.storm.second;
import java.util.List;
import java.util.Map;
import java.util.Random;
import backtype.storm.spout.SpoutOutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichSpout;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Values;
import backtype.storm.utils.Utils;
public class WordCountSpout extends BaseRichSpout {
private SpoutOutputCollector collector;
String[] text = { "hadoop spark storm", "hadoop hive hbase spark", "flume kafka storm" };
Random r = new Random();
@Override
public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
this.collector = collector;
}
/**
* 发送行数据给bolt
*/
@Override
public void nextTuple() {
List line = new Values(text[r.nextInt(text.length)]);
this.collector.emit(line);
System.err.println("spout------------" + line);
Utils.sleep(1000);
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declare) {
declare.declare(new Fields("line"));
}
}
5.2.2、定义Bolt类--切分单子为数组并发送
package com.lxk.storm.second;
import java.util.List;
import java.util.Map;
import backtype.storm.task.OutputCollector;
import backtype.storm.task.TopologyContext;
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;
public class WordSplitBolt extends BaseRichBolt {
private OutputCollector collector;
@Override
public void prepare(Map conf, TopologyContext context, OutputCollector collector) {
this.collector = collector;
}
@Override
public void execute(Tuple input) {
// 获取数据
String line = input.getString(0);
// 切分成单词数组
String[] words = line.split(" ");
// 发送单词
for (String word : words) {
List w = new Values(word);
this.collector.emit(w);
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("wc"));
}
}
5.2.3、定义Bolt类--单词计数reducebykey
package com.lxk.storm.second;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import backtype.storm.task.OutputCollector;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.topology.base.BaseRichBolt;
import backtype.storm.tuple.Tuple;
import backtype.storm.tuple.Values;
public class WordCountBolt extends BaseRichBolt {
Map<String,Integer> map = new HashMap<>();
int count =1;
@Override
public void prepare(Map conf, TopologyContext context, OutputCollector collector) {
}
@Override
public void execute(Tuple input) {
// 获取数据
String word = input.getStringByField("wc");
// 单词数组计数
if(map.containsKey(word)){
count = map.get(word)+1;
}
map.put(word, count);
System.out.println(word+"------------"+count);
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// 已经是结果,不需要继续定义
}
}
5.2.4、建造者模式创建测试类
package com.lxk.storm.second;
import backtype.storm.Config;
import backtype.storm.LocalCluster;
import backtype.storm.topology.TopologyBuilder;
public class TopologyTest {
public static void main(String[] args) {
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("wcSpout", new WordCountSpout());
// 设置三个线程,但是最后一个Bolt需要聚合只能用一个线程
builder.setBolt("wcSplitBolt", new WordSplitBolt(), 3).shuffleGrouping("wcSpout");
builder.setBolt("wcBolt", new WordCountBolt()).shuffleGrouping("wcSplitBolt");
// 放入本地集群
LocalCluster localCluster = new LocalCluster();
localCluster.submitTopology("wc", new Config(), builder.createTopology());
}
}
结果: