Hadoop学习教程,从入门到精通,Storm 完整知识点详解(17)

Storm 完整知识点详解


一、什么是 Storm

1.1 Storm 概述

Apache Storm 是一个免费开源、分布式、高容错的实时计算系统 。Storm 使得持续不断的流数据流的处理变得简单,类似于 Hadoop 对批量数据(批处理)的处理方式,Storm 处理的是实时流数据(流处理)

1.2 Storm 核心特点

特点 说明
实时性 数据产生即处理,毫秒级延迟
分布式 可水平扩展到大规模集群
容错性 自动重启失败的组件,保证数据不丢失
语言无关 支持任何编程语言(Java、Python、Ruby等)
可靠的消息处理 保证每条消息至少被处理一次(at-least-once)

1.3 Storm 与 Hadoop 对比

对比项 Hadoop Storm
处理方式 批处理(Batch) 实时流处理(Streaming)
延迟 分钟/小时级 毫秒/秒级
数据模型 静态数据集 无限持续数据流
核心进程 JobTracker/TaskTracker Nimbus/Supervisor
编程模型 MapReduce Spout/Bolt
是否终止 处理完数据后任务终止 持续运行,永不停止

1.4 Storm 应用场景

复制代码
- 实时数据分析(网站点击流、用户行为分析)
- 实时监控告警(系统日志监控、异常检测)
- 在线机器学习(实时推荐系统)
- 实时ETL(数据实时抽取转换加载)
- 分布式RPC(远程过程调用)

二、Storm 核心概念

2.1 Tuple(元组)

Tuple 是 Storm 中最基本的数据结构,是一个命名的值列表,Tuple 中的字段可以是任意类型的对象。

java 复制代码
// 创建一个Tuple示例
// Storm的Tuple是由框架自动生成的,这里展示的是Tuple的使用方式

// 在Spout或Bolt中,emit发送的就是Tuple
// 例如发送一个包含两个字段的Tuple
collector.emit(new Values("hello", 1));
// Values("hello", 1) 就是一个Tuple,包含两个字段
// 第一个字段值为字符串 "hello"
// 第二个字段值为整数 1

2.2 Stream(流)

Stream 是 Storm 中最核心的抽象概念,是一个无限的 Tuple 序列

复制代码
Stream 的特征:
  1. 无限性 ------ 数据持续不断产生
  2. 有序性 ------ Tuple 按照产生的顺序排列
  3. 命名性 ------ 每个 Stream 可以定义一个唯一的ID(默认为 "default")

2.3 Spout(数据源)

Spout 是 Topology 中数据流的源头,负责从外部数据源读取数据并发射到 Topology 中。

java 复制代码
/**
 * 自定义Spout示例
 * 继承 BaseRichSpout 类
 * Spout 负责从外部数据源(如Kafka、日志文件等)读取数据
 * 并将数据以Tuple的形式发射(emit)到Topology中
 */
public class MySpout extends BaseRichSpout {

    // Spout发射器,用于向下游Bolt发送Tuple
    private SpoutOutputCollector collector;

    // 随机数生成器,用于模拟数据
    private Random random;

    /**
     * open方法:Spout初始化时调用
     * 类似于MapReduce中Setup方法,只执行一次
     * @param conf        Storm配置信息
     * @param context     Topology上下文,包含Topology相关信息
     * @param collector   发射器,用于发送Tuple
     */
    @Override
    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
        // 初始化SpoutOutputCollector发射器
        this.collector = collector;
        // 初始化随机数生成器
        this.random = new Random();
    }

    /**
     * nextTuple方法:Spout的核心方法
     * 会被Storm框架循环调用,不断产生新的Tuple
     * 该方法中实现从外部数据源读取数据的逻辑
     * 注意:如果暂时没有数据,应该使用Thread.sleep()避免空循环消耗CPU
     */
    @Override
    public void nextTuple() {
        // 模拟产生数据的句子
        String[] sentences = {
            "hello storm hello world",
            "apache storm real time",
            "big data streaming processing"
        };
        // 随机选择一个句子
        String sentence = sentences[random.nextInt(sentences.length)];
        // 使用collector发射Tuple
        // new Values(sentence) 创建一个包含一个字段的Tuple
        // 该Tuple会被发送到下游的Bolt进行处理
        this.collector.emit(new Values(sentence));
        // 休眠100毫秒,避免过快产生数据
        Utils.sleep(100);
    }

    /**
     * declareOutputFields方法:声明输出Tuple的字段名
     * 这里的字段名用于下游Bolt通过字段名获取对应的值
     * @param declarer  输出字段声明器
     */
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        // 声明输出字段名为 "sentence"
        // downstream Bolt可以通过 tuple.getValueByField("sentence") 获取值
        declarer.declare(new Fields("sentence"));
    }
}

2.4 Bolt(处理单元)

Bolt 负责接收 Tuple、处理数据、发射新的 Tuple 到下游 Bolt 或输出结果。

java 复制代码
/**
 * 自定义Bolt示例:将句子拆分为单词
 * 继承 BaseRichBolt 类
 * Bolt 负责处理接收到的Tuple,并将处理结果发射到下游
 */
public class SplitBolt extends BaseRichBolt {

    // Bolt发射器,用于向下游发送Tuple
    private OutputCollector collector;

    /**
     * prepare方法:Bolt初始化时调用
     * 类似于Spout的open方法,只执行一次
     * @param stormConf   Storm配置信息
     * @param context      Topology上下文
     * @param collector    输出发射器
     */
    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
        // 初始化OutputCollector发射器
        this.collector = collector;
    }

    /**
     * execute方法:Bolt的核心方法
     * 每接收到一个Tuple,该方法就会被调用一次
     * 在此方法中实现具体的处理逻辑
     * @param input  接收到的Tuple对象
     */
    @Override
    public void execute(Tuple input) {
        // 通过字段名 "sentence" 获取Tuple中对应的值
        String sentence = input.getStringByField("sentence");
        // 将句子按空格拆分为单词数组
        String[] words = sentence.split(" ");
        // 遍历每个单词,发射到下游Bolt
        for (String word : words) {
            // 发射一个包含单词字段的Tuple
            this.collector.emit(new Values(word));
        }
        // 确认(ack)该Tuple已经被成功处理
        // 这是Storm可靠消息处理机制的关键
        // 如果不ack,Spout会认为消息处理失败,会重新发送
        this.collector.ack(input);
    }

    /**
     * declareOutputFields方法:声明输出Tuple的字段名
     * @param declarer  输出字段声明器
     */
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        // 声明输出字段名为 "word"
        declarer.declare(new Fields("word"));
    }
}

三、Storm Topology(拓扑)

3.1 Topology 概述

Topology 是 Storm 中实时计算逻辑的封装 ,是一个由 Spout 和 Bolt 组成的 DAG(有向无环图)

复制代码
Topology 的特点:
  1. 一旦提交,将永不停止运行(除非手动kill)
  2. Spout 是数据源节点,Bolt 是处理节点
  3. 数据在 Spout 和 Bolt 之间通过 Stream 传递
  4. Topology 是 Storm 任务提交的基本单位

3.2 Topology 结构图

复制代码
              ┌──────────┐
              │  Spout   │  ← 数据源(如Kafka)
              └────┬─────┘
                   │ Stream
          ┌────────┼────────┐
          ▼        ▼        ▼
     ┌────────┐ ┌────────┐ ┌────────┐
     │ Bolt A │ │ Bolt A │ │ Bolt A │  ← 第一层Bolt(如拆分单词)
     └───┬────┘ └───┬────┘ └───┬────┘
         │          │          │
         └──────────┼──────────┘
                    ▼
              ┌──────────┐
              │  Bolt B  │  ← 第二层Bolt(如计数聚合)
              └──────────┘

3.3 Topology 构建代码

java 复制代码
/**
 * 构建并提交Storm Topology
 * TopologyBuilder 是构建Topology的核心工具类
 * 通过它可以设置Spout和Bolt之间的关系
 */
public class WordCountTopology {

    // 定义Spout的ID标识符
    private static final String SPOUT_ID = "sentence-spout";
    // 定义拆分Bolt的ID标识符
    private static final String SPLIT_BOLT_ID = "split-bolt";
    // 定义计数Bolt的ID标识符
    private static final String COUNT_BOLT_ID = "count-bolt";

    public static void main(String[] args) throws Exception {
        // 1. 创建TopologyBuilder实例
        TopologyBuilder builder = new TopologyBuilder();

        // 2. 设置Spout组件
        // setSpout方法参数说明:
        //   参数1:组件ID(字符串标识符)
        //   参数2:Spout实例
        //   参数3:并行度(executor数量,即该Spout的线程数)
        builder.setSpout(SPOUT_ID, new MySpout(), 2);

        // 3. 设置第一个Bolt组件(拆分单词)
        // setBolt方法参数说明:
        //   参数1:组件ID
        //   参数2:Bolt实例
        //   参数3:并行度
        // shuffleGrouping说明:
        //   定义该Bolt的数据来源以及数据分组方式
        //   参数1:上游组件ID
        //   shuffleGrouping表示随机分组,数据均匀随机分配到各task
        builder.setBolt(SPLIT_BOLT_ID, new SplitBolt(), 4)
               .shuffleGrouping(SPOUT_ID);

        // 4. 设置第二个Bolt组件(单词计数)
        // fieldsGrouping说明:
        //   相同字段值的Tuple会被发送到同一个task中处理
        //   这样可以保证同一个单词的计数在同一个task中完成
        builder.setBolt(COUNT_BOLT_ID, new CountBolt(), 4)
               .fieldsGrouping(SPLIT_BOLT_ID, new Fields("word"));

        // 5. 创建Storm配置
        Config config = new Config();
        // 设置Worker进程数量
        config.setNumWorkers(2);
        // 设置调试模式
        config.setDebug(true);

        // 6. 提交Topology
        if (args != null && args.length > 0) {
            // 集群模式:通过命令行参数指定Topology名称
            // StormSubmitter.submitTopology 提交到远程集群
            StormSubmitter.submitTopology(args[0], config, builder.createTopology());
        } else {
            // 本地模式:在本地JVM中运行Topology
            // 适合开发和调试
            LocalCluster cluster = new LocalCluster();
            cluster.submitTopology("word-count-topology", config, builder.createTopology());
            // 本地模式运行60秒后关闭
            Utils.sleep(60000);
            // 关闭本地集群
            cluster.shutdown();
        }
    }
}

3.4 完整单词计数 Bolt 代码

java 复制代码
/**
 * 单词计数Bolt
 * 接收上游Bolt发送的单词,进行计数统计
 * 继承 BaseRichBolt 类
 */
public class CountBolt extends BaseRichBolt {

    // Bolt发射器
    private OutputCollector collector;
    // 用于存储单词及其对应的计数
    // Key:单词字符串,Value:该单词出现的次数
    private Map<String, Long> wordCountMap;

    /**
     * prepare方法:初始化Bolt
     * @param stormConf  Storm配置
     * @param context    Topology上下文
     * @param collector  输出发射器
     */
    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
        // 初始化发射器
        this.collector = collector;
        // 初始化HashMap用于存储单词计数
        this.wordCountMap = new HashMap<>();
    }

    /**
     * execute方法:处理接收到的每个Tuple
     * 每接收到一个单词Tuple,就在map中进行累加计数
     * @param input  接收到的Tuple
     */
    @Override
    public void execute(Tuple input) {
        // 从Tuple中获取 "word" 字段的值
        String word = input.getStringByField("word");
        // 获取该单词当前的计数,如果不存在则为0
        Long count = wordCountMap.getOrDefault(word, 0L);
        // 计数加1,并更新到map中
        wordCountMap.put(word, count + 1);
        // 打印当前单词的计数结果到控制台
        System.out.println("单词: " + word + ", 计数: " + (count + 1));
        // 确认Tuple处理完成
        this.collector.ack(input);
    }

    /**
     * 声明输出字段
     * CountBolt作为最终输出节点,不需要声明输出字段
     * @param declarer  输出字段声明器
     */
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        // 最终输出节点,可以不声明输出字段
        // 如果需要输出,可以声明如 new Fields("word", "count")
    }
}

四、Storm 集群架构

4.1 架构组件

复制代码
┌─────────────────────────────────────────────────────────┐
│                   Storm 集群架构                         │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌──────────┐         ┌──────────────┐                  │
│  │  Nimbus   │◄───────►│   ZooKeeper  │                 │
│  │ (主节点)  │         │  (协调服务)   │                  │
│  └──────────┘         └──────┬───────┘                  │
│                              │                           │
│           ┌──────────────────┼──────────────────┐       │
│           ▼                  ▼                  ▼       │
│    ┌─────────────┐  ┌─────────────┐  ┌─────────────┐   │
│    │ Supervisor  │  │ Supervisor  │  │ Supervisor  │   │
│    │  (工作节点) │  │  (工作节点) │  │  (工作节点) │   │
│    ├─────────────┤  ├─────────────┤  ├─────────────┤   │
│    │  Worker     │  │  Worker     │  │  Worker     │   │
│    │  Worker     │  │  Worker     │  │  Worker     │   │
│    │  ┌───────┐  │  │  ┌───────┐  │  │  ┌───────┐  │   │
│    │  │Executor│  │  │  │Executor│  │  │  │Executor│  │   │
│    │  │Executor│  │  │  │Executor│  │  │  │Executor│  │   │
│    │  └───────┘  │  │  └───────┘  │  │  └───────┘  │   │
│    └─────────────┘  └─────────────┘  └─────────────┘   │
│                                                         │
└─────────────────────────────────────────────────────────┘

4.2 各组件详解

4.2.1 Nimbus(主节点)
复制代码
Nimbus 是 Storm 集群的"主节点",负责:
  1. 接收客户端提交的Topology
  2. 将Topology代码分发到各个Supervisor节点
  3. 分配Task给Supervisor
  4. 监控Topology的运行状态
  5. 处理Topology的失败重分配

特点:
  - Nimbus 是无状态的,所有状态存储在ZooKeeper中
  - Nimbus 进程挂掉后重启不会影响正在运行的Topology
  - Storm 1.x以后支持Nimbus HA(高可用)
4.2.2 Supervisor(工作节点)
复制代码
Supervisor 是 Storm 集群的"工作节点",负责:
  1. 接收Nimbus分配的任务
  2. 管理本节点上的Worker进程
  3. 启动或停止Worker进程
  4. 通过ZooKeeper与Nimbus通信

一个Supervisor节点可以运行多个Worker进程
4.2.3 Worker(工作进程)
复制代码
Worker 是运行在Supervisor节点上的JVM进程,负责:
  1. 执行Topology的一个子集(一个或多个Executor)
  2. 每个Worker进程执行一个Topology的一部分
  3. Worker进程之间通过Netty或ZeroMQ通信

一个Topology由多个Worker进程共同完成
4.2.4 Executor(执行线程)
复制代码
Executor 是 Worker 中的一个线程,负责:
  1. 执行一个或多个Task(同一类型的)
  2. 默认情况下,一个Executor执行一个Task
  3. Executor 是真正执行Spout或Bolt逻辑的线程
4.2.5 Task(任务)
复制代码
Task 是 Storm 中最小的处理单元:
  1. Task 对应一个 Spout 或 Bolt 的实例
  2. 默认情况下,一个Executor对应一个Task
  3. 可以通过 setNumTasks() 设置一个Executor对应多个Task

4.3 层级关系

复制代码
Topology
  └── Worker(进程级)
        └── Executor(线程级)
              └── Task(实例级)

举例:
  Topology设置为 2个Worker,每个Worker 2个Executor
  则总共有 4个Executor(4个线程)
  如果每个Executor运行1个Task,则总共有4个Task

4.4 ZooKeeper 在 Storm 中的作用

复制代码
ZooKeeper 在 Storm 集群中的角色:
  1. Nimbus 和 Supervisor 之间的通信桥梁
  2. 存储集群状态信息(Topology配置、Task分配等)
  3. 监控节点心跳,检测Supervisor是否存活
  4. 存储Task的错误信息和统计信息

注意:Storm 本身不存储大量数据在ZooKeeper中
      主要存储元数据和协调信息

五、Storm 流分组(Stream Grouping)

5.1 流分组概述

流分组(Stream Grouping)定义了Tuple 如何从 Spout/Bolt 发送到下游 Bolt 的各个 Task

5.2 七种内置分组方式

5.2.1 Shuffle Grouping(随机分组)
java 复制代码
/**
 * Shuffle Grouping(随机分组)
 * 
 * 特点:
 *   Tuple 随机均匀地分发到下游Bolt的各个Task中
 *   每个Tuple只发送到一个Task
 *   保证每个Task接收到大致相同数量的Tuple
 * 
 * 适用场景:
 *   不需要关心数据分配到哪个Task
 *   各Task处理逻辑完全独立
 */
builder.setBolt("split-bolt", new SplitBolt(), 4)
       // 随机分组:Tuple均匀随机分配到4个split-bolt的Task
       .shuffleGrouping("spout-id");
5.2.2 Fields Grouping(字段分组)
java 复制代码
/**
 * Fields Grouping(字段分组)
 * 
 * 特点:
 *   根据指定字段的值进行分组
 *   相同字段值的Tuple一定会被发送到同一个Task
 *   不同字段值可能被发送到不同Task
 *   类似于MapReduce中Partitioner的逻辑
 * 
 * 适用场景:
 *   需要保证相同Key的数据由同一个Task处理
 *   如:单词计数中,同一单词必须由同一Task计数
 */
builder.setBolt("count-bolt", new CountBolt(), 4)
       // 字段分组:相同"word"值的Tuple发送到同一个Task
       .fieldsGrouping("split-bolt-id", new Fields("word"));
5.2.3 All Grouping(全复制分组)
java 复制代码
/**
 * All Grouping(全复制分组)
 * 
 * 特点:
 *   每个Tuple被发送到下游Bolt的所有Task
 *   相当于广播(broadcast)
 * 
 * 适用场景:
 *   每个Task都需要处理所有数据
 *   如:全局配置更新、全局缓存刷新
 */
builder.setBolt("all-bolt", new AllBolt(), 4)
       // 全复制:每个Tuple发送到所有4个Task
       .allGrouping("spout-id");
5.2.4 Global Grouping(全局分组)
java 复制代码
/**
 * Global Grouping(全局分组)
 * 
 * 特点:
 *   所有Tuple都被发送到下游Bolt的某一个Task
 *   具体是哪个Task取决于Task ID最小的那个
 *   保证全局有序
 * 
 * 适用场景:
 *   需要集中处理所有数据
 *   如:全局统计、全局排序
 */
builder.setBolt("global-bolt", new GlobalBolt(), 4)
       // 全局分组:所有Tuple发送到ID最小的那个Task
       .globalGrouping("spout-id");
5.2.5 None Grouping(无分组)
java 复制代码
/**
 * None Grouping(无分组)
 * 
 * 特点:
 *   等价于 Shuffle Grouping
 *   目前和随机分组效果相同
 *   保留给未来使用
 * 
 * 说明:Storm开发团队保留该分组方式
 *       将来可能会有不同的语义
 */
builder.setBolt("none-bolt", new SomeBolt(), 4)
       // 无分组:等同于随机分组
       .noneGrouping("spout-id");
5.2.6 Direct Grouping(直接分组)
java 复制代码
/**
 * Direct Grouping(直接分组)
 * 
 * 特点:
 *   生产者(Bolt)直接指定Tuple发送到哪个Task
 *   需要使用 emitDirect() 方法发送
 *   必须知道目标Task的ID
 * 
 * 适用场景:
 *   需要精确控制数据路由
 */
// 在Bolt的execute方法中使用直接分组
public class DirectBolt extends BaseRichBolt {
    private OutputCollector collector;
    // 假设目标Task ID为0
    private int targetTaskId = 0;

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

    @Override
    public void execute(Tuple input) {
        // 直接发送到指定的Task ID
        collector.emitDirect(targetTaskId, new Values("direct-data"));
        collector.ack(input);
    }

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

// 在TopologyBuilder中设置
builder.setBolt("direct-bolt", new DirectBolt(), 4)
       // 直接分组
       .directGrouping("spout-id");
5.2.7 Local or Shuffle Grouping(本地或随机分组)
java 复制代码
/**
 * Local or Shuffle Grouping(本地或随机分组)
 * 
 * 特点:
 *   优先将Tuple发送到同一个Worker进程中的Task
 *   如果同一个Worker中没有目标Task,则退化为Shuffle Grouping
 *   减少网络传输,提高性能
 * 
 * 适用场景:
 *   性能优化场景
 *   尽量减少跨Worker的网络通信
 */
builder.setBolt("local-bolt", new SomeBolt(), 4)
       // 本地优先随机分组
       .localOrShuffleGrouping("spout-id");

5.3 自定义流分组

java 复制代码
/**
 * 自定义流分组
 * 实现 CustomStreamGrouping 接口
 * 可以自定义Tuple如何分配到下游Task
 */
public class MyCustomGrouping implements CustomStreamGrouping {

    // 存储所有目标Task的ID列表
    private List<Integer> targetTasks;

    /**
     * prepare方法:初始化分组
     * @param context      Topology上下文
     * @param stream       输入流信息
     * @param targetTasks  所有可能接收Tuple的Task ID列表
     */
    @Override
    public void prepare(WorkerTopologyContext context, 
                        GlobalStreamId stream, 
                        List<Integer> targetTasks) {
        // 保存目标Task列表
        this.targetTasks = targetTasks;
    }

    /**
     * chooseTasks方法:决定Tuple发送到哪个Task
     * @param values  Tuple的值列表
     * @return        目标Task ID的列表
     */
    @Override
    public List<Integer> chooseTasks(List<Object> values) {
        // 获取Tuple的第一个值(假设是字符串)
        String value = values.get(0).toString();
        // 使用字符串的hash值对Task数量取模
        // 保证相同字符串总是路由到同一个Task
        int index = Math.abs(value.hashCode()) % targetTasks.size();
        // 返回目标Task ID
        List<Integer> result = new ArrayList<>();
        result.add(targetTasks.get(index));
        return result;
    }
}

// 在Topology中使用自定义分组
builder.setBolt("custom-bolt", new SomeBolt(), 4)
       // 使用自定义分组
       .customGrouping("spout-id", new MyCustomGrouping());

5.4 分组方式对比总结

复制代码
┌──────────────────────┬───────────────────────────────────────┐
│     分组方式          │              特点                      │
├──────────────────────┼───────────────────────────────────────┤
│ Shuffle Grouping     │ 随机均匀分发,每个Tuple到一个Task       │
│ Fields Grouping      │ 按字段值分组,同值Tuple到同一Task       │
│ All Grouping         │ 广播,每个Tuple到所有Task               │
│ Global Grouping      │ 全局,所有Tuple到最小ID的Task           │
│ None Grouping        │ 等同于Shuffle Grouping                 │
│ Direct Grouping      │ 发送方指定目标Task                      │
│ Local or Shuffle     │ 优先本地,其次随机                      │
└──────────────────────┴───────────────────────────────────────┘

六、Storm 集群环境搭建

6.1 环境准备

bash 复制代码
# 系统要求
# 操作系统:CentOS 7.x / Ubuntu 18.04+
# JDK:JDK 1.8+
# ZooKeeper:3.4.x+
# Storm:2.x(本例使用 apache-storm-2.4.0)

# 1. 配置hosts文件(所有节点)
# 编辑 /etc/hosts
192.168.1.101  nimbus1
192.168.1.102  supervisor1
192.168.1.103  supervisor2
192.168.1.104  supervisor3

6.2 安装 ZooKeeper(所有节点)

bash 复制代码
# 解压ZooKeeper
tar -zxvf apache-zookeeper-3.7.0-bin.tar.gz -C /opt/module/

# 进入ZooKeeper目录
cd /opt/module/apache-zookeeper-3.7.0-bin

# 创建配置文件
cp conf/zoo_sample.cfg conf/zoo.cfg

# 编辑配置文件
# vi conf/zoo.cfg
# 配置内容如下:
tickTime=2000
initLimit=10
syncLimit=5
dataDir=/opt/module/apache-zookeeper-3.7.0-bin/zkData
clientPort=2181
# 集群配置
server.1=nimbus1:2888:3888
server.2=supervisor1:2888:3888
server.3=supervisor2:2888:3888

# 创建数据目录
mkdir -p /opt/module/apache-zookeeper-3.7.0-bin/zkData

# 在每个节点上创建myid文件
# 节点1 (nimbus1):
echo "1" > /opt/module/apache-zookeeper-3.7.0-bin/zkData/myid
# 节点2 (supervisor1):
echo "2" > /opt/module/apache-zookeeper-3.7.0-bin/zkData/myid
# 节点3 (supervisor2):
echo "3" > /opt/module/apache-zookeeper-3.7.0-bin/zkData/myid

# 启动ZooKeeper(每个节点都要执行)
bin/zkServer.sh start

# 检查状态
bin/zkServer.sh status

6.3 安装 Storm

bash 复制代码
# 1. 解压Storm安装包(所有节点)
tar -zxvf apache-storm-2.4.0.tar.gz -C /opt/module/

# 2. 配置环境变量(所有节点)
# vi /etc/profile
# 添加以下内容:
export STORM_HOME=/opt/module/apache-storm-2.4.0
export PATH=$PATH:$STORM_HOME/bin

# 使环境变量生效
source /etc/profile

# 3. 编辑Storm配置文件
# vi $STORM_HOME/conf/storm.yaml

6.4 Storm 配置文件详解

yaml 复制代码
########## storm.yaml 配置文件 ##########

# Storm ZooKeeper集群地址
# 多个地址用逗号分隔
storm.zookeeper.servers:
    - "nimbus1"
    - "supervisor1"
    - "supervisor2"

# ZooKeeper端口号
storm.zookeeper.port: 2181

# Storm本地数据存储目录
# 存储Topology的jar包、配置等
storm.local.dir: "/opt/module/apache-storm-2.4.0/storm-data"

# Nimbus节点地址
# 客户端通过该地址提交Topology
nimbus.host: "nimbus1"

# Nimbus RPC端口
nimbus.thrift.port: 6627

# Supervisor管理的端口范围
# 每个Worker进程占用一个端口
supervisor.slots.ports:
    - 6700
    - 6701
    - 6702
    - 6703

# Worker进程的堆内存大小(单位:MB)
worker.heap.memory.mb: 768

# Worker进程之间的通信方式
# 可选值:"netty" 或 "zmq"(推荐netty)
storm.messaging.transport: "org.apache.storm.messaging.netty.Context"

# UI监控端口
ui.port: 8080

# 日志目录
storm.log.dir: "/opt/module/apache-storm-2.4.0/logs"

6.5 启动集群

bash 复制代码
# 1. 启动ZooKeeper集群(每个ZK节点执行)
zkServer.sh start

# 2. 在Nimbus节点上启动Nimbus进程
# 前台启动(调试用):
storm nimbus
# 后台启动(生产用):
nohup storm nimbus &

# 3. 在Nimbus节点上启动UI监控服务
nohup storm ui &

# 4. 在每个Supervisor节点上启动Supervisor进程
nohup storm supervisor &

# 5. 在Nimbus节点上启动Logviewer(日志查看服务)
nohup storm logviewer &

# 6. 验证集群状态
# 查看进程
jps

# 浏览器访问Storm UI
# http://nimbus1:8080

6.6 提交 Topology

bash 复制代码
# 打包Topology为jar文件
mvn clean package

# 提交Topology到集群
storm jar target/storm-demo-1.0.jar \
    com.example.WordCountTopology \
    word-count-topology

# 查看当前运行的Topology
storm list

# 停止Topology
storm kill word-count-topology

# 重新平衡Topology(调整并行度)
storm rebalance word-count-topology -n 4 -e split-bolt=6

七、案例分析一:单词计数(Word Count)

7.1 设计思路

复制代码
整体流程:
  1. SentenceSpout → 随机产生英文句子
  2. SplitBolt → 将句子拆分为单词
  3. CountBolt → 统计每个单词出现的次数

数据流向:
  SentenceSpout --shuffleGrouping--> SplitBolt --fieldsGrouping--> CountBolt

组件说明:
  ┌────────────────┐     ┌──────────────┐     ┌──────────────┐
  │ SentenceSpout  │────>│  SplitBolt   │────>│  CountBolt   │
  │ 发射句子Tuple  │     │ 拆分单词     │     │ 统计计数     │
  │ 输出:sentence │     │ 输出:word   │     │ 输出:无     │
  └────────────────┘     └──────────────┘     └──────────────┘

7.2 Maven 项目配置

xml 复制代码
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>storm-wordcount</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <!-- Storm版本 -->
        <storm.version>2.4.0</storm.version>
        <!-- Java编译版本 -->
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <!-- 项目编码 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- Storm核心依赖 -->
        <dependency>
            <groupId>org.apache.storm</groupId>
            <artifactId>storm-client</artifactId>
            <version>${storm.version}</version>
            <!-- provided表示该依赖在集群上已经存在,打包时不包含 -->
            <scope>provided</scope>
        </dependency>

        <!-- Storm服务端依赖(本地模式运行需要) -->
        <dependency>
            <groupId>org.apache.storm</groupId>
            <artifactId>storm-server</artifactId>
            <version>${storm.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Maven打包插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

7.3 完整代码编写

7.3.1 SentenceSpout(数据源)
java 复制代码
package com.example.storm.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 org.apache.storm.utils.Utils;

import java.util.Map;
import java.util.Random;

/**
 * SentenceSpout - 数据源组件
 * 
 * 功能:随机产生英文句子,发射到Topology中
 * 继承 BaseRichSpout,这是Storm提供的Spout基础实现类
 * BaseRichSpout 提供了IRichSpout接口的默认实现
 * 开发者只需要重写需要的方法即可
 */
public class SentenceSpout extends BaseRichSpout {

    // ============================================
    // 成员变量定义
    // ============================================
    
    /**
     * SpoutOutputCollector:Spout的输出收集器
     * 用于将Tuple发送到下游的Bolt
     * 在open方法中由框架注入
     */
    private SpoutOutputCollector collector;

    /**
     * Random:随机数生成器
     * 用于随机选择句子
     */
    private Random random;

    /**
     * 模拟数据源的句子数组
     * 实际项目中,这里替换为从Kafka、数据库等读取数据
     */
    private String[] sentences = {
        "hello storm hello world",
        "apache storm is a real time",
        "big data processing framework",
        "storm is fast and reliable",
        "hello apache storm hello",
        "real time stream processing",
        "distributed and fault tolerant"
    };

    // ============================================
    // 核心方法实现
    // ============================================

    /**
     * open方法 - 初始化方法
     * 
     * 调用时机:Spout被初始化时调用一次
     * 功能:初始化资源(如数据库连接、文件句柄等)
     * 
     * @param conf       Storm配置(storm.yaml中的配置项)
     * @param context    Topology上下文信息
     *                   包含Topology ID、Task ID、Worker信息等
     * @param collector  输出收集器,用于向下游Bolt发送Tuple
     */
    @Override
    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
        // 将框架传入的collector保存为成员变量
        // 后续在nextTuple方法中使用它来发射数据
        this.collector = collector;
        // 初始化随机数生成器
        this.random = new Random();
        System.out.println("SentenceSpout 初始化完成,Task ID: " + context.getThisTaskId());
    }

    /**
     * nextTuple方法 - 核心数据发射方法
     * 
     * 调用时机:被Storm框架循环调用
     * 功能:产生/读取数据并发射Tuple
     * 
     * 注意事项:
     *   1. 该方法会被不断循环调用,如果没有数据应休眠
     *   2. 不要使用while(true)阻塞,会占用过多CPU
     *   3. 每次调用应该发射一个或零个Tuple
     */
    @Override
    public void nextTuple() {
        // 随机选择一个句子
        String sentence = sentences[random.nextInt(sentences.length)];
        
        // 使用collector发射一个Tuple
        // new Values(sentence) 等价于 new Values("hello storm hello world")
        // Values是ArrayList的子类,用于封装Tuple的字段值
        //
        // emit方法默认使用 "default" 流
        // 下游Bolt通过 shuffleGrouping("spout-id") 接收
        this.collector.emit(new Values(sentence));
        
        // 休眠500毫秒,控制数据发射频率
        // 避免产生过多数据导致系统过载
        // 生产环境中通常不需要休眠,因为数据来自外部系统
        Utils.sleep(500);
    }

    /**
     * declareOutputFields方法 - 声明输出字段
     * 
     * 功能:告诉Storm框架该Spout发射的Tuple包含哪些字段
     * 这些字段名用于下游Bolt通过字段名获取值
     * 
     * @param declarer  输出字段声明器
     */
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        // 声明输出包含一个字段,名为 "sentence"
        // 下游Bolt中可以通过以下方式获取值:
        //   tuple.getStringByField("sentence")
        //   或 tuple.getString(0)  -- 通过索引获取
        declarer.declare(new Fields("sentence"));
    }
}
7.3.2 SplitBolt(拆分单词)
java 复制代码
package com.example.storm.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;

/**
 * SplitBolt - 单词拆分组件
 * 
 * 功能:接收上游Spout发送的句子Tuple
 *       将句子拆分为单个单词
 *       将每个单词作为一个新的Tuple发射到下游
 * 
 * 继承 BaseRichBolt,这是Storm提供的Bolt基础实现类
 */
public class SplitBolt extends BaseRichBolt {

    // ============================================
    // 成员变量定义
    // ============================================
    
    /**
     * OutputCollector:Bolt的输出收集器
     * 用于向下游Bolt发送Tuple
     * 也用于ack(确认)/fail(失败)上游的Tuple
     */
    private OutputCollector collector;

    // ============================================
    // 核心方法实现
    // ============================================

    /**
     * prepare方法 - 初始化方法
     * 
     * 调用时机:Bolt被初始化时调用一次
     * 功能:初始化资源
     * 
     * 与Spout的open方法类似,但参数类型不同
     * Bolt使用 OutputCollector,Spout使用 SpoutOutputCollector
     * 
     * @param stormConf  Storm配置信息
     * @param context    Topology上下文信息
     * @param collector  输出收集器
     */
    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
        // 保存collector到成员变量
        this.collector = collector;
        System.out.println("SplitBolt 初始化完成,Task ID: " + context.getThisTaskId());
    }

    /**
     * execute方法 - 核心处理方法
     * 
     * 调用时机:每接收到一个Tuple,该方法被调用一次
     * 功能:处理接收到的数据,产生新的Tuple发送到下游
     * 
     * 重要:处理完成后必须调用 collector.ack(input) 确认
     *       否则Spout会认为消息处理失败,触发重发机制
     * 
     * @param input  接收到的上游Tuple
     */
    @Override
    public void execute(Tuple input) {
        // 第1步:从接收到的Tuple中获取句子
        // getStringByField通过字段名获取对应的值
        // 这里的 "sentence" 对应 SentenceSpout 中声明的字段名
        String sentence = input.getStringByField("sentence");
        
        // 第2步:将句子按空格拆分为单词数组
        // split(" ") 以空格为分隔符拆分字符串
        // 例如 "hello world" → ["hello", "world"]
        String[] words = sentence.split(" ");
        
        // 第3步:遍历每个单词,发射到下游Bolt
        for (String word : words) {
            // 过滤空白字符串(处理连续空格的情况)
            if (word != null && !word.trim().isEmpty()) {
                // 发射一个包含单词的Tuple到下游
                // new Values(word.trim()) 创建包含一个字段的Tuple
                // word.trim() 去除单词两端的空白字符
                this.collector.emit(new Values(word.trim()));
            }
        }
        
        // 第4步:确认Tuple处理完成
        // ack(acknowledge)告诉Storm框架该Tuple已被成功处理
        // Storm的可靠性机制会追踪每个Tuple的处理状态
        // 如果Tuple处理链路中某个环节失败,Spout可以重新发送
        this.collector.ack(input);
    }

    /**
     * declareOutputFields方法 - 声明输出字段
     * 
     * @param declarer  输出字段声明器
     */
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        // 声明输出包含一个字段,名为 "word"
        // 下游CountBolt通过 tuple.getStringByField("word") 获取单词
        declarer.declare(new Fields("word"));
    }
}
7.3.3 CountBolt(单词计数)
java 复制代码
package com.example.storm.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;

/**
 * CountBolt - 单词计数组件
 * 
 * 功能:接收上游SplitBolt发送的单词Tuple
 *       统计每个单词出现的次数
 *       输出计数结果
 * 
 * 注意:该Bolt使用 fieldsGrouping 接收数据
 *       保证相同单词总是由同一个Task处理
 *       这样才能正确地进行计数累加
 */
public class CountBolt extends BaseRichBolt {

    // ============================================
    // 成员变量定义
    // ============================================

    /**
     * OutputCollector:输出收集器
     */
    private OutputCollector collector;

    /**
     * wordCountMap:单词计数存储
     * Key:单词字符串
     * Value:该单词累计出现的次数
     * 
     * 因为使用了fieldsGrouping,相同的word总是发送到同一个Task
     * 所以同一个Task中的wordCountMap只包含部分单词的计数
     * 这不影响最终结果的正确性
     */
    private Map<String, Long> wordCountMap;

    // ============================================
    // 核心方法实现
    // ============================================

    /**
     * prepare方法 - 初始化方法
     * 
     * @param stormConf  Storm配置
     * @param context    Topology上下文
     * @param collector  输出收集器
     */
    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
        // 初始化输出收集器
        this.collector = collector;
        // 初始化单词计数Map
        // 使用HashMap存储每个单词的累计计数
        this.wordCountMap = new HashMap<>();
        System.out.println("CountBolt 初始化完成,Task ID: " + context.getThisTaskId());
    }

    /**
     * execute方法 - 核心处理方法
     * 
     * 每接收到一个单词Tuple,更新该单词的计数
     * 
     * @param input  接收到的Tuple
     */
    @Override
    public void execute(Tuple input) {
        // 第1步:从Tuple中获取单词
        // "word" 对应 SplitBolt 中声明的字段名
        String word = input.getStringByField("word");

        // 第2步:获取该单词当前的计数
        // getOrDefault方法:如果key存在返回对应的value,否则返回默认值0L
        // 0L 表示Long类型的0
        Long count = wordCountMap.getOrDefault(word, 0L);

        // 第3步:计数加1并更新到Map中
        wordCountMap.put(word, count + 1);

        // 第4步:输出结果
        // 格式:"单词: hello, 计数: 3"
        System.out.println("【单词计数结果】单词: " + word + ", 计数: " + (count + 1));

        // 第5步:发射结果Tuple到下游(可选)
        // 如果还有下游Bolt,可以通过emit发送
        // 如果是最终Bolt,可以不发射或发射到输出
        this.collector.emit(new Values(word, count + 1));

        // 第6步:确认Tuple处理完成
        this.collector.ack(input);
    }

    /**
     * declareOutputFields方法 - 声明输出字段
     * 
     * @param declarer  输出字段声明器
     */
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        // 声明输出字段:单词和计数
        declarer.declare(new Fields("word", "count"));
    }
}
7.3.4 WordCountTopology(拓扑构建与提交)
java 复制代码
package com.example.storm.wordcount;

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

/**
 * WordCountTopology - 单词计数拓扑
 * 
 * 功能:组装Spout和Bolt,构建Topology并提交运行
 * 
 * 拓扑结构:
 *   SentenceSpout ──(shuffle)──> SplitBolt ──(fields:word)──> CountBolt
 * 
 * 并行度设置:
 *   SentenceSpout: 2个executor(2个线程并行产生数据)
 *   SplitBolt:     4个executor(4个线程并行拆分单词)
 *   CountBolt:     4个executor(4个线程并行计数)
 */
public class WordCountTopology {

    // ============================================
    // 组件ID常量定义
    // ============================================
    
    /** Spout组件ID */
    private static final String SPOUT_ID = "sentence-spout";
    
    /** 拆分Bolt组件ID */
    private static final String SPLIT_BOLT_ID = "split-bolt";
    
    /** 计数Bolt组件ID */
    private static final String COUNT_BOLT_ID = "count-bolt";
    
    /** Topology名称 */
    private static final String TOPOLOGY_NAME = "word-count-topology";

    // ============================================
    // 主方法
    // ============================================

    /**
     * 主方法:构建并提交Topology
     * 
     * @param args  命令行参数
     *              本地模式:不传参数
     *              集群模式:args[0] = topology名称
     * @throws Exception  可能抛出的异常
     */
    public static void main(String[] args) throws Exception {

        // ============================================
        // 第1步:创建TopologyBuilder
        // ============================================
        // TopologyBuilder 是构建Topology的核心工具类
        // 它提供了设置Spout和Bolt的方法
        TopologyBuilder builder = new TopologyBuilder();

        // ============================================
        // 第2步:设置Spout组件
        // ============================================
        // setSpout方法说明:
        //   参数1 (SPOUT_ID): 组件的唯一标识符
        //   参数2 (new SentenceSpout()): Spout实例
        //   参数3 (2): 并行度,即executor数量(线程数)
        //
        // 每个executor运行一个Spout实例
        // 设置为2表示有2个线程同时运行SentenceSpout
        builder.setSpout(SPOUT_ID, new SentenceSpout(), 2);

        // ============================================
        // 第3步:设置SplitBolt组件
        // ============================================
        // setBolt方法说明:
        //   参数1 (SPLIT_BOLT_ID): Bolt的唯一标识符
        //   参数2 (new SplitBolt()): Bolt实例
        //   参数3 (4): 并行度
        //
        // shuffleGrouping(SPOUT_ID):
        //   数据来源:SPOUT_ID对应的SentenceSpout
        //   分组方式:shuffleGrouping(随机分组)
        //   效果:Spout发射的Tuple均匀随机分配到4个SplitBolt Task
        builder.setBolt(SPLIT_BOLT_ID, new SplitBolt(), 4)
               .shuffleGrouping(SPOUT_ID);

        // ============================================
        // 第4步:设置CountBolt组件
        // ============================================
        // fieldsGrouping(SPLIT_BOLT_ID, new Fields("word")):
        //   数据来源:SPLIT_BOLT_ID对应的SplitBolt
        //   分组方式:fieldsGrouping(字段分组)
        //   分组字段:"word"
        //   效果:相同单词的Tuple一定会被发送到同一个CountBolt Task
        //   这保证了同一个单词的计数在同一个Task中累加
        //
        // 例如:
        //   单词"hello" 始终发送到 CountBolt Task 0
        //   单词"storm"  始终发送到 CountBolt Task 1
        builder.setBolt(COUNT_BOLT_ID, new CountBolt(), 4)
               .fieldsGrouping(SPLIT_BOLT_ID, new Fields("word"));

        // ============================================
        // 第5步:创建配置对象
        // ============================================
        Config config = new Config();
        
        // 设置Worker进程数
        // 整个Topology运行在2个Worker进程中
        // 每个Worker是一个独立的JVM进程
        config.setNumWorkers(2);
        
        // 开启调试模式
        // 在调试模式下,Storm会记录更多的日志信息
        config.setDebug(true);
        
        // 设置消息超时时间(秒)
        // 如果一个Tuple在30秒内没有被完整处理,视为超时
        // 超时后Spout会重新发送该Tuple
        config.setMessageTimeoutSecs(30);

        // ============================================
        // 第6步:提交Topology
        // ============================================
        if (args != null && args.length > 0) {
            // ---- 集群模式 ----
            // 当通过命令行传入参数时,使用集群模式提交
            // StormSubmitter.submitTopology() 将Topology提交到远程Storm集群
            // args[0] 作为Topology的名称
            StormSubmitter.submitTopology(args[0], config, builder.createTopology());
            System.out.println("Topology [" + args[0] + "] 已提交到集群");
        } else {
            // ---- 本地模式 ----
            // 不传参数时使用本地模式
            // LocalCluster 在本地JVM中模拟一个Storm集群
            // 适合开发和调试阶段使用
            System.out.println("启动本地模式...");
            
            // 创建本地集群实例
            LocalCluster cluster = new LocalCluster();
            
            // 提交Topology到本地集群
            cluster.submitTopology(TOPOLOGY_NAME, config, builder.createTopology());
            System.out.println("Topology [" + TOPOLOGY_NAME + "] 已提交到本地集群");
            
            // 本地模式运行60秒
            // 实际生产中不会这样写,Topology会一直运行
            Utils.sleep(60000);
            
            // 关闭Topology
            cluster.killTopology(TOPOLOGY_NAME);
            System.out.println("Topology [" + TOPOLOGY_NAME + "] 已关闭");
            
            // 关闭本地集群
            cluster.shutdown();
            System.out.println("本地集群已关闭");
        }
    }
}

7.4 程序运行

bash 复制代码
# ===== 本地模式运行 =====
# 直接运行main方法,不传参数即可
# 在IDE中右键 Run 或执行:
mvn clean compile exec:java -Dexec.mainClass="com.example.storm.wordcount.WordCountTopology"

# ===== 集群模式运行 =====
# 1. 打包
mvn clean package

# 2. 提交到Storm集群
storm jar target/storm-wordcount-1.0-SNAPSHOT.jar \
    com.example.storm.wordcount.WordCountTopology \
    word-count-topology

# 3. 查看运行结果
# 方式一:查看Worker日志
tail -f $STORM_HOME/logs/workers-artifacts/word-count-topology/*/worker.log

# 方式二:访问Storm UI
# 浏览器打开 http://nimbus1:8080

# 4. 停止Topology
storm kill word-count-topology
复制代码
预期输出结果示例:
========================================
【单词计数结果】单词: hello, 计数: 1
【单词计数结果】单词: storm, 计数: 1
【单词计数结果】单词: hello, 计数: 2
【单词计数结果】单词: world, 计数: 1
【单词计数结果】单词: apache, 计数: 1
【单词计数结果】单词: storm, 计数: 2
【单词计数结果】单词: is, 计数: 1
【单词计数结果】单词: a, 计数: 1
【单词计数结果】单词: real, 计数: 1
【单词计数结果】单词: time, 计数: 1
【单词计数结果】单词: hello, 计数: 3
...
========================================

八、案例分析二:Storm 与 Kafka 整合

8.1 设计思路

复制代码
整体架构:
  Kafka Topic ──> KafkaSpout ──> SplitBolt ──> CountBolt ──> 存储/输出

数据流向:
  ┌──────────┐    ┌────────────┐    ┌────────────┐    ┌────────────┐
  │  Kafka   │───>│  KafkaSpout │───>│  SplitBolt  │───>│  CountBolt │
  │  Topic   │    │ 读取Kafka   │    │ 拆分单词    │    │ 单词计数    │
  │(数据源)  │    │ 消息        │    │             │    │             │
  └──────────┘    └────────────┘    └────────────┘    └────────────┘

组件说明:
  1. KafkaSpout:Storm提供的官方Kafka集成Spout
                 从Kafka Topic中消费消息
                 自动管理消费偏移量(offset)
                 支持消息可靠处理(at-least-once语义)
  
  2. SplitBolt:  将Kafka消息(句子)拆分为单词
  
  3. CountBolt:  统计每个单词的出现次数

Kafka Topic设计:
  Topic名称:storm-input
  分区数:3
  消息格式:每条消息为一个英文句子

8.2 Maven 依赖配置

xml 复制代码
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>storm-kafka-integration</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <storm.version>2.4.0</storm.version>
        <kafka.version>3.3.1</kafka.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- ==================== Storm 核心依赖 ==================== -->
        <!-- Storm客户端库:包含TopologyBuilder、Config等核心类 -->
        <dependency>
            <groupId>org.apache.storm</groupId>
            <artifactId>storm-client</artifactId>
            <version>${storm.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- Storm服务端库:本地模式运行需要 -->
        <dependency>
            <groupId>org.apache.storm</groupId>
            <artifactId>storm-server</artifactId>
            <version>${storm.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- ==================== Storm-Kafka 集成依赖 ==================== -->
        <!-- Storm官方提供的Kafka集成组件 -->
        <!-- 包含KafkaSpout、KafkaSpoutConfig等类 -->
        <dependency>
            <groupId>org.apache.storm</groupId>
            <artifactId>storm-kafka-client</artifactId>
            <version>${storm.version}</version>
        </dependency>

        <!-- ==================== Kafka 客户端依赖 ==================== -->
        <!-- Kafka Java客户端库 -->
        <dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>${kafka.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Maven Shade插件:将所有依赖打成一个fat jar -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <!-- 解决多个jar中META-INF冲突问题 -->
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.example.storm.kafka.KafkaWordCountTopology</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

8.3 Kafka 环境准备

bash 复制代码
# 1. 创建Kafka Topic
# 进入Kafka安装目录
cd /opt/module/kafka_2.13-3.3.1

# 创建Topic:storm-input
# --partitions 3:创建3个分区
# --replication-factor 1:副本因子为1(测试环境)
bin/kafka-topics.sh --create \
    --bootstrap-server localhost:9092 \
    --topic storm-input \
    --partitions 3 \
    --replication-factor 1

# 2. 验证Topic创建成功
bin/kafka-topics.sh --describe \
    --bootstrap-server localhost:9092 \
    --topic storm-input

# 3. 启动生产者(用于测试发送数据)
bin/kafka-console-producer.sh \
    --bootstrap-server localhost:9092 \
    --topic storm-input

# 在生产者控制台输入测试数据:
# hello storm hello world
# apache storm is great
# big data streaming processing
# hello kafka integration with storm

8.4 完整代码编写

8.4.1 SplitSentenceBolt(拆分句子Bolt)
java 复制代码
package com.example.storm.kafka;

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;

/**
 * SplitSentenceBolt - 句子拆分Bolt
 * 
 * 功能:
 *   接收KafkaSpout发送的Kafka消息(一个Tuple对应Kafka中的一条消息)
 *   将消息内容(句子)拆分为单个单词
 *   将每个单词发射到下游CountBolt
 * 
 * 数据流转:
 *   KafkaSpout ──Tuple(value="hello storm")──> SplitSentenceBolt
 *   SplitSentenceBolt ──Tuple(word="hello")──> CountBolt
 *   SplitSentenceBolt ──Tuple(word="storm")──> CountBolt
 */
public class SplitSentenceBolt extends BaseRichBolt {

    // 输出收集器
    private OutputCollector collector;

    /**
     * 初始化方法
     * 
     * @param stormConf  Storm配置
     * @param context    Topology上下文
     * @param collector  输出收集器
     */
    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
        // 保存输出收集器
        this.collector = collector;
    }

    /**
     * 处理接收到的Tuple
     * 
     * KafkaSpout发送的Tuple中,消息内容存储在 "value" 字段中
     * 也可以通过 tuple.getValue(0) 获取第一条消息内容
     * 
     * @param input  接收到的Tuple
     */
    @Override
    public void execute(Tuple input) {
        try {
            // 第1步:从Tuple中获取Kafka消息内容
            // KafkaSpout默认将消息的value存储在 "value" 字段中
            // getStringByField("value") 获取消息的字符串值
            String message = input.getStringByField("value");
            
            // 打印接收到的原始消息(调试用)
            System.out.println("【SplitBolt】接收到Kafka消息: " + message);

            // 第2步:将消息(句子)拆分为单词
            // 使用正则表达式 "\\s+" 匹配一个或多个空白字符
            // 这样可以处理连续空格、制表符等情况
            String[] words = message.split("\\s+");

            // 第3步:遍历单词并发射
            for (String word : words) {
                // 去除两端空白和标点符号
                // 转换为小写,保证计数时大小写一致
                String cleanWord = word.trim().toLowerCase()
                                      .replaceAll("[^a-zA-Z0-9]", "");
                
                // 过滤空字符串
                if (!cleanWord.isEmpty()) {
                    // 发射单词Tuple到下游CountBolt
                    // 使用 "word" 字段名
                    this.collector.emit(new Values(cleanWord));
                }
            }

            // 第4步:确认Tuple处理成功
            this.collector.ack(input);
            
        } catch (Exception e) {
            // 异常处理:标记Tuple处理失败
            // fail方法告诉Storm框架该Tuple处理失败
            // Storm会通知KafkaSpout进行重试
            System.err.println("【SplitBolt】处理Tuple异常: " + e.getMessage());
            this.collector.fail(input);
        }
    }

    /**
     * 声明输出字段
     * 
     * @param declarer  输出字段声明器
     */
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        // 声明输出字段为 "word"
        declarer.declare(new Fields("word"));
    }
}
8.4.2 WordCountBolt(单词计数Bolt)
java 复制代码
package com.example.storm.kafka;

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.Tuple;

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

/**
 * WordCountBolt - 单词计数Bolt
 * 
 * 功能:
 *   接收SplitSentenceBolt发送的单词Tuple
 *   使用HashMap统计每个单词的出现次数
 *   实时打印计数结果
 * 
 * 注意:
 *   该Bolt作为最终处理节点,不需要发射Tuple到下游
 *   所以不需要在execute中调用emit方法
 *   也不需要声明输出字段
 */
public class WordCountBolt extends BaseRichBolt {

    // 输出收集器
    private OutputCollector collector;
    
    /**
     * 单词计数Map
     * 使用HashMap存储每个单词的出现次数
     * Key:单词字符串
     * Value:累计出现次数
     */
    private Map<String, Long> wordCountMap;

    /**
     * 初始化方法
     * 
     * @param stormConf  Storm配置
     * @param context    Topology上下文
     * @param collector  输出收集器
     */
    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
        // 初始化输出收集器
        this.collector = collector;
        // 初始化计数Map
        this.wordCountMap = new HashMap<>();
    }

    /**
     * 处理接收到的单词Tuple
     * 
     * @param input  接收到的Tuple
     */
    @Override
    public void execute(Tuple input) {
        try {
            // 第1步:获取单词
            String word = input.getStringByField("word");

            // 第2步:获取当前计数并加1
            // getOrDefault:如果Map中存在该key则返回对应value,否则返回默认值0L
            long newCount = wordCountMap.getOrDefault(word, 0L) + 1;
            
            // 第3步:更新Map中的计数
            wordCountMap.put(word, newCount);

            // 第4步:打印实时计数结果
            // 输出格式:【Kafka词频统计】单词: hello, 当前计数: 3
            System.out.println("【Kafka词频统计】单词: " + word + 
                             ", 当前计数: " + newCount +
                             ", 总共统计单词数: " + wordCountMap.size());

            // 第5步:确认处理成功
            this.collector.ack(input);
            
        } catch (Exception e) {
            // 异常处理:标记Tuple处理失败
            System.err.println("【CountBolt】处理Tuple异常: " + e.getMessage());
            this.collector.fail(input);
        }
    }

    /**
     * 声明输出字段
     * 
     * 作为最终Bolt,没有下游Bolt,可以不声明输出字段
     * 
     * @param declarer  输出字段声明器
     */
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        // 最终Bolt,不声明输出字段
        // 如果需要输出到外部存储,可以在此声明字段
    }
}
8.4.3 KafkaWordCountTopology(拓扑构建)
java 复制代码
package com.example.storm.kafka;

import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.StormSubmitter;
import org.apache.storm.kafka.spout.KafkaSpout;
import org.apache.storm.kafka.spout.KafkaSpoutConfig;
import org.apache.storm.topology.TopologyBuilder;
import org.apache.storm.tuple.Fields;
import org.apache.storm.utils.Utils;

/**
 * KafkaWordCountTopology - Storm与Kafka整合的单词计数拓扑
 * 
 * 功能:
 *   从Kafka Topic中消费消息
 *   实现单词拆分和计数统计
 * 
 * 拓扑结构:
 *   KafkaSpout ──(shuffle)──> SplitSentenceBolt ──(fields:word)──> WordCountBolt
 * 
 * 关键知识点:
 *   1. KafkaSpoutConfig:配置Kafka消费者参数
 *   2. KafkaSpout:从Kafka消费消息的Spout
 *   3. 消费者组:Storm使用消费者组管理Kafka偏移量
 */
public class KafkaWordCountTopology {

    // ============================================
    // 常量定义
    // ============================================

    /** Kafka Broker地址 */
    private static final String KAFKA_BROKERS = "nimbus1:9092,supervisor1:9092,supervisor2:9092";
    
    /** Kafka Topic名称 */
    private static final String KAFKA_TOPIC = "storm-input";
    
    /** Kafka消费者组ID */
    private static final String KAFKA_GROUP_ID = "storm-consumer-group";
    
    /** Topology名称 */
    private static final String TOPOLOGY_NAME = "kafka-word-count-topology";

    /** 组件ID */
    private static final String KAFKA_SPOUT_ID = "kafka-spout";
    private static final String SPLIT_BOLT_ID = "split-bolt";
    private static final String COUNT_BOLT_ID = "count-bolt";

    // ============================================
    // 主方法
    // ============================================

    /**
     * 主方法:构建并提交Kafka-Storm整合拓扑
     * 
     * @param args  命令行参数
     * @throws Exception  异常
     */
    public static void main(String[] args) throws Exception {

        // ============================================
        // 第1步:创建TopologyBuilder
        // ============================================
        TopologyBuilder builder = new TopologyBuilder();

        // ============================================
        // 第2步:配置KafkaSpout
        // ============================================
        
        /**
         * 创建KafkaSpoutConfig
         * 
         * KafkaSpoutConfig是配置KafkaSpout的核心类
         * 它定义了Kafka消费者的连接参数和行为
         */
        KafkaSpoutConfig<String, String> kafkaSpoutConfig = KafkaSpoutConfig
                // 设置Kafka Broker地址和Topic
                // firstBootstrapBrokers:Kafka Broker列表
                // withStringDeserializer:Key和Value的反序列化器
                .builder(KAFKA_BROKERS, KAFKA_TOPIC)
                
                // 设置消费者组ID
                // Storm使用该ID在Kafka中管理消费偏移量(offset)
                // 不同的消费者组独立消费,互不影响
                .setGroupId(KAFKA_GROUP_ID)
                
                // 设置Kafka消费者属性
                // 处理偏移量重置策略
                // earliest:从最早的可用消息开始消费
                // 可选值:"earliest"、"latest"、"none"
                .setProp(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")
                
                // 设置每次拉取的最大记录数
                // 控制每次poll()方法返回的最大消息数
                // 避免一次拉取过多消息导致内存溢出
                .setProp(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500)
                
                // 设置记录处理的重试策略
                // 第一个参数:重试次数(1表示最多重试1次)
                // 第二个参数:重试间隔时间
                .setRetry(new KafkaSpoutConfig.RetryService(1, 2000))
                
                // 构建配置对象
                .build();

        // ============================================
        // 第3步:设置Spout和Bolt组件
        // ============================================
        
        // 设置KafkaSpout
        // KafkaSpout是Storm官方提供的Kafka集成Spout
        // 它封装了Kafka消费者,自动处理:
        //   - 消费者与Kafka的连接
        //   - 消息消费和偏移量管理
        //   - 消息可靠处理(at-least-once)
        //   - 消费者Rebalance
        builder.setSpout(KAFKA_SPOUT_ID, new KafkaSpout<>(kafkaSpoutConfig), 3);
        // 并行度为3,对应Kafka Topic的3个分区
        // 每个KafkaSpout Task处理一个Kafka分区

        // 设置SplitSentenceBolt
        // 随机分组:KafkaSpout的消息均匀分配到各个SplitBolt Task
        builder.setBolt(SPLIT_BOLT_ID, new SplitSentenceBolt(), 4)
               .shuffleGrouping(KAFKA_SPOUT_ID);
        // shuffleGrouping确保消息均匀分配到4个SplitBolt Task
        // 每条消息只被一个SplitBolt Task处理

        // 设置WordCountBolt
        // 字段分组:保证相同单词由同一个CountBolt Task处理
        builder.setBolt(COUNT_BOLT_ID, new WordCountBolt(), 4)
               .fieldsGrouping(SPLIT_BOLT_ID, new Fields("word"));
        // fieldsGrouping("word") 确保:
        //   单词"hello"始终发送到同一个CountBolt Task
        //   单词"storm"始终发送到同一个CountBolt Task
        // 这样每个Task都能正确累计对应单词的计数

        // ============================================
        // 第4步:创建配置
        // ============================================
        Config config = new Config();
        
        // 设置Worker进程数
        config.setNumWorkers(3);
        
        // 设置消息超时时间
        // 如果Tuple在指定时间内没有被ack,视为处理失败
        config.setMessageTimeoutSecs(60);
        
        // 开启调试模式(生产环境应关闭)
        config.setDebug(false);
        
        // 设置每个Topology的Kafka客户端最大偏移量滞后
        // 用于监控Kafka消费延迟

        // ============================================
        // 第5步:提交Topology
        // ============================================
        if (args != null && args.length > 0) {
            // 集群模式
            StormSubmitter.submitTopology(args[0], config, builder.createTopology());
            System.out.println("Topology [" + args[0] + "] 已提交到集群");
        } else {
            // 本地模式
            System.out.println("以本地模式启动 Kafka-Storm 单词计数...");
            
            LocalCluster cluster = new LocalCluster();
            cluster.submitTopology(TOPOLOGY_NAME, config, builder.createTopology());
            System.out.println("Topology 已提交,等待Kafka消息...");
            System.out.println("请在Kafka生产者中输入英文句子进行测试");
            
            // 本地模式运行120秒
            Utils.sleep(120000);
            
            // 关闭
            cluster.killTopology(TOPOLOGY_NAME);
            cluster.shutdown();
            System.out.println("Topology已关闭");
        }
    }
}

注意:上述代码中 KafkaSpoutConfig.RetryServiceConsumerConfig 需要正确导入:

java 复制代码
// 需要的导入语句
import org.apache.kafka.clients.consumer.ConsumerConfig;  // Kafka消费者配置类
8.4.4 使用 Builder 模式的简化版 KafkaSpoutConfig
java 复制代码
/**
 * KafkaSpoutConfig 配置详解
 * 
 * 这是创建KafkaSpoutConfig的更详细方式
 * 包含了更多的配置选项说明
 */
public class KafkaSpoutConfigBuilder {

    /**
     * 创建并返回KafkaSpoutConfig对象
     * 
     * @return 配置好的KafkaSpoutConfig
     */
    public static KafkaSpoutConfig<String, String> createKafkaSpoutConfig() {
        
        // 创建KafkaSpoutConfig
        KafkaSpoutConfig<String, String> config = KafkaSpoutConfig
                
                // 基本连接配置
                // 参数1:Kafka Broker地址(多个用逗号分隔)
                // 参数2:要消费的Topic名称
                .builder("nimbus1:9092", "storm-input")
                
                // ========== 反序列化配置 ==========
                // 设置Key的反序列化器
                // StringDeserializer将byte[]转为String
                .setProp(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, 
                        "org.apache.kafka.common.serialization.StringDeserializer")
                // 设置Value的反序列化器
                .setProp(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, 
                        "org.apache.kafka.common.serialization.StringDeserializer")
                
                // ========== 消费者组配置 ==========
                // 消费者组ID
                // 相同group.id的消费者属于同一组
                // 同一组内的消费者共同消费Topic的所有分区
                .setGroupId("my-storm-consumer-group")
                
                // ========== Offset管理配置 ==========
                // 当消费者组首次消费(没有已提交的offset)时的策略
                // "earliest":从Topic最早的消息开始消费
                // "latest":从最新消息开始消费
                // "none":抛出异常
                .setProp(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")
                
                // 是否自动提交offset
                // false:由Storm管理offset提交(推荐)
                .setProp(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false)
                
                // ========== 性能配置 ==========
                // 每次poll()的最大记录数
                .setProp(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 100)
                
                // ========== 构建 ==========
                .build();
        
        return config;
    }
}

8.5 程序运行

bash 复制代码
# ===== 准备工作 =====

# 1. 启动ZooKeeper集群
# 每个ZK节点执行:
zkServer.sh start

# 2. 启动Kafka集群
# 每个Kafka节点执行:
kafka-server-start.sh -daemon $KAFKA_HOME/config/server.properties

# 3. 启动Storm集群
# Nimbus节点:
storm nimbus &
storm ui &
# 每个Supervisor节点:
storm supervisor &

# 4. 创建Kafka Topic(如果还没创建)
kafka-topics.sh --create \
    --bootstrap-server nimbus1:9092 \
    --topic storm-input \
    --partitions 3 \
    --replication-factor 2

# ===== 运行程序 =====

# 5. 打包项目
mvn clean package

# 6. 提交Topology到Storm集群
storm jar target/storm-kafka-integration-1.0-SNAPSHOT.jar \
    com.example.storm.kafka.KafkaWordCountTopology \
    kafka-word-count-topology

# 7. 启动Kafka生产者,输入测试数据
kafka-console-producer.sh \
    --bootstrap-server nimbus1:9092 \
    --topic storm-input

# 在生产者控制台输入:
# > hello storm hello world
# > apache storm is great
# > hello kafka integration with storm
# > big data real time processing
# > hello storm kafka hello

# 8. 查看Storm Worker日志,观察输出结果
tail -f $STORM_HOME/logs/workers-artifacts/kafka-word-count-topology/*/worker.log

# ===== 预期输出 =====
# 【SplitBolt】接收到Kafka消息: hello storm hello world
# 【Kafka词频统计】单词: hello, 当前计数: 1, 总共统计单词数: 1
# 【Kafka词频统计】单词: storm, 当前计数: 1, 总共统计单词数: 2
# 【Kafka词频统计】单词: hello, 当前计数: 2, 总共统计单词数: 2
# 【Kafka词频统计】单词: world, 当前计数: 1, 总共统计单词数: 3
# 【SplitBolt】接收到Kafka消息: apache storm is great
# 【Kafka词频统计】单词: apache, 当前计数: 1, 总共统计单词数: 4
# 【Kafka词频统计】单词: storm, 当前计数: 2, 总共统计单词数: 4
# ...

# 9. 停止Topology
storm kill kafka-word-count-topology

九、Storm 可靠性机制详解

9.1 Tuple 树(Tuple Tree)

复制代码
Storm可靠性机制的核心是Tuple Tree(元组树):

  Spout发射一个Tuple
       │
       ├── Bolt A 处理后发射新Tuple
       │        ├── Bolt B 处理
       │        └── Bolt C 处理
       └── Bolt D 处理后发射新Tuple
                └── Bolt E 处理

  这棵树中所有的Tuple都是从Spout最初发射的Tuple衍生出来的
  
  如果整棵树中的所有Tuple都被成功ack → Spout调用ack()
  如果树中任何一个Tuple失败或超时 → Spout调用fail()

9.2 可靠性 API

java 复制代码
/**
 * 演示Storm可靠性机制的Bolt实现
 */
public class ReliableBolt extends BaseRichBolt {
    
    private OutputCollector collector;
    
    @Override
    public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
        this.collector = collector;
    }
    
    @Override
    public void execute(Tuple input) {
        try {
            // ---- 处理业务逻辑 ----
            String data = input.getStringByField("word");
            
            // ---- 锚定(Anchoring)发射 ----
            // 锚定发射:将新发射的Tuple与输入Tuple关联
            // 这样新Tuple也成为Tuple Tree的一部分
            // 如果新Tuple没有被下游ack,输入Tuple也会被认为处理失败
            this.collector.emit(input, new Values(data.toUpperCase()));
            
            // ---- 非锚定发射 ----
            // 非锚定发射:新Tuple与输入Tuple无关
            // 新Tuple的处理失败不会影响输入Tuple
            // this.collector.emit(new Values(data.toUpperCase()));
            
            // ---- 多Tuple锚定发射 ----
            // 一次锚定多个输出Tuple
            // this.collector.emit(input, new Values("a"));
            // this.collector.emit(input, new Values("b"));
            
            // ---- 确认输入Tuple ----
            // 告诉Storm框架该Tuple处理成功
            // 所有下游Tuple都ack后,Spout才会收到ack
            this.collector.ack(input);
            
        } catch (Exception e) {
            // ---- 标记失败 ----
            // 告诉Storm框架该Tuple处理失败
            // 会导致Spout重新发送原始Tuple
            this.collector.fail(input);
        }
    }
    
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("upperWord"));
    }
}

9.3 Spout 可靠性

java 复制代码
/**
 * 支持可靠性机制的Spout实现
 */
public class ReliableSpout extends BaseRichSpout {
    
    private SpoutOutputCollector collector;
    
    // 存储已发射但未确认的Tuple
    // Key:消息ID,Value:消息内容
    // 用于失败重发
    private Map<Long, String> pendingMessages;
    
    // 消息ID生成器
    private long msgId = 0;
    
    @Override
    public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
        this.collector = collector;
        this.pendingMessages = new HashMap<>();
    }
    
    @Override
    public void nextTuple() {
        String message = "hello storm";
        long id = msgId++;
        
        // 存储待确认的消息
        pendingMessages.put(id, message);
        
        // 发射Tuple时指定消息ID(第一个参数)
        // 这个ID会在ack()或fail()回调中传回
        this.collector.emit(new Values(message), id);
    }
    
    /**
     * ack回调方法
     * 当Tuple Tree中所有Tuple都被成功处理后调用
     * @param msgId  消息ID
     */
    @Override
    public void ack(Object msgId) {
        // 从pending中移除已确认的消息
        pendingMessages.remove(msgId);
        System.out.println("消息处理成功,ID: " + msgId);
    }
    
    /**
     * fail回调方法
     * 当Tuple Tree中任何一个Tuple处理失败或超时时调用
     * @param msgId  消息ID
     */
    @Override
    public void fail(Object msgId) {
        // 获取失败的消息
        String message = pendingMessages.get(msgId);
        System.err.println("消息处理失败,ID: " + msgId + ",将重新发送: " + message);
        
        // 重新发送失败的消息
        this.collector.emit(new Values(message), msgId);
    }
    
    @Override
    public void declareOutputFields(OutputFieldsDeclarer declarer) {
        declarer.declare(new Fields("message"));
    }
}

十、Storm 配置参数详解

10.1 Topology 级别配置

java 复制代码
/**
 * Topology级别常用配置参数
 * 在提交Topology时通过Config对象设置
 */
public class StormConfigExample {

    public static Config createConfig() {
        Config config = new Config();
        
        // ========== 并行度相关 ==========
        
        // Worker进程数量
        // 每个Worker是一个独立的JVM进程
        // 总Worker数 = 设置的Worker数
        config.setNumWorkers(4);
        
        // ========== 可靠性相关 ==========
        
        // 消息超时时间(秒)
        // Tuple从Spout发射开始计时
        // 如果超过该时间Tuple树没有完全ack,Spout的fail()被调用
        // 默认30秒
        config.setMessageTimeoutSecs(60);
        
        // 是否跳过已过期的Tuple
        // true:如果Tuple超时,Spout的ack/fail不会被调用
        // 默认false
        config.setSkipMissingKryoRegistrations(true);
        
        // ========== 调试相关 ==========
        
        // 调试模式
        // true:记录更详细的日志
        config.setDebug(false);
        
        // Topology运行的最大时间(毫秒)
        // 0表示永不过期
        config.setMaxTaskParallelism(10);
        
        // ========== 序列化相关 ==========
        
        // 注册自定义序列化器
        // 如果Tuple中包含自定义对象,需要注册对应的序列化器
        // config.registerSerialization(MyCustomClass.class);
        
        return config;
    }
}

10.2 Component 级别并行度

java 复制代码
/**
 * Component级别并行度设置
 * 在TopologyBuilder中设置
 */
public class ParallelismExample {

    public static void main(String[] args) {
        TopologyBuilder builder = new TopologyBuilder();

        // ========== 并行度参数说明 ==========
        
        // 参数1:组件ID
        // 参数2:组件实例
        // 参数3:并行度(executor数量,即线程数)
        
        // 设置Spout并行度为2(2个线程)
        builder.setSpout("spout", new MySpout(), 2);
        
        // 设置Bolt并行度为6(6个线程)
        // 并通过setNumTasks设置Task数量为12
        // 意味着6个executor平均每个executor运行2个task
        builder.setBolt("bolt", new MyBolt(), 6)
               .setNumTasks(12)    // 设置Task总数为12
               .shuffleGrouping("spout");
        
        // ========== 并行度关系 ==========
        // executor(线程)数 <= task(任务)数
        // 
        // 示例:
        //   executor数=6, task数=12
        //   每个executor运行 12/6 = 2 个task
        //
        //   executor数=6, task数=6(默认)
        //   每个executor运行 1 个task
    }
}

十一、知识要点总结

11.1 核心概念总结

复制代码
1. Storm 是分布式实时流处理框架
2. Topology = Spout + Bolt 组成的DAG
3. Spout 是数据源头,Bolt 是处理单元
4. Tuple 是数据传输的基本单元
5. Stream 是无限的Tuple序列
6. Stream Grouping 控制Tuple如何路由到Bolt Task
7. 可靠性机制通过Tuple Tree和ack/fail实现

11.2 流分组选择指南

复制代码
场景                          → 推荐分组方式
───────────────────────────────────────────
无需数据关联,均匀分配          → shuffleGrouping
相同Key数据需要同一Task处理     → fieldsGrouping
需要广播到所有Task              → allGrouping
需要集中到一个Task处理          → globalGrouping
性能优先,优先本地Task          → localOrShuffleGrouping
需要精确控制路由                → directGrouping
自定义路由逻辑                  → customGrouping

11.3 Storm vs 其他流处理框架

复制代码
┌──────────┬────────────┬───────────┬───────────┐
│ 特性      │ Storm      │ Spark     │ Flink     │
│          │            │ Streaming │           │
├──────────┼────────────┼───────────┼───────────┤
│ 处理模型  │ 逐条处理    │ 微批处理  │ 逐条处理   │
│ 延迟     │ 毫秒级      │ 秒级      │ 毫秒级     │
│ 吞吐量   │ 中等        │ 高        │ 高        │
│ 状态管理  │ 无内置      │ 有        │ 有        │
│ 窗口支持  │ 有限        │ 有        │ 丰富      │
│ 精确一次  │ at-least-once│ exactly-once│ exactly-once│
│ 编程复杂度│ 简单        │ 中等      │ 中等      │
│ 生态系统  │ 成熟        │ 非常成熟  │ 快速成长   │
└──────────┴────────────┴───────────┴───────────┘