初入Storm

前言

学习Storm已经有两周左右的时间,但是认真来说学习过程确实是零零散散,遇到问题去百度一下,找到新概念再次学习,在这样的一个循环又不成体系的过程中不断学习Storm。

前人栽树,后人乘凉,也正是因为网上有这样多热心的人,分享自己的见解,才能够让开发变得更简单。也正是基于这个目的,同时公司恰好是做大数据的,预计还有相当长的时间需要深入Storm,决定写一下Storm系列相关知识。

正文

在大数据处理中,目前来看,有这样三种主要的数据处理方式,以hadoop为主的大数据批处理框架, 以Storm为主的实时计算流处理框架, 还有以Spark为主的微型批处理流框架。

解释可能不太到位, 但是Storm最重要的特点也就是, 实时, 流处理。

基本概念

在这里通过一个网络上比较常见的案例来作为开始吧, 假设我们需要对一篇文章,一本书中的所有单词按照首字母进行统计,统计每种首字母的单词按照长度划分进行统计,也就是等首字母,等长度的单词,究竟出现了多少次,应该怎样做呢?

无论是以怎样的开发框架,模式来进行思考,我们很容易想到这样一个处理步骤:

用IO流从文章中不断读取内容作为输入,然后提取每个单词的首字母,判断单词的首字母,先按照首字母分组,再将分组过后的数据一个个统计其长度,对应的数值即可。

那么一点点来进行拆分。

拓扑(Topology)

首先需要提到的一个概念就是拓扑。不难将上述概念转换成如下流程图:

这样的每一个圆都代表一个简单的处理或计算过程,每条边就代表将上一个节点处理结束的数据发送到下一个节点,这样一个数据流向。

而拓扑正是这样一个计算图,结点代表一些计算,数据处理逻辑,边代表在结点之间数据的传递,由结点和边所构建出来的这个整体,完成一个完整功能的整体,就被称作是拓扑。

元组(Tuple)

元组是拓扑之间传输数据的形式,它本身是一个有序的数值序列。因为是有序的数值序列,就意味着在特定的index有着特定的含义,而这个含义又或者字段名称(field)就是由使用者自己定义的。

任何一个节点都可以创造元组,并发送给任意其他一个或多个节点,而这个过程就被称作是发射(emit)一个元组。

那么就会有这样一个问题,在元组中并没有对数据类型做出强制限定,对于处在不同机器,或不同进程的节点,一般是需要通过网络发送,或是socket在本机间发送,是如何发送java中的对象呢?答案是通过序列化的方式。而在这一点,我们在后续的篇章再提。

流(Streaming)

流就是一个“无边界的元组序列”, 元组是基本的传输单位,当元组在两个节点之间源源不断的发送,就是所谓的流。

而除了根节点是从数据源不断读取数据之外,其他的节点都可以从任意多个节点接收数据,而每一个节点都可以向任意多个节点发送数据。

spout

Spout的主要功能是从数据源中读取数据,并向其他节点发送数据,数据源可以有多种,文件,消息队列,数据库。

Spout中并不包含对数据的处理逻辑,所需要做的是,从数据源读取,发送。但也并非完全意义上的什么都不做,一般来说,在这一步会选择完成反序列化这一工作,甚至更近一步的,将接收到的数据转换成相应的基本Java对象发射出去,之所以说是基本的,也意味着仅仅是将字符串或其他形式的数据,转换成对象,并不做任何特殊处理。

为什么Spout不做任何的数据处理功能呢?在这里是不是连对象转换也不要有比较好呢?

个人理解,由于Spout是整个数据处理的第一环,大批量的数据流入并从当前节点分发,在nextTuple中不能有阻塞是基本要求。在这一环节做出的操作越多,对性能的影响越大。至于对象转换, 由于这是对数据的基本操作,也就是放在任何节点都需要执行的东西,更何况,如果是自己设计, 如果数据被原样发出,会在下一节点做出数据转换后,进一步发送。相当于做了一次无效的发送操作。

所以,仍需考量。

bolt

不同于spout只监听数据源,bolt可以完成从输入流的元组接收, 转换, 处理, 发射功能。是我们的topology中真正的数据处理节点。

在我们的例子中有这样两个bolt:

  • 提取单词首字母:所做的工作是,接收单词,获取单词首字母,并发送到下一节点。

  • 数据更新节点:接收单词,判断首字母标识字段, 判断长度, 更新计数器。

我们会注意到, 接收, 处理, 或许发送, 是bolt的所有功能。

就接收来说,我们的数据来源可能并不止一个,可能是Spout,也可能是其他bolt。 对于发送来说, 我们的目的地也可能不止一个,既可以是bolt, 也有可能是其他拓扑的Spout。 bolt 可以是多入多出的。

小结

就现在而言,我们知道了:

  • 一个拓扑包含大量的节点和边

  • 节点有Spout或bolt

  • 边代表节点间的元组流

  • 一个元祖是一个有序的数值列表,每个数组都被赋予一个命名

  • 一个数据流失一个在spout 和 bolt 或两个bolt之间的无边界元组序列。

  • spout是拓扑的数据源

  • bolt接收输入流,做出数据处理,可能会发送数据给下一节点。

  • 在实际中,每个spout可能会同时运行一个或多个独立的实例,并行的进行相应的数据处理。

流分组

我们还需要关注一下下其中的一个策略性问题, 即流分组, 当数据从一个节点发送到另一个节点是以流的形式进行发送。

我们已经知道处在当前节点的下游,可能存在多个不同种类的bolt, 也可能存在同一bolt的多个实例。数据流是怎样分配的呢?

对待第一种情况, 不同种类的bolt,比较好处理, 我们为每一种类型的流,就案例而言,如果我们设计了多种bolt, 分别处理相应字母开头的单词, 那么在spout发送时, 就可以指定流的名字, bolt接收时,不同的bolt实例去接收不同的流即可。

而第二种情况即是流分组, 最常见的是随机分组, 它可以保证每个bolt接收到的数据量基本一致,负载均衡。但是,并不是绝对均衡,因为采取的是随机的方式,并不是轮询策略。

第二种比较常见的方式是, 字段分组, 它可以保证特定字段上的值相同的元组发射到同一个bolt实例。

流分组策略有多种,在后续会有章节提到。

Storm工程

相应代码已经上传至:

git@github.com:zyzdisciple/storm_study.git

需要提到的一点是:在运行topology时,可能会打印的东西过多,即使加了debug false也不能够改变这一问题,需要在当前项目的 resources中加入, log4j2.xml 更改打印Level;

log4j2.xml 在 storm-core jar包中自带。

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration monitorInterval="60">
  3. <Appenders>
  4. <Console name="Console" target="SYSTEM_OUT">
  5. <PatternLayout pattern="%-4r [%t] %-5p %c{1.} - %msg%n"/>
  6. </Console>
  7. </Appenders>
  8. <Loggers>
  9. <!--<Logger name="org.apache.zookeeper" level="WARN"/>-->
  10. <Root level="WARN">
  11. <AppenderRef ref="Console"/>
  12. </Root>
  13. </Loggers>
  14. </configuration>

获取Storm的最简单方式是通过Maven:

  1. <!-- https://mvnrepository.com/artifact/org.apache.storm/storm-core -->
  2. <dependency>
  3. <groupId>org.apache.storm</groupId>
  4. <artifactId>storm-core</artifactId>
  5. <version>1.2.1</version>
  6. <!--在真实项目中一般需要定义为provided,暂时注释 -->
  7. <!--<scope>provided</scope>-->
  8. </dependency>

创建Spout

在开始你的代码之前,最好对整个拓扑有一个较为清晰的了解,也就是我们之前所做的工作, 需要知道数据源的数据输出格式, 拥有几个节点,每个节点是做什么的,数据在各个节点之间如何分发,数据输入节点之前应该是怎样的,流出节点之后又应该是怎样的?

在弄清楚上述问题之前,一般最好不要开始进行代码。

而我们的输入呢?是读取一个文件, 读取文件中的每一行数据即可,然后分发到下一个节点去:

  1. import org.apache.storm.spout.SpoutOutputCollector;
  2. import org.apache.storm.task.TopologyContext;
  3. import org.apache.storm.topology.OutputFieldsDeclarer;
  4. import org.apache.storm.topology.base.BaseRichSpout;
  5. import org.apache.storm.tuple.Fields;
  6. import org.apache.storm.tuple.Values;
  7. import java.io.BufferedReader;
  8. import java.io.FileNotFoundException;
  9. import java.io.FileReader;
  10. import java.io.IOException;
  11. import java.util.Map;
  12. /**
  13. * @author zyzdisciple
  14. * @date 2019/4/3
  15. */
  16. public class FileReaderSpout extends BaseRichSpout {
  17. private static final long serialVersionUID = -1379474443608375554L;
  18. private SpoutOutputCollector collector;
  19. private BufferedReader br;
  20. /**
  21. * 方法是用来初始化一些资源类,具体的参数需要待对storm有了更深入的了解之后再度来看。
  22. * 这些资源类不仅仅是参数提供的资源, 包括读取文件, 读取数据库,等等其他任何方式,
  23. * 打开数据资源都是在这个方法中实现。
  24. * 原因则是可以理解为,当对象被初始化时执行的方法,并不准确,但可以这样理解。
  25. * @param conf
  26. * @param topologyContext
  27. * @param spoutOutputCollector
  28. */
  29. public void open(Map conf, TopologyContext topologyContext, SpoutOutputCollector spoutOutputCollector) {
  30. this.collector = spoutOutputCollector;
  31. try {
  32. br = new BufferedReader(new FileReader("E:\\IdeaProjects\\storm_demo\\src\\main\\resources\\data.txt"));
  33. } catch (FileNotFoundException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. /**
  38. * 流的核心,不断调用这个方法,读取数据,发送数据。
  39. * 在这里采取的方式是每次读取一行,当然也可以在一次中读取所有数据,然后在循环中
  40. * emit发射数据。
  41. * 需要特别注意的是,这个方法一定是不能够被阻塞的, 也不能够抛出异常,
  42. * 抛出异常会让当然程序停止,阻塞严重影响性能。
  43. */
  44. public void nextTuple() {
  45. try {
  46. //向外发射数据
  47. String line = br.readLine();
  48. if (line == null) {
  49. return;
  50. }
  51. collector.emit(new Values(line));
  52. } catch (IOException e) {
  53. e.printStackTrace();
  54. }
  55. }
  56. /**
  57. * 定义输出格式,在collector.emit时,new values可接受数组, 如发送 a b c,
  58. * 则此时会与declare field中的名称一一对应,且顺序一致,并且必须保证数量一致。
  59. * 通过这种配置的方式,就无需以map形式输出数据, 我们可以仅输出值即可。
  60. *
  61. * 当然declare不止这一种重载方法,其余的暂时不用理会。
  62. * @param declarer
  63. */
  64. public void declareOutputFields(OutputFieldsDeclarer declarer) {
  65. /*Fields名称这里,一般使用中会拆出来,定义为常量,而不是直接字符串,
  66. * 包括Stream等其他属性也是,因为很有可能在其他地方会被用到,所以一般拆分成常量
  67. * */
  68. declarer.declare(new Fields("line"));
  69. //declarer.declare(new Fields(DemoConstants.FIELD_LINE)); //应该采取这种方式
  70. }
  71. /**
  72. * 在fileReader结束之后关闭对应的流
  73. * 可以暂时忽略
  74. */
  75. @Override
  76. public void close() {
  77. if (br != null) {
  78. try {
  79. br.close();
  80. } catch (IOException e) {
  81. e.printStackTrace();
  82. }
  83. }
  84. }
  85. }

bolt节点

在bolt的代码中, 并没有太多值得提到的地方, 因为它的操作大都与Spout保持一致。

  1. import org.apache.storm.task.OutputCollector;
  2. import org.apache.storm.task.TopologyContext;
  3. import org.apache.storm.topology.OutputFieldsDeclarer;
  4. import org.apache.storm.topology.base.BaseRichBolt;
  5. import org.apache.storm.tuple.Fields;
  6. import org.apache.storm.tuple.Tuple;
  7. import org.apache.storm.tuple.Values;
  8. import java.util.Map;
  9. /**
  10. * @author zyzdisciple
  11. * @date 2019/4/3
  12. */
  13. public class WordsBolt extends BaseRichBolt {
  14. private static final long serialVersionUID = 520139031105355867L;
  15. private OutputCollector collector;
  16. /**
  17. * 与spout中的open方法功能基本一致。
  18. * @param stormConf
  19. * @param context
  20. * @param collector
  21. */
  22. public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
  23. this.collector = collector;
  24. }
  25. /**
  26. * 类比于Spout中的nextTuple
  27. * @param input 接收的数据,存有数据以及其相关信息。
  28. */
  29. public void execute(Tuple input) {
  30. String line = input.getStringByField("line").trim();
  31. //input.getStringByField(DemoConstants.FIELD_LINE);
  32. if (!line.isEmpty()) {
  33. String[] words = line.split(" ");
  34. for (String word : words) {
  35. if (!word.trim().isEmpty()) {
  36. collector.emit(new Values(word.charAt(0), word.length(), word));
  37. }
  38. }
  39. }
  40. }
  41. public void declareOutputFields(OutputFieldsDeclarer declarer) {
  42. declarer.declare(new Fields("headWord", "wordLength", "word"));
  43. //declarer.declare(new Fields(DemoConstants.FIELD_HEAD_WORD, DemoConstants.FIELD_WORD_LENGTH, DemoConstants.FIELD_WORD));
  44. }
  45. }
  46. import org.apache.storm.task.OutputCollector;
  47. import org.apache.storm.task.TopologyContext;
  48. import org.apache.storm.topology.OutputFieldsDeclarer;
  49. import org.apache.storm.topology.base.BaseRichBolt;
  50. import org.apache.storm.tuple.Tuple;
  51. import java.util.HashMap;
  52. import java.util.Map;
  53. /**
  54. * @author zyzdisciple
  55. * @date 2019/4/3
  56. */
  57. public class CountBolt extends BaseRichBolt {
  58. private static final long serialVersionUID = 3693291291362580453L;
  59. //这里存的时候取巧,用 a1 a2 表示首字母为1,长度为1,2 的单词
  60. private Map<String, Integer> counterMap;
  61. private OutputCollector collector;
  62. @Override
  63. public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
  64. this.collector = collector;
  65. /*为什么hashMap也要放在这里进行初始化以后再提,这里暂时忽略。
  66. *在storm中, bolt和 spout的初始化一般都不会放在构造器中进行,
  67. * 而都是放在prepare中。
  68. */
  69. counterMap = new HashMap<>();
  70. }
  71. @Override
  72. public void execute(Tuple input) {
  73. String key = input.getValueByField("headWord").toString().toLowerCase() + input.getIntegerByField("wordLength");
  74. counterMap.put(key, countFor(key) + 1);
  75. counterMap.forEach((k, v) -> {
  76. System.out.println(k + " : " + v);
  77. });
  78. }
  79. /**
  80. * 在这里因为不需要向下一个节点下发数据, 因此不需要定义。
  81. * @param declarer
  82. */
  83. @Override
  84. public void declareOutputFields(OutputFieldsDeclarer declarer) {
  85. }
  86. /**
  87. * 统计当前key已经出现多少次。
  88. * @param key
  89. * @return
  90. */
  91. private int countFor(String key) {
  92. Integer count = counterMap.get(key);
  93. return count == null ? 0 : count;
  94. }
  95. /**
  96. * 与Spout的close方法类似
  97. */
  98. @Override
  99. public void cleanup() {
  100. }
  101. }

在countBolt中存在一个属性, map, 这是私有属性, 而storm在执行的时候可能会创建多个bolt实例,他们之间的变量并不共享, 这必然会导致一些问题, 这就是我们为什么在流分组策略中选择 fieldGroup分组的方式, 它能够保证, field相同的数据, 最终必然会流向同一个bolt实例。

但不能够保证 key: a key: b,的两个tuple流向不同的bolt。

topology

  1. import com.storm.demo.rudiments.bolt.CountBolt;
  2. import com.storm.demo.rudiments.bolt.WordsBolt;
  3. import com.storm.demo.rudiments.spout.FileReaderSpout;
  4. import org.apache.storm.Config;
  5. import org.apache.storm.LocalCluster;
  6. import org.apache.storm.generated.StormTopology;
  7. import org.apache.storm.topology.TopologyBuilder;
  8. import org.apache.storm.tuple.Fields;
  9. import org.apache.storm.utils.Utils;
  10. /**
  11. * @author zyzdisciple
  12. * @date 2019/4/3
  13. */
  14. public class WordCountTopology {
  15. private static final String STREAM_SPOUT = "spoutStream";
  16. private static final String STREAM_WORD_BOLT = "wordBoltStream";
  17. private static final String STREAM_COUNT_BOLT = "countBoltStream";
  18. private static final String TOPOLOGY_NAME = "rudimentsTopology";
  19. private static final Long TEN_SECONDS = 1000L * 10;
  20. public static void main(String[] args) {
  21. TopologyBuilder builder = new TopologyBuilder();
  22. //设置Spout,第一个参数为节点名称, 第二个为对应的Spout实例
  23. builder.setSpout(STREAM_SPOUT, new FileReaderSpout());
  24. //设置bolt,在这里采用随机分组即可,在shuffleGrouping,中第一个参数为接收的节点名称,表示从哪个节点接收数据
  25. //这里并不能等同于流名称,这个概念还有其他用处。
  26. builder.setBolt(STREAM_WORD_BOLT, new WordsBolt()).shuffleGrouping(STREAM_SPOUT);
  27. //在这里采取的是fieldsGrouping,原因则是因为在CountBolt中存在自有Map,必须保证属性一致的分到同一个bolt实例中
  28. builder.setBolt(STREAM_COUNT_BOLT, new CountBolt()).fieldsGrouping(STREAM_WORD_BOLT, new Fields("headWord", "wordLength"));
  29. //相关配置
  30. Config config = new Config();
  31. config.setDebug(true);
  32. //本地集群
  33. LocalCluster cluster = new LocalCluster();
  34. //通过builder创建拓扑
  35. StormTopology topology = builder.createTopology();
  36. //提交拓扑
  37. cluster.submitTopology(TOPOLOGY_NAME, config, topology);
  38. //停留几秒后关闭拓扑,否则会永久运行下去
  39. Utils.sleep(TEN_SECONDS);
  40. cluster.killTopology(TOPOLOGY_NAME);
  41. cluster.shutdown();
  42. }
  43. }

在这个topology中,虽然功能简单,但事实已经完整的展示了一个topology的设计流程, 同时在 main方法中也蕴藏了整个 topology的执行流程,生命周期等等。 这部分在后续会提到。

Storm系列一: Storm初步的更多相关文章

  1. Storm系列之一——Storm Topology并发

    1.是什么构成一个可运行的topology? worker processes(worker进程),executors(线程)和tasks. 一台Storm集群里面的机器可能运行一个或多个worker ...

  2. Storm系列二: Storm拓扑设计

    Storm系列二: Storm拓扑设计 在本篇中,我们就来根据一个案例,看看如何去设计一个拓扑, 如何分解问题以适应Storm架构,同时对Storm拓扑内部的并行机制会有一个基本的了解. 本章代码都在 ...

  3. Storm系列(三):创建Maven项目打包提交wordcount到Storm集群

    在上一篇博客中,我们通过Storm.Net.Adapter创建了一个使用Csharp编写的Storm Topology - wordcount.本文将介绍如何编写Java端的程序以及如何发布到测试的S ...

  4. Storm系列(二):使用Csharp创建你的第一个Storm拓扑(wordcount)

    WordCount在大数据领域就像学习一门语言时的hello world,得益于Storm的开源以及Storm.Net.Adapter,现在我们也可以像Java或Python一样,使用Csharp创建 ...

  5. Storm系列(一):搭建dotNet开发Storm拓扑的环境

    上篇博客比较了目前流行的计算框架特性,如果你是 Java 开发者,那么根据业务场景选择即可:但是如果你是 .Net 开发者,那么三者都不能拿来即用,至少在这篇文章出现之前是如此.基于上篇文章的比较发现 ...

  6. Storm 系列(三)Storm 集群部署和配置

    Storm 系列(二)Storm 集群部署和配置 本章中主要介绍了 Storm 的部署过程以及相关的配置信息.通过本章内容,帮助读者从零开始搭建一个 Storm 集群. 一.Storm 的依赖组件 1 ...

  7. Storm 系列(二)实时平台介绍

    Storm 系列(二)实时平台介绍 本章中的实时平台是指针对大数据进行实时分析的一整套系统,包括数据的收集.处理.存储等.一般而言,大数据有 4 个特点: Volumn(大量). Velocity(高 ...

  8. Storm 系列(一)基本概念

    Storm 系列(一)基本概念 Apache Storm(http://storm.apache.org/)是由 Twitter 开源的分布式实时计算系统. Storm 可以非常容易并且可靠地处理无限 ...

  9. Storm系列三: Storm消息可靠性保障

    Storm系列三: Storm消息可靠性保障 在上一篇 Storm系列二: Storm拓扑设计 中我们已经设计了一个稍微复杂一点的拓扑. 而本篇就是在上一篇的基础上再做出一定的调整. 在这里先大概提一 ...

随机推荐

  1. centos_x64 6.4 安装jdk1.7

    1.行到user目录下新建一个java目录 #cd /usr #mkdir java #cd /usr/java/ 2.下载jdk 先从oracle找到要下载的jdk地址然后 wget http:// ...

  2. 20155210 2016-2017-2 《Java程序设计》第7周学习总结

    20155210 2016-2017-2 <Java程序设计>第7周学习总结 教材学习内容总结 时间的度量: GMT(Greenwich Mean Time)时间:现在不是标准时间 世界时 ...

  3. JavaScript 闭包的例子

    例子出自<<JavaScript权威指南>>, 加上个人的理解和总结, 欢迎交流! /********************************************* ...

  4. 2018.10.20 loj#2593. 「NOIP2010」乌龟棋(多维dp)

    传送门 f[i][j][k][l]f[i][j][k][l]f[i][j][k][l]表示用iii张111,jjj张222,kkk张333,lll张444能凑出的最大贡献. 然后从f[i−1][j][ ...

  5. phalapi框架where条件查询

    // WHERE name = 'dogstar' AND age = 18 $user->where(array('name' => 'dogstar', 'age' => 18) ...

  6. html 源码 引入样式

    post-title2 示例 sdf post-title 示例

  7. obj-c的优缺点

    优点: 1) Cateogies : 类别 2) Posing : 扮演 3) 动态识别 : 编译时与运行时动态识别类型 4) 指标计算 : 指针计算 指针的 +- * / 5) 弹性信息传递 : 某 ...

  8. (KMP)Seek the Name, Seek the Fame -- poj --2752

    http://poj.org/problem?id=2752 Seek the Name, Seek the Fame Time Limit: 2000MS   Memory Limit: 65536 ...

  9. hdu1171 Big Event in HDU(01背包) 2016-05-28 16:32 75人阅读 评论(0) 收藏

    Big Event in HDU Time Limit: 10000/5000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others ...

  10. 洛谷P3567[POI2014]KUR-Couriers(主席树+二分)

    题意:给一个数列,每次询问一个区间内有没有一个数出现次数超过一半 题解: 最近比赛太多,都没时间切水题了,刚好日推了道主席树裸题,就写了一下 然后 WA80 WA80 WA0 WA90 WA80 ?? ...