TL;DR Storm 与传统 Java 程序有三大关键区别:main 方法仅在提交节点运行、初始化须在生命周期方法中完成、数据传输必须可序列化。
Storm(以及阿里巴巴开源的 JStorm)是一个分布式实时计算框架。它的编程模型跟传统 Java 程序有一些关键区别,如果不了解这些区别,很容易写出在本地跑得好好的、部署到集群就出各种诡异 bug 的代码。
本文不深入 Storm 内核原理,而是聚焦开发者最需要知道的几个关键区别,帮你快速上手并避开常见坑。
Storm 运行机制速览
一个 Storm Topology 由若干 Spout(数据源)和 Bolt(处理逻辑)组成,它们分散运行在多个 Worker(进程)中。同一个 Worker 可能同时运行多个 Spout/Bolt 的多个线程。
关键要理解的是:你的代码会被序列化后分发到各个 Worker 上执行,而不是在提交 Topology 的那台机器上运行。
与传统 Java 程序的三大关键区别
1. main 方法只跑在 Nimbus 上
main 方法只在提交 Topology 时运行在 Nimbus(Storm 的 master 节点)上。它的作用仅限于构建 Topology 的结构并提交——实际的 Spout 和 Bolt 代码不在 main 方法所在进程执行。
这意味着:
1 2 3 4 5 6 7 8 9 10 11
| public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); TopologyBuilder builder = new TopologyBuilder(); builder.setSpout("myspout", new MySpout()); builder.setBolt("mybolt", new MyBolt()).shuffleGrouping("myspout"); StormSubmitter.submitTopology("mytopo", config, builder.createTopology()); }
|
Spring 配置、数据库连接池、加密密钥等,都不能放在 main 中初始化。main 里只放 Storm 本身的配置项(Config 对象)和 Topology 结构构建。
2. Spout/Bolt 的生命周期
每个 Spout 和 Bolt 都有自己的生命周期方法:
| 组件 |
初始化 |
每条消息 |
退出 |
| Spout |
open() |
nextTuple() |
close() |
| Bolt |
prepare() |
execute() |
cleanup() |
关键点:
prepare()/open() 在 Worker 进程启动时执行一次——这里才是做初始化的正确位置。Spring 容器、数据库连接、配置加载等,都放在这里。
- 多个 Bolt 需要加载 Spring 时,建议使用相同配置:多个 Bolt 在同一个 Worker 中可能共享 JVM 进程,重复初始化 Spring 容器可能造成资源浪费甚至冲突。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class MyBolt extends BaseRichBolt { private SomeService service; @Override public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); this.service = ctx.getBean(SomeService.class); } @Override public void execute(Tuple input) { service.process(input); } }
|
3. 序列化——最常见的坑
由于 Spout 和 Bolt 可能在不同 Worker(甚至不同机器)上运行,所有通过 Storm 传输的数据都必须可序列化:
emit() 发出的 Tuple
- Spout/Bolt 的成员变量(如果不是在
prepare() 中初始化而是在构造器或静态块中)
- 配置中传递的对象
Storm 默认使用 Kryo 序列化,它比 Java 原生序列化更快,但有一些要求:
- 类必须有公开的无参构造函数。如果类没有无参构造(比如某些第三方库的类),会有
IllegalArgumentException
- 内部类和匿名类默认不支持,需要手动注册
如果不方便添加无参构造函数,可以降级:
1 2
| Config config = new Config(); config.setFallBackOnJavaSerialization(true);
|
一个完整的 WordCount 示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| public class SentenceSpout extends BaseRichSpout { private SpoutOutputCollector collector; private String[] sentences = { "hello world", "hello storm", "goodbye world" }; private int index = 0;
@Override public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) { this.collector = collector; }
@Override public void nextTuple() { collector.emit(new Values(sentences[index])); index = (index + 1) % sentences.length; Utils.sleep(1000); }
@Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("sentence")); } }
public class SplitBolt extends BaseRichBolt { private OutputCollector collector;
@Override public void prepare(Map conf, TopologyContext context, OutputCollector collector) { this.collector = collector; }
@Override public void execute(Tuple tuple) { String sentence = tuple.getStringByField("sentence"); for (String word : sentence.split(" ")) { collector.emit(new Values(word)); } }
@Override public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("word")); } }
public class CountBolt extends BaseRichBolt { private OutputCollector collector; private Map<String, Integer> counts = new HashMap<>();
@Override public void prepare(Map conf, TopologyContext context, OutputCollector collector) { this.collector = collector; }
@Override public void execute(Tuple tuple) { String word = tuple.getStringByField("word"); counts.merge(word, 1, Integer::sum); System.out.println(word + ": " + counts.get(word)); }
@Override public void declareOutputFields(OutputFieldsDeclarer declarer) {} }
|
2026 年的视角
Storm 曾经是实时计算的事实标准,但近年来 Apache Flink 已经成为流计算的主流选择。Flink 提供了更好的 exactly-once 语义、更丰富的窗口操作和更成熟的状态管理。如果你是新项目选型,我会建议优先考虑 Flink。
不过,如果你维护的是已有的 Storm/JStorm 遗留系统,本文的内容依然能帮你快速定位问题。理解了 Storm 的 Worker 模型和序列化机制,很多”诡异的线上 bug”其实都有迹可循。