什么是Apache Storm
Apache Storm是一个分布式实时大数据处理系统,通过zookeeper来管理分布式集群。
Apache Storm 核心概念
Storm读取实时数据流,并传递给处理单元,最终输出处理后的数据。
下图描述了storm的处理数据的主要结构。
元组(Tuple) : 元组是Storm提供的一个轻量级的数据格式,可以用来包装你需要实际处理的数据。元组是一次消息传递的基本单元。一个元组是一个命名的值列表,其中的每个值都可以是任意类型的。元组是动态地进行类型转化的(字段的类型不需要事先声明)。在Storm中编程时,就是在操作和转换由元组组成的流。通常,元组包含整数,字节,字符串,浮点数,布尔值和字节数组等类型。要想在元组中使用自定义类型,就需要实现自己的序列化方式。
流(Stream) :一个流由无限的元组序列组成,这些元组会被分布式并行地创建和处理。通过流中元组包含的字段名称来定义这个流。
每个流声明时都被赋予了一个ID。只有一个流的Spout和Bolt非常常见,所以OutputFieldsDeclarer提供了不需要指定ID来声明一个流的函数(Spout和Bolt都需要声明输出的流)。这种情况下,流的ID是默认的“default”。
Spouts :Spout(喷嘴)是Storm中流的来源。通常Spout从外部数据源,如消息队列中读取元组数据并吐到拓扑里。Spout可以是可靠的(reliable)或者不可靠(unreliable)的。可靠的Spout能够在一个元组被Storm处理失败时重新进行处理,而非可靠的Spout只是吐数据到拓扑里,不关心处理成功还是失败了。
Spout可以一次给多个流吐数据。此时需要通过OutputFieldsDeclarer的declareStream函数来声明多个流并在调用SpoutOutputCollector提供的emit方法时指定元组吐给哪个流。
Spout中最主要的函数是nextTuple,Storm框架会不断调用它去做元组的轮询。如果没有新的元组过来,就直接返回,否则把新元组吐到拓扑里。nextTuple必须是非阻塞的,因为Storm在同一个线程里执行Spout的函数。
Spout中另外两个主要的函数是ack和fail。当Storm检测到一个从Spout吐出的元组在拓扑中成功处理完时调用ack,没有成功处理完时调用fail。只有可靠型的Spout会调用ack和fail函数。
Bolts :storm是一种分布式实时计算系统,而storm topology中,所有的实时计算的业务逻辑都是定义在bolt中的。bolt中可以做任何计算逻辑,比如过滤、执行自定义的函数、聚合、join、访问数据库,等等。简而言之,bolt实际上就是我们实现或者继承了storm提供的接口或基类,自己开发的类。
接着看一个实例,如何通过Apache Storm来构建Twitter Analysis。结构如下图所示。
通过Twitter Streaming API为Twitter Analysis提供输入数据。Spout通过Twitter Streaming API读取数据,并以tuple流的形式输出。随后tuple将转发给bolt,bolt将会对tuple进行处理。
Topology(拓扑):storm topology和mapreduce job是有些类似的。唯一关键的区别就在于,mapreduce job是肯定会结束运行的;但是storm topology是永远会运行的,除非你自己手动杀了它。
使用storm开发的实时计算应用程序,所有的计算逻辑都在topology中。一个topology,其实就是逻辑上的计算流向图,由spout和bolt组成。一个topology可以包含一个或者多个spout和bolt。而spout和bolt,就是topology这个计算流向图种的一个一个的计算节点,其中包含了我们自己编写的计算代码。spout和bolt之间的关系和联系,其实就定义了实时计算的数据流向。可以想象成,数据从外部读入spout,然后传输到后面一个一个的bolt;而bolt之间的数据流向,可能是交叉层叠的,看起来整个topology就像一个DAG(有向无环图)一样。 简而言之,topology,就是逻辑上的实时计算拓扑图。
Tasks(任务):Spout 和 bolt是topology中的最小逻辑单元。topology是通过一个spout和一组bolt构建。逻辑单元需要按特定的顺序来执行。Storm所执行的每个spout和bolt称为task。简而言之,spout或bolt的执行称为task。每个spout和bolt都可以有多个不同的实例运行在不同的线程中。(每一个task对应到一个线程)。
Workers:toplogy是在分布式环境下,多个worker节点上运行。storm将任务均匀分配在所有worker节点上。work节点的作用是监听任务(jobs),当有新任务来时,启动或停止任务的处理。每个worker是一个物理JVM并且执行整个topology的一部分。
Stream Grouping:流分组,是拓扑定义中的一部分,为每个bolt指定应该接收哪个流作为输入。流分组定义流/元组如何在bolt的任务之间进行分发。
Apache Storm集群结构
接着看一下storm集群是如何设计的,以及它的内部结构,下图描绘了集群的结构。
storm有两类节点,Nimbus(主节点)和Supervisor(工作节点)。Nimbus主要的任务是运行storm拓扑。Nimbus分析拓扑结构,并收集任务来执行。随后,Nimbus将分配任务给可用的Supervisor。一个supervisor有一个或多个工作进程。Supervisor分派任务给工作进程。worker process会按需生成多个executor并且执行任务(task)。Nimbus和supervisors间通过storm内部的分布式消息系统来进行通信。
Nimbus : Nimbus负责在集群中分发代码,对节点分配任务,并监视主机故障。
Supervisor:每个工作节点运行一个称为Supervisor的守护进程。Supervisor监听其主机上已经分配的主机的作业,启动和停止Nimbus已经分配的工作进程。
Work process : worker process 会执行与具体 topology相关的任务。worker process通过创建executor并通过executor来执行具体的任务。一个worker process会有多个executor。
Executor:在Storm0.8以后,Task不再与物理线程对应,同一个Spout/Bolt的Task可能会共享一个物理线程,该线程称为执行器(Executor)。
Task:task执行实际的数据处理,可以是spout或bolt。
ZooKeeper framework:Zookeeper是完成Supervisor和Nimbus之间协调的服务。Nimbus的守护进程和Supervisors守护进程是无法连接和无状态的;所有的状态维持在Zookeeper中 或保存在本地磁盘上。这意味着你可以 kill -9 Nimbus或Supervisors 进程,所以他们不需要做备份。这种设计导致Storm集群具有令人难以置信的稳定性。而应用程序实现实时的逻辑则被封装进Storm中的“topology”。topology则是一组由Spouts(数据源)和Bolts(数据操作)通过Stream Groupings进行连接的图。
Apache storm 工作流程
storm集群有一个nimbus及一个或多个supervisor。此外还需要zookeeper来协调nimbus与supervisor。
- nimbus首先会等待 topoloy提交。
- 一旦topology被提交后,nimbus会处理topology并收集所有要执行的task及确定执行顺序。
- nimbus将这些task平均分配到所有可用的supervisor上。supervisor通过发送心跳包的形式通知nimbus他们还“存活”。
- 如果supervisor不可用,那么nimbus将分配任务(task)给其它的supervisor。
- nimubs如果挂掉,supervisor会继续处理已分派的任务(task)。
- 一旦任务执行完毕,supervisor将会等待新的任务到来。于此同时,挂掉的nimbus会由服务监控工具来重新启动。重启后,nimbus继续工作。类似的supervisor也可以自动的重启。由于重启机制,storm会保证所有任务至少被处理一次。
- 所有的topology处理完毕后,nimbus将等待新的topology,同样supervisor也会等待新的任务。
Storm集群中默认有两种模式:
本地模式 : 这种模式用于开发,测试与调试。在这种模式下我们可以通过调整参数来看到topoloy如何在不同的storm配置环境下运行。在本地模式下,storm topology将运行在本地机器的一个JVM上。
生产模式:这种模式下,我们提交我们的topology给storm集群,集群由运行在不同机器上的多个进程组成。
分布式消息系统
storm处理的实时数据来自消息系统。外部分布式消息系统会提交数据来进行实时计算。spout将会读取消息系统的数据,并将这些数据转换为tuple并输入到storm中。
storm内部广泛使用thrift协议来进行通信与数据定义。storm topology是thrift struct。storm nimbus是一个thrift 服务。
环境搭建
安装Java
安装zookeeper
安装storm
- 官网下载storm
- 修改配置文件
当前storm发行版包含一个配置storm守护进行的配置文件,其位置在 conf/storm.yaml。将下列信息添加到配置文件中。
$ vi conf/storm.yaml storm.zookeeper.servers: - "localhost" storm.local.dir: “/path/to/storm/data(any path)” nimbus.host: "localhost" supervisor.slots.ports:
- 6700
- 6701
- 6702
- 6703
启动nimbus
$ bin/storm nimbus
启动supervisor
$ bin/storm supervisor
启动UI
$ bin/storm ui
执行命令后访问http://localhost:8080将会看到集群信息。
一个实例
电话拨打日志分析:电话拨打及其时间将作为storm的输入,storm将会处理这些数据,并按拨打者和接听者进行分组并记录拨打次数。
创建spout:spout用来生产数据。一个spout需要实现IRichSpout接口。
在这个例子中,我们需要收集电话拨打日志详情。这包括: 拨号人电话号、 接听人电话号、 拨打电话时间。
因为我们没有这些日志信息,所以需要生成虚拟拨打日志。下面的代码用来生成日志。
引入依赖
<dependency>
<groupId>org.apache.storm</groupId>
<artifactId>storm-core</artifactId>
<version>1.1.0</version>
<!--<scope>provided</scope>-->
</dependency>
FakeCallLogReaderSpout
package storm;
/**
* Created by on 2017/7/9.
*/
import java.util.*;
//import storm tuple packages
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Values;
//import Spout interface packages
import org.apache.storm.topology.IRichSpout;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.spout.SpoutOutputCollector;
import org.apache.storm.task.TopologyContext;
//Create a class FakeLogReaderSpout which implement IRichSpout interface
// to access functionalities
public class FakeCallLogReaderSpout implements IRichSpout {
//Create instance for SpoutOutputCollector which passes tuples to bolt.
private SpoutOutputCollector collector;
private boolean completed = false;
//Create instance for TopologyContext which contains topology data.
private TopologyContext context;
//Create instance for Random class.
private Random randomGenerator = new Random();
private Integer idx = 0;
@Override
public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
this.context = context;
this.collector = collector;
}
@Override
public void nextTuple() {
if(this.idx <= 1000) {
List<String> mobileNumbers = new ArrayList<String>();
mobileNumbers.add("1234123401");
mobileNumbers.add("1234123402");
mobileNumbers.add("1234123403");
mobileNumbers.add("1234123404");
Integer localIdx = 0;
while(localIdx++ < 100 && this.idx++ < 1000) {
String fromMobileNumber = mobileNumbers.get(randomGenerator.nextInt(4));
String toMobileNumber = mobileNumbers.get(randomGenerator.nextInt(4));
while(fromMobileNumber == toMobileNumber) {
toMobileNumber = mobileNumbers.get(randomGenerator.nextInt(4));
}
Integer duration = randomGenerator.nextInt(60);
this.collector.emit(new Values(fromMobileNumber, toMobileNumber, duration));
}
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("from", "to", "duration"));
}
//Override all the interface methods
@Override
public void close() {}
public boolean isDistributed() {
return false;
}
@Override
public void activate() {}
@Override
public void deactivate() {}
@Override
public void ack(Object msgId) {}
@Override
public void fail(Object msgId) {}
@Override
public Map<String, Object> getComponentConfiguration() {
return null;
}
}
创建bolt:Call log creator bolt接收调用日志. 拨打日志由拨打号码,接收号码,拨打时间组成。这个bolt仅仅是将信息组合在一起。
Call log counter bolt 接收拨打信息及拨打时常并作为tuple。这个类初在prepare方法中始化一个Map。在execute方法中更新拨打计数。
CallLogCreatorBolt
import java.util.Map;
import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.IRichBolt;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
import org.apache.storm.tuple.Values;
//import Storm IRichBolt package
//Create a class CallLogCreatorBolt which implement IRichBolt interface
public class CallLogCreatorBolt implements IRichBolt {
//Create instance for OutputCollector which collects and emits tuples to produce output
private OutputCollector collector;
@Override
public void prepare(Map conf, TopologyContext context, OutputCollector collector) {
this.collector = collector;
}
@Override
public void execute(Tuple tuple) {
String from = tuple.getString(0);
String to = tuple.getString(1);
Integer duration = tuple.getInteger(2);
collector.emit(new Values(from + " - " + to, duration));
}
@Override
public void cleanup() {}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("call", "duration"));
}
@Override
public Map<String, Object> getComponentConfiguration() {
return null;
}
}
CallLogCounterBolt
import java.util.HashMap;
import java.util.Map;
import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.IRichBolt;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
public class CallLogCounterBolt implements IRichBolt {
Map<String, Integer> counterMap;
private OutputCollector collector;
@Override
public void prepare(Map conf, TopologyContext context, OutputCollector collector) {
this.counterMap = new HashMap<String, Integer>();
this.collector = collector;
}
@Override
public void execute(Tuple tuple) {
String call = tuple.getString(0);
Integer duration = tuple.getInteger(1);
if(!counterMap.containsKey(call)){
counterMap.put(call, 1);
}else{
Integer c = counterMap.get(call) + 1;
counterMap.put(call, c);
}
collector.ack(tuple);
}
@Override
public void cleanup() {
for(Map.Entry<String, Integer> entry:counterMap.entrySet()){
System.out.println(entry.getKey()+" : " + entry.getValue());
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("call"));
}
@Override
public Map<String, Object> getComponentConfiguration() {
return null;
}
}
Creating Topology 创建拓扑
Storm topology是一个Thrift structure。TopologyBuilder类提供了一些方法可以创建复杂的Topology。TopologyBuilder类有setSpout,setBolt来设置spout和bolt。最后createTopology来创建topology。
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("call-log-reader-spout", new FakeCallLogReaderSpout()); builder.setBolt("call-log-creator-bolt", new CallLogCreatorBolt()) .shuffleGrouping("call-log-reader-spout");
builder.setBolt("call-log-counter-bolt", new CallLogCounterBolt()) .fieldsGrouping("call-log-creator-bolt", new Fields("call"));
shuffleGrouping 和 fieldsGrouping 方法 用来设置spout和bolt的流分组。
Local Cluster 本地集群
我们通过LocalCluster类的submitTopology方法来提交topology。submitTopology方法的一个参数是Config类实例。Config类用来在提交topology前进行一些配置。这个配置信息将会与集群配置在运行时合并,并通过prepare方法发送给所有的task(spout和bolt)。一旦topology提交到集群,我们会等待集群完成对topology的计算,随后使用LocalCluster的shutdown方法关闭集群。
import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.topology.TopologyBuilder;
import org.apache.storm.tuple.Fields;
//import storm configuration packages
//Create main class LogAnalyserStorm submit topology.
public class LogAnalyserStorm {
public static void main(String[] args) throws Exception{
//Create Config instance for cluster configuration
Config config = new Config();
config.setDebug(true);
//
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("call-log-reader-spout", new FakeCallLogReaderSpout());
builder.setBolt("call-log-creator-bolt", new CallLogCreatorBolt())
.shuffleGrouping("call-log-reader-spout");
builder.setBolt("call-log-counter-bolt", new CallLogCounterBolt())
.fieldsGrouping("call-log-creator-bolt", new Fields("call"));
LocalCluster cluster = new LocalCluster();
System.out.println("after localCluster");
cluster.submitTopology("LogAnalyserStorm", config, builder.createTopology());
System.out.println("after submitTopology");
//Stop the topology
Thread.sleep(15000);
cluster.shutdown();
}
}