文章目录

  • 1.storm集群架构
  • 2.storm编程模型
  • Topologies(拓扑)
  • Streams(流)
  • Spouts
  • Bolts
  • Stream groupings
  • Tasks
  • Workers
  • 3.storm入门实例
  • 集群安装
  • 项目创建
  • 定义WordReaderSpout
  • 定义WordSplitBolt
  • 定义WordCountBolt
  • 定义main
  • 本地集群运行
  • 生产集群运行
  • 4.storm其他应用场景

1.storm集群架构

Apache Storm的主要亮点是,它是一个容错,快速,没有“单点故障”(SPOF)分布式应用程序。我们可以根据需要在多个系统中安装Apache Storm,以增加应用程序的容量。

让我们看看Apache Storm集群如何设计和其内部架构。下图描述了集群设计。

hadoop 流 hadoop流式计算_hadoop 流

Apache Storm有两种类型的节点,Nimbus(主节点)和Supervisor(工作节点)。Nimbus是Apache Storm的核心组件。Nimbus的主要工作是运行Storm拓扑。Nimbus分析拓扑并收集要执行的任务。然后,它将任务分配给可用的supervisor。

Supervisor将有一个或多个工作进程。Supervisor将任务委派给工作进程。工作进程将根据需要产生尽可能多的执行器并运行任务。Apache Storm使用内部分布式消息传递系统来进行Nimbus和管理程序之间的通信。

组件

描述

Nimbus(主节点)

Nimbus是Storm集群的主节点。集群中的所有其他节点称为工作节点。主节点负责在所有工作节点之间分发数据,向工作节点分配任务和监视故障。

Supervisor(工作节点)

遵循指令的节点被称为Supervisors 。Supervisor有多个工作进程,它管理工作进程以完成由nimbus分配的任务。

Worker process(工作进程)

工作进程将执行与特定拓扑相关的任务。工作进程不会自己运行任务,而是创建执行器并要求他们执行特定的任务。工作进程将有多个执行器。

Executor(执行者)

执行器只是工作进程产生的单个线程。执行器运行一个或多个任务,但仅用于特定的spout或bolt。

Task(任务)

任务执行实际的数据处理。所以,它是一个spout或bolt。

ZooKeeper framework(ZooKeeper框架)

Apache的ZooKeeper的是使用群集(节点组)自己和维护具有强大的同步技术共享数据之间进行协调的服务。Nimbus是无状态的,所以它依赖于ZooKeeper来监视工作节点的状态。ZooKeeper的帮助supervisor与nimbus交互。它负责维持nimbus,supervisor的状态。

Storm是无状态的。即使无状态性质有它自己的缺点,它实际上帮助Storm以最好的可能和最快的方式处理实时数据。

Storm虽然不是完全无状态的。它将其状态存储在Apache ZooKeeper中。由于状态在Apache ZooKeeper中可用,故障的网络可以重新启动,并从它离开的地方工作。通常,像monit这样的服务监视工具将监视Nimbus,并在出现任何故障时重新启动它。

Apache Storm还有一个称为Trident拓扑的高级拓扑,它具有状态维护,并且还提供了一个高级API,如Pig。我们将在接下来的章节中讨论所有这些功能。

2.storm编程模型

本页列出了Storm 的主要概念, 以及可以获取到更多信息的资源链接, 概念如下:

  1. Topologies(拓扑)
  2. Streams(流)
  3. Spouts
  4. Bolts
  5. Stream groupings(流分组)
  6. Reliability(可靠性)
  7. Tasks
  8. Workers

Topologies(拓扑)

实时应用程序的逻辑被封装在 Storm topology(拓扑)中. Storm topology(拓扑)类似于 MapReduce 作业. 两者之间关键的区别是 MapReduce 作业最终会完成, 而 topology(拓扑)任务会永远运行(除非 kill 掉它). 一个拓扑是 Spout 和 Bolt 通过 stream groupings 连接起来的有向无环图.这些概念会在下面的段落中具体描述.

Streams(流)

stream 是 Storm 中的核心概念.一个 stream 是一个无界的、以分布式方式并行创建和处理的 Tuple 序列. stream 以一个 schema 来定义, 这个 schema 用来命名 stream tuple(元组)中的字段.默认情况下 Tuple 可以包含 integers, longs, shorts, bytes, strings, doubles, floats, booleans, and byte arrays 等数据类型.你也可以定义自己的 serializers, 以至于可以在 Tuple 中使用自定义的类型.

每一个流在声明的时候会赋予一个 ID. 由于只包含一个 stream 的 Spout 和 Bolt 比较常见, OutputFieldsDeclarer 有更方便的方法可以定义一个单一的 stream 而不用指定ID. 这个 stream 被赋予一个默认的 ID, “default”.

Spouts

Spout 是一个 topology(拓扑)中 streams 的源头. 通常 Spout 会从外部数据源读取 Tuple,然后把他们发送到拓扑中(如 Kestel 队列, 或者 Twitter API). Spout 可以是 可靠的不可靠的 . 可靠的 Spout 在 Storm 处理失败的时候能够重放 Tuple, 不可靠的 Spout 一旦把一个 Tuple 发送出去就撒手不管了.

Spout 可以发送多个流. 可以使用 OutputFieldsDeclarer 的 declareStream 方法定义多个流, 在 SpoutOutputCollector 对象的 emit 方法中指定要发送到的 stream .

Spout 中的最主要的方法是 nextTuple. nextTuple 要么向 topology(拓扑)中发送一个新的 Tuple, 要么在没有 Tuple 需要发送的情况下直接返回. 对于任何 Spout 实现, nextTuple 方法都必须非阻塞的, 因为 Storm 在一个线程中调用所有的 Spout 方法.

hadoop 流 hadoop流式计算_hadoop 流_02

Spout 的另外几个重要的方法是 ackfail. 这些方法在 Storm 检测到 Spout 发送出去的 Tuple 被成功处理或者处理失败的时候调用. ackfail只会在可靠的 Spout 中调用.

Bolts

拓扑中所有的业务处理都在 Bolts 中完成. Bolt 可以做很多事情,过滤, 函数, 聚合, 关联, 与数据库交互等.

Bolt 可以做简单 stream 转换. 复杂的 stream 转换一般需要多个步骤,因此也就要多个 Bolt 协同工作. 如, 转换一个 tweets stream 为一个 trending images stream 需要两个步骤:一个 Bolt 做每个图片被收藏 的滚动计数,同时一个或者多个 Bolt 输出被收藏 Top X 的图片 (你可以使用更具弹性的方式处理这个 stream 转换, 用3个 Bolt 而不是先前的2个 Bolt ).

Bolt 可以发送多个 stream. 可以使用 OutputFieldsDeclarer 的 declareStream 方法定义多个 streams, 并且在使用 OutputCollector emit 方法的时候指定要发送的 stream.

当你声明一个 Bolt 的 input stream,你总是会订阅其他组件特定的 stream .如果你想要订阅其他组件所有的 streams,你必须一个个的订阅. InputDeclarer 有语法可以订阅默认 stream-id 的 stream,代码:declarer.shuffleGrouping ("1"),意思是: 订阅组件 “1” 的默认 stream, 等价于 declarer.shuffleGrouping("1", DEFAULT_STREAM_ID).

Bolt 中最主要的方法是 execute 方法, 当有一个新 Tuple 输入的时候会进入这个方法. Bolt 使用OutputCollector对象发送新的 Tuple. Bolt 必须在每一个 Tuple 处理完以后调用 OutputCollector 上的 ack 方法, Storm 就会知道 tuple 什么时候完成 (最终可以确定 调用源 Spout Tuple 是没有问题的). 当处理一个输入的 Tuple:会基于这个 Tuple 产生零个或者多个 Tuple 发送出去,当所有的tuple 完成后,会调用 acking. Storm 提供了 IBasicBolt 接口会自动执行 acking .

最好在 Bolt 中启动新的线程异步处理 tuples. OutputCollector 是线程安全的, 并且可以在任何时刻调用.

Stream groupings

topology(拓扑)定义中有一部分是为每一个 bolt 指定输入的 streams . stream grouping 定义了stream 如何在 Bolts tasks 之间分区.

Storm 中一共有8个内置的 Stream Grouping. 可以通过实现 CustomStreamGrouping 接口来自定义 Stream groupings.

1. Shuffle grouping : Tuple 随机的分发到 Bolt Task, 每个 Bolt 获取到等量的 Tuple.
2. Fields grouping : streams 通过 grouping 指定的字段来分区. 例如流通过 “user-id” 字段分区, 具有相同 “user-id” 的 Tuple 会发送到同一个task, 不同 “user-id” 的 Tuple 可能会流入到不同的 tasks.
3. Partial Key grouping : stream 通过 grouping 中指定的 field 来分组, 与 Fields Grouping 相似. 但是对于 2 个下游的 Bolt 来说是负载均衡的, 可以在输入数据不平均的情况下提供更好的优化. 以下地址This paper 更好的解释了它是如何工作的及它的优势.
4. All grouping : stream 在所有的 Bolt Tasks之间复制. 这个 Grouping 小心使用.
5. Global grouping : 整个 stream 会进入 Bolt 其中一个任务.特别指出, 它会进入 id 最小的 task.
6. None grouping : 这个 grouping , 你不需要关心 stream 如何分组. 当前, None grouping 和 Shuffle grouping 等价. 同时, Storm 将使用 None grouping 的 bolts 和上游订阅的 bolt和spout 运行在同一个线程 (when possible).
7. Direct grouping : 这是一种特殊的 grouping 方式. stream 用这个方式 group 意味着由这个 Tuple 的生产者 来决定哪个消费者 来接收它. Direct grouping 只能被用于 direct streams . 被发射到 direct stream 的 tuple 必须使用emitDirect(int, int, java.util.List) 方法来发送. Bolt 可以使用TopologyContext 或者通过保持对OutputCollector(返回 Tuple 被发送到的目标 task id) 中的emit 方法输出的跟踪,获取到它的所有消费者的 ID .
8. Local or shuffle grouping : 如果目标 Bolt 有多个 task 和 streams源 在同一个 woker 进程中, Tuple 只会 shuffle 到相同 worker 的任务.否则, 就和 shuffle goruping 一样.

Storm 保障每一个 Spout 的 Tuple 都会被 topology(拓扑)处理.通过跟踪 tuples tree,每个 spout tuple 都会触发 tree , 确保 tuples tree 成功完成. 每一个拓扑都有一个关联的“message timeout”. 如果 Storm 检测到一个 Spout Tuple 没有在这个超时时间内被处理完成, 则判定这个 Tuple 失败, 稍后重新执行.

要利用这个可靠性的功能, 当在 Tuple tree 中创建一个新的 edge ,必须告诉Storm,并且在一个单独的 tuple 完成时也要通知 Storm. 以上操作在 Bolt 用于发送 Tuple 的 OutputCollector对象中完成这个操作. Anchoring(锚点)在 emit 方法中完成, 使用 ack 方法来声明你已经成功完成了一个 Tuple 的处理.

Tasks

每个 Spout 或者 Bolt 都以跨集群的多个 Task 方式执行. 每个 Task 对应一个 execution 的线程, stream groupings 定义如何从一个 Task 发送 Tuple 到另一个 Task. 可以在 TopologyBuilder 的 setSpoutsetBolt 方法中为每个 Spout 或者 Bolt 设置并行度,.

Workers

Topologies (拓扑)在一个或者跨多个 worker 执行. 每个 Worker 进程是一个物理的 JVM, 执行 topology(拓扑) Tasks 中的一个子集. 例如, 如果一个拓扑的并行度是 300, 共有 50 个 Worker 在运行, 每个 Worker 会分配到 6 个 Task(作为 Worker 中的线程). Storm 会尽量把所有 Task 均匀的分配到所有的 Worker 上.

3.storm入门实例

集群安装

入门实例可直接使用本地集群调试,集群安装请参考:官网教程

项目创建

实例描述:

  1. 定义spout(WordReaderSpout)从某个源文件中读取单词,每一行将分开到stream.
  2. 定义bolt(WordSplitBolt)获取到数据行,使用空格进行切分,将每个单词分发到stream。
  3. 定义bold(WordCountBolt)获取单词,定义全局map,单词出现一次即使累加一次,cleanup中统计词频数。

创建基于maven的java项目,添加如下依赖

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

定义WordReaderSpout

package cn.test.wordCount;

import org.apache.storm.spout.SpoutOutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseRichSpout;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Values;

import java.io.*;
import java.util.Map;

/*
    spout是流的源头,需要从其他输入源获取数组转换成元祖,同时作为流发散出去,bolt负责消费
 */
public class WordReaderSpout extends BaseRichSpout {
    SpoutOutputCollector spoutOutputCollector;
    BufferedReader fileReader=null;
    boolean completed=false;

    /**
     * 当spout打开时自动调用
     * @param map 配置对象,在定义topology 对象是创建
     * @param topologyContext 包含所有拓扑数据
     * @param spoutOutputCollector 它能让我们发布交给 bolts 处理的数据
     */
    public void open(Map<String, Object> map, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {
        this.spoutOutputCollector=spoutOutputCollector;
        String wordsFile = map.get("wordsFile").toString();
        try {
            this.fileReader=new BufferedReader(new FileReader(new File(wordsFile)));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    public void nextTuple() {
        if(completed){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                //什么也不做
            }
            return;
        }
        String line=null;
        try {
            while ((line = fileReader.readLine()) != null) {
                spoutOutputCollector.emit(new Values(line));
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }finally {
            completed=true;
            try {
                fileReader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 这个其实定义字段意义不大,分发到任何的bolt进行拆分都是无状态的。
     * @param outputFieldsDeclarer
     */
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
        outputFieldsDeclarer.declare(new Fields("line"));
    }
}

定义WordSplitBolt

package cn.test.wordCount;

import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseRichBolt;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
import org.apache.storm.tuple.Values;

import java.util.Map;

public class WordSplitBolt extends BaseRichBolt {
    private OutputCollector outputCollector;
    public void prepare(Map<String, Object> map, TopologyContext topologyContext, OutputCollector outputCollector) {
        this.outputCollector=outputCollector;
    }

    public void execute(Tuple tuple) {
        String string = tuple.getString(0);
        for (String s : string.split(" ")) {
            if(!s.trim().equals("")){
                outputCollector.emit(new Values(s.toLowerCase()));
            }
        }
        outputCollector.ack(tuple);
    }

    /**
     *  定义字段后如果按照字段分组,相同的字段就会进入同一个bolt中,比如统计就适合使用
     * @param outputFieldsDeclarer
     */
    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
        outputFieldsDeclarer.declare(new Fields("word"));
    }
}

定义WordCountBolt

package cn.test.wordCount;

import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseRichBolt;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
import org.apache.storm.tuple.Values;

import java.util.HashMap;
import java.util.Map;

public class WordCountBolt extends BaseRichBolt {
    private OutputCollector outputCollector;
    private Map<String,Integer> counter=new HashMap<String, Integer>();

    @Override
    public void cleanup() {
        super.cleanup();
        for(Map.Entry entry : counter.entrySet()){
            System.out.println("单词:"+entry.getKey()+": "+entry.getValue());
        }
    }

    public void prepare(Map<String, Object> map, TopologyContext topologyContext, OutputCollector outputCollector) {
        this.outputCollector=outputCollector;
    }

    public void execute(Tuple tuple) {
        String string = tuple.getString(0);
        if(counter.containsKey(string)){
            counter.put(string,counter.get(string)+1);
        }else{
            counter.put(string,+1);
        }
        outputCollector.ack(tuple);
    }

    public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {

    }
}

定义main

本地集群运行
package cn.test.wordCount;

import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.topology.TopologyBuilder;
import org.apache.storm.tuple.Fields;

public class WordTopologyMain {
    public static void main(String[] args) {
        Config conf = new Config();
        conf.put("wordsFile", "c:/a.txt");
        conf.setDebug(true);
        TopologyBuilder topologyBuilder=new TopologyBuilder();
        topologyBuilder.setSpout("word",new WordReaderSpout(),1);
        topologyBuilder.setBolt("wordSplit",new WordSplitBolt(),1).shuffleGrouping("word");
        //加入有两个WordCount,如果使用随机分组a单词如果出现两次 可能两个单词去到不同的bolt,就会导致统计错误,定义
        //字段后,可以使用字段分组,相同的字段一定会进入同一个bolt,相同的单词就会统计正确
        topologyBuilder.setBolt("wordCounter",new WordCountBolt(),1).fieldsGrouping("wordSplit",new Fields("word"));
        try (LocalCluster cluster = new LocalCluster()) {
            cluster.submitTopology("WordTopologie", conf, topologyBuilder.createTopology());
            Thread.sleep(10000);
            cluster.shutdown();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
生产集群运行

修改LocalCluster为:

StormSubmitter.submitTopology("WordTopologie", conf, topology);

提交拓扑到集群使用storm客户端,打包代码到jar后,运行以下命令执行:

storm jar path/to/allmycode.jar org.me.MyTopology arg1 arg2 arg3

关闭topology, 请执行:

storm kill {stormname}

4.storm其他应用场景

  1. 求TopN:在规定时间窗口内,统计数据出现的TopN,该类处理在购物及电商业务需求中,比较常见,如实时统计排名(如商户销售产品排名),网站访问量统计等。
  2. 条件过滤:这是Storm最基本的处理方式,对符合条件的数据进行实时过滤,将符合条件的数据保存下来,这种实时查询的业务需求再实际应用中很常见。
  3. 中间计算:我们需要改变数据中某一个字段(例如是数值),我们需要利用一个中间值经过计算(值比较、求和、求平均等等)后改变该值,然后将数据重新输出。
  4. 推荐系统:有时候在实时处理时会从mysql及hadoop中获取数据库中的信息,例如在电影推荐系统中,传入数据为:用户当前点播电影信息,从数据库中获取的是该用户之前的一些点播电影信息统计,例如点播最多的电影类型、最近点播的电影类型,及其社交关系中点播信息,结合本次点击及从数据库中获取的信息,生成推荐数据,推荐给该用户。并且该次点击记录将会更新其数据库中的参考信息,这样就是实现了简单的智能推荐。
  5. 分布式RPC:Storm有对RPC进行专门的设计,分布式RPC用于对Storm上大量的函数进行并行计算,最后将结果返回给客户端。
  6. 批处理:所谓批处理就是数据积攒到一定触发条件,就批量输出,所谓的触发条件类似事件窗口到了,统计数量够了即检测到某种数据传入等等。
  7. 热度统计:热度统计实现依赖于Storm提供的TimeCacheMap数据结构,现在可能推荐用RotatingMap,关于这两个数据结构的源码分析,移步Storm TimeCacheMap RotatingMap源码分析,该结构能够在内存中保存近期活跃的对象。我们可以使用它来实现例如论坛中热帖排行计算等。