BigData-Notes/notes/Storm编程模型详解.md
2019-04-20 12:36:57 +08:00

18 KiB
Raw Blame History

Storm 编程模型

一、简介
二、IComponent接口
三、Spout
    3.1 ISpout接口
    3.2 BaseRichSpout抽象类
四、Bolt
    4.1 IBolt 接口
    4.2 BaseRichBolt抽象类
五、词频统计案例
六、提交到服务器集群运行
七、关于项目打包的扩展说明

一、简介

下图为Strom的运行流程图也是storm的编程模型图在storm 进行流处理时我们需要自定义实现自己的spout数据源和bolt处理单元并通过TopologyBuilder将它们之间进行关联,定义好数据处理的流程。

下面小结分别介绍如何按照storm内置接口分别实现spout和bolt然后将其进行关联最后将其提交到本地和服务器进行运行。

二、IComponent接口

IComponent接口定义了Topology中所有组件spout/bolt的公共方法我们实现spout或bolt都必须直接或者间接实现这个接口。

public interface IComponent extends Serializable {

    /**
     * 声明此拓扑的所有流的输出模式。
     * @param declarer这用于声明输出流id输出字段以及每个输出流是否是直接流direct stream
     */
    void declareOutputFields(OutputFieldsDeclarer declarer);

    /**
     * 声明此组件的配置。
     *
     */
    Map<String, Object> getComponentConfiguration();

}

三、Spout

3.1 ISpout接口

实现自定义的spout需要实现ISpout其定义了spout的所有可用方法

public interface ISpout extends Serializable {
    /**
     * 组件初始化时候被调用
     *
     * @param conf ISpout的配置
     * @param context 应用上下文可以通过其获取任务ID和组件ID输入和输出信息等。
     * @param collector  用来发送spout中的tuples它是线程安全的建议保存为此spout对象的实例变量
     */
    void open(Map conf, TopologyContext context, SpoutOutputCollector collector);

    /**
     * ISpout将要被关闭的时候调用。但是其不一定会被执行如果在集群环境中通过kill -9 杀死进程时其就无法被执行。
     */
    void close();
    
    /**
     * 当ISpout从停用状态激活时被调用
     */
    void activate();
    
    /**
     * 当ISpout停用时候被调用
     */
    void deactivate();

    /**
     * 这是一个核心方法主要通过在此方法中调用collector将tuples发送给下一个接收器这个方法必须是非阻塞的。              
     * nextTuple/ack/fail/是在同一个线程中执行的所以不用考虑线程安全方面。当没有tuples发出时应该让nextTuple
     * 休眠(sleep)一下以免浪费CPU。
     */
    void nextTuple();

    /**
     * 通过msgId进行tuples处理成功的确认被确认后的tuples不会再次被发送
     */
    void ack(Object msgId);

    /**
     * 通过msgId进行tuples处理失败的确认被确认后的tuples会再次被发送进行处理
     */
    void fail(Object msgId);
}

3.2 BaseRichSpout抽象类

通常情况下我们实现自定义的Spout时不会直接去实现ISpout接口,而是继承BaseRichSpoutBaseRichSpout继承自BaseCompont,同时实现了IRichSpout接口。

IRichSpout接口继承自ISpoutIComponent,自身并没有定义任何方法。

public interface IRichSpout extends ISpout, IComponent {

}

BaseComponent 抽象类也仅仅是空实现了IComponentgetComponentConfiguration方法。

public abstract class BaseComponent implements IComponent {
    @Override
    public Map<String, Object> getComponentConfiguration() {
        return null;
    }    
}

BaseRichSpout通过继承自BaseCompont,同时实现了IRichSpout接口,并且空实现了其中部分方法。

public abstract class BaseRichSpout extends BaseComponent implements IRichSpout {
    @Override
    public void close() {}

    @Override
    public void activate() {}

    @Override
    public void deactivate() {}

    @Override
    public void ack(Object msgId) {}

    @Override
    public void fail(Object msgId) {}
}

通过这样的设计,我们在继承BaseRichSpout实现自己的spout时就只需要实现三个必须的方法

  • open 来源于ISpout可以通过此方法获取用来发送tuples的SpoutOutputCollector
  • nextTuple 来源于ISpout必须在此方法内部才能调用SpoutOutputCollector发送tuple
  • declareOutputFields 来源于IComponent通过此方法声明发送的tuple的名称这样下一个组件才能知道如何接受数据。

四、Bolt

通过上小结我们已经了解了storm如何对spout接口进行设计的bolt接口的设计也是一样的。

4.1 IBolt 接口

 /**
  * 在客户端计算机上创建的IBolt对象。会被被序列化到topology中使用Java序列化,并提交给集群的主机Nimbus  * Nimbus启动workers反序列化对象调用prepare然后开始处理tuples。
 */

public interface IBolt extends Serializable {
    /**
     * 组件初始化时候被调用
     *
     * @param conf storm中定义的此bolt的配置
     * @param context 应用上下文可以通过其获取任务ID和组件ID输入和输出信息等。
     * @param collector  用来发送spout中的tuples它是线程安全的建议保存为此spout对象的实例变量
     */
    void prepare(Map stormConf, TopologyContext context, OutputCollector collector);

    /**
     * 处理单个tuple输入。
     * 
     * @param Tuple对象包含关于它的元数据如来自哪个组件/流/任务)
     */
    void execute(Tuple input);

    /**
     * IBolt将要被关闭的时候调用。但是其不一定会被执行如果在集群环境中通过kill -9 杀死进程时其就无法被执行。
     */
    void cleanup();

4.2 BaseRichBolt抽象类

同样的在实现我们自己的bolt时我们也通常是继承BaseRichBolt抽象类来实现。BaseRichBolt继承自BaseComponent抽象类,并实现了IRichBolt接口。

IRichBolt接口继承自IBoltIComponent,自身并没有定义任何方法。

public interface IRichBolt extends IBolt, IComponent {

}

通过这样的设计,我们在继承BaseRichBolt实现自己的bolt时就只需要实现三个必须的方法

  • prepare 来源于IBolt可以通过此方法获取用来发送tuples的OutputCollector
  • execute来源于IBolt处理tuple和发送处理完成的tuple
  • declareOutputFields 来源于IComponent通过此方法声明发送的tuple的名称这样下一个组件才能知道如何接受数据。

五、词频统计案例

5.1 案例简介

使用模拟数据进行词频统计。这里我们使用自定义的DataSourceSpout产生模拟数据。实际生产环境中通常是通过日志收集引擎如Flume、logstash等将收集到的数据发送到kafka指定的topic通过storm内置的KafkaSpout 来监听该topic并获取数据。

案例源码下载地址:storm-word-count

5.2 代码实现

1. 项目依赖

<dependency>
    <groupId>org.apache.storm</groupId>
    <artifactId>storm-core</artifactId>
    <version>1.2.2</version>
</dependency>

2. DataSourceSpout

public class DataSourceSpout extends BaseRichSpout {

    private List<String> list = Arrays.asList("Spark", "Hadoop", "HBase", "Storm", "Flink", "Hive");

    private SpoutOutputCollector spoutOutputCollector;

    @Override
    public void open(Map map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {
        this.spoutOutputCollector = spoutOutputCollector;
    }

    @Override
    public void nextTuple() {
        // 模拟产生数据
        String lineData = productData();
        spoutOutputCollector.emit(new Values(lineData));
        Utils.sleep(1000);
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
        outputFieldsDeclarer.declare(new Fields("line"));
    }


    /**
     * 模拟数据
     */
    private String productData() {
        Collections.shuffle(list);
        Random random = new Random();
        int endIndex = random.nextInt(list.size()) % (list.size()) + 1;
        return StringUtils.join(list.toArray(), "\t", 0, endIndex);
    }

}

上面类使用productData方法来产生模拟数据,产生数据的格式如下:

Spark	HBase
Hive	Flink	Storm	Hadoop	HBase	Spark
Flink
HBase	Storm
HBase	Hadoop	Hive	Flink
HBase	Flink	Hive	Storm
Hive	Flink	Hadoop
HBase	Hive
Hadoop	Spark	HBase	Storm

3. SplitBolt

public class SplitBolt extends BaseRichBolt {

    private OutputCollector collector;

    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
        this.collector=collector;
    }

    @Override
    public void execute(Tuple input) {
        String line = input.getStringByField("line");
        String[] words = line.split("\t");
        for (String word : words) {
            collector.emit(new Values(word));
        }
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("word"));
    }
}

4. CountBolt

public class CountBolt extends BaseRichBolt {

    private Map<String, Integer> counts = new HashMap<>();

    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {

    }

    @Override
    public void execute(Tuple input) {
        String word = input.getStringByField("word");
        Integer count = counts.get(word);
        if (count == null) {
            count = 0;
        }
        count++;
        counts.put(word, count);
        // 输出
        System.out.print("当前实时统计结果:");
        counts.forEach((key, value) -> System.out.print(key + ":" + value + "; "));
        System.out.println();
    }

    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {

    }
}

5. LocalWordCountApp

通过TopologyBuilder将上面定义好的组件进行串联形成 Topology并提交到本地集群LocalCluster运行。在通常开发中可用本地集群进行测试完成后再提交到服务器集群运行。

public class LocalWordCountApp {

    public static void main(String[] args) {
        TopologyBuilder builder = new TopologyBuilder();
        
        builder.setSpout("DataSourceSpout", new DataSourceSpout());
        
        // 指明将 DataSourceSpout 的数据发送到 SplitBolt 中处理
        builder.setBolt("SplitBolt", new SplitBolt()).shuffleGrouping("DataSourceSpout");
        
        //  指明将 SplitBolt 的数据发送到 CountBolt 中 处理
        builder.setBolt("CountBolt", new CountBolt()).shuffleGrouping("SplitBolt");

        // 创建本地集群用于测试 这种模式不需要本机安装storm,直接运行该Main方法即可
        LocalCluster cluster = new LocalCluster();
        cluster.submitTopology("LocalWordCountApp",
                new Config(), builder.createTopology());
    }

}

6. 运行结果

启动WordCountApp的main方法即可运行采用本地模式storm会自动在本地搭建一个集群所以启动的过程会稍慢一点启动成功后即可看到输出日志。

六、提交到服务器集群运行

6.1 代码更改

提交到服务器的代码和本地代码略有不同,提交到服务器集群时需要使用StormSubmitter进行提交。主要代码如下。

为了结构清晰这里新建ClusterWordCountApp类来演示集群提交。实际开发中可以将两种模式的代码写在同一个类中通过外部传参来决定启动何种模式。

public class ClusterWordCountApp {

    public static void main(String[] args) {
        TopologyBuilder builder = new TopologyBuilder();
        
        builder.setSpout("DataSourceSpout", new DataSourceSpout());
        
        // 指明将 DataSourceSpout 的数据发送到 SplitBolt 中处理
        builder.setBolt("SplitBolt", new SplitBolt()).shuffleGrouping("DataSourceSpout");
        
        //  指明将 SplitBolt 的数据发送到 CountBolt 中 处理
        builder.setBolt("CountBolt", new CountBolt()).shuffleGrouping("SplitBolt");

        // 使用StormSubmitter提交Topology到服务器集群
        try {
            StormSubmitter.submitTopology("ClusterWordCountApp",  new Config(), builder.createTopology());
        } catch (AlreadyAliveException | InvalidTopologyException | AuthorizationException e) {
            e.printStackTrace();
        }
    }

}

6.2 打包上传

打包后上传到服务器任意位置,这里我打包后的名称为storm-word-count-1.0.jar

# mvn clean package -Dmaven.test.skip=true

6.3 提交Topology

使用以下命令提交Topology到集群

# 命令格式storm  jar  jar包位置  主类的全路径  ...可选传参
storm jar /usr/appjar/storm-word-count-1.0.jar  com.heibaiying.wordcount.ClusterWordCountApp

出现successfully则代表提交成功

6.4 查看Topology与停止Topology命令行方式

# 查看所有Topology
storm list

# 停止  storm kill topology-name [-w wait-time-secs]
storm kill ClusterWordCountApp -w 3

6.5 查看Topology与停止Topology界面方式

使用UI界面同样也可进行同样的操作进入WEB UI界面8080端口Topology Summary中点击对应Topology 即可进入详情页面进行操作。

七、关于项目打包的扩展说明

mvn package的局限性

在上面的步骤中我们没有在POM中配置任何插件直接使用mvn package进行项目打包这对于没有使用外部依赖包的项目是可行的。但如果项目中使用了第三方JAR包就会出现问题因为package打包后的JAR中是不含有依赖包的如果此时你提交到服务器上运行就会出现找不到第三方依赖的异常。

这时候可能大家会有疑惑,在我们的项目中不是使用了storm-core这个依赖吗其实上面之所以我们能运行成功是因为在Storm的集群环境中提供了这个JAR包在安装目录的lib目录下

为了说明这个问题我在Maven中引入了一个第三方的JAR包并修改产生数据的方法

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>

StringUtils.join()这个方法在commons.lang3storm-core中都有,原来的代码无需任何更改,只需要在import时指明使用commons.lang3

import org.apache.commons.lang3.StringUtils;

private String productData() {
    Collections.shuffle(list);
    Random random = new Random();
    int endIndex = random.nextInt(list.size()) % (list.size()) + 1;
    return StringUtils.join(list.toArray(), "\t", 0, endIndex);
}

此时直接使用mvn clean package打包上传到服务器运行就会抛出下图异常。所以在此说明一下这种直接打包的方式并不适用于实际的开发因为实际开发中通常都是需要第三方的JAR包的。

想把依赖包一并打入最后的JAR中maven提供了两个插件来实现分别是maven-assembly-pluginmaven-shade-plugin。鉴于本篇文章篇幅已经比较长且关于Storm打包还有很多需要说明的地方所以关于Storm的打包方式单独整理至下一篇文章

Storm三种打包方式对比分析

参考资料

  1. Running Topologies on a Production Cluster
  2. Pre-defined Descriptor Files