1、容错机制
正常运行的Storm集群,如果nimbus、supervisor、worker出现挂机,会怎么样?
1.1、worker进程死亡
如果所运行的任务的worker进程死亡,supervisor会尝试重启worker进程,如果尝试多次后依然无法启动,那么nimbus会重新分配一个supervisor来执行该任务。
1.2、supervisor所在的机器宕机
如果supervisor所在的机器宕机,首先会将分配给该机器的任务暂停,并且nimbus会重新分配机器来执行该节点上的任务。
1.3、nimbus或supervisor进程死亡
在Storm设计中,nimbus和supervisor都是独立的进程,和执行业务逻辑的worker完全分离,那么,nimbus和supervisor的进程死亡,对于Storm任务而言,没有任何的影响。
只是,我们需要在服务中对nimbus和supervisor的进程进行运维监控,如果发现进程死亡,需要及时的进行再重启,否则无法增加新的任务。
思考:worker进程和supervisor进程都是独立的,互不影响,这样设计的好处是什么?你接触到的产品还有什么是这样设计的?
2、消息不丢失机制
2.1、消息的“完全处理”
什么是消息的完全处理?看下面这个图:
该图展现了WordCount程序的执行过程,从Spout开始,经过了各种Bolt,最后将数据保存到Redis的过程。
整个拓扑的环节都执行通过的话,那么这个消息就称为:完全处理。
在Storm中,从Spout发出的一个Tupe,被完全处理的时间默认是30秒,如果处理超时,也会任务是 未完全处理。
当然了,这个超时时间,我们是可以自定义的,如下:
Config config = new Config(); config.setMessageTimeoutSecs(100); //设置消息的超时时间,这里的消息就是指Tupe
2.2、Spout是如何确保发出的Tupe是成功还是失败?
如下图:
从图上看出,在中间的环节中出了问题,那么整个拓扑的执行一定是有问题的。所以,Spout必须得知道自己发出的数据被所有的下游处理完成。Storm有设计这个机制吗? 其实是有的,我们一步步来分析。
首先,我们看下ISpout接口的方法:
public interface ISpout extends Serializable { // 初始化 void open(Map conf, TopologyContext context, SpoutOutputCollector collector); // 关闭 void close(); // 活跃的 void activate(); // 非活跃 void deactivate(); // 发出Tupe void nextTuple(); // 成功 (重点关注) void ack(Object msgId); // 失败 (重点关注) void fail(Object msgId); }
可以看到,在ISpout接口中已经有ack和fail方法,也就是说,Spoout是可以处理成功或失败的。
是不是说,我们现在已经实现了成功、失败处理? 其实并不是的!
2.3、Bolt处理消息后该做什么?
由图可见:
-
Spout需要实现ack方法和fail方法。
-
Spout发出消息需要发送一个消息id(msgid)。
-
Bolt执行完成后,如果成功需要通知Spout成功,如果失败需要通知Spout失败。
实现:
Spoout中实现ack和fail方法:
@Override public void ack(Object msgId) { System.out.println(msgId + " --> 处理成功!"); messages.remove(msgId); } @Override public void fail(Object msgId) { System.out.println(msgId + " --> 处理失败! 需要进行重试"); // 进行重试 this.collector.emit(new Values(messages.get(msgId)),msgId); System.out.println(msgId + " --> 重试完成!"); }
Spout中发送消息时需要发送消息id:
// 生成消息id,并且把数据存放到messages的map中 String msgId = UUID.randomUUID().toString(); messages.put(msgId, sentence); //向下游输出 this.collector.emit(new Values(sentence),msgId);
Bolt中的实现:SplitSentenceBolt
public void execute(Tuple input) { // 通过Tuple的getValueByField获取上游传递的数据,其中"sentence"是定义的字段名称 String sentence = input.getStringByField("sentence"); // 进行分割处理 String[] words = sentence.split(" "); // 向下游输出数据 for (String word : words) { this.collector.emit(input,new Values(word)); // 注意这里,需要将原始数据input传入,在这里称之为锚点,意思是将新的数据和原有数据进行关联绑定 } // 处理成功 this.collector.ack(input); }
失败:WordCountBolt
package cn.itcast.storm; 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; import java.util.Random; public class WordCountBolt extends BaseRichBolt { private Map<String, Integer> wordMaps = new HashMap<String, Integer>(); private OutputCollector collector; public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) { this.collector = collector; } public void execute(Tuple input) { String word = input.getStringByField("word"); Integer count = this.wordMaps.get(word); if (null == count) { count = 0; } count++; this.wordMaps.put(word, count); // 向下游输出数据,注意这里输出的多个字段数据 this.collector.emit(input,new Values(word, count)); //这里模拟有时成功有时失败 int num = new Random().nextInt(100); if(num % 2 == 0){ // 偶数,成功 this.collector.ack(input); }else{ // 奇数,失败 this.collector.fail(input); } } public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("word", "count")); } }
测试结果:
………………………… 32ca3d09-9a03-4bd5-bfdd-356848d6e275 --> 重试完成! 1fd11417-f01a-4c6f-99b0-17319a4dffcf --> 处理失败! 需要进行重试 1fd11417-f01a-4c6f-99b0-17319a4dffcf --> 重试完成! e0e83a20-a9ef-4ed5-ba91-35a5861cb150 --> 处理失败! 需要进行重试 e0e83a20-a9ef-4ed5-ba91-35a5861cb150 --> 重试完成! 4d1beb92-167a-4729-8949-1717253e396d --> 处理成功! cow : 260 jumped : 260 over : 115 moon : 260 apple : 288 ………………
需要注意的是,所有的bolt必须完成ack或者fail操作,否则Spout中的ack方法将不会执行,最终该tupe将会超时。
2.3.1、BaseBasicBolt的使用
由上面的测试可以看出,如果Bolt是继承了BaseRichBolt,那么成功与失败是需要我们手动调用成功或失败方法的。
这样在实际开发的过程中是非常不便的,Storm提供了BaseBasicBolt,帮我们自动完成了成功或失败的处理,如果是成功,什么都不需要处理,如果是失败,只需要抛出FailedException即可。
package cn.itcast.storm.acker; import org.apache.storm.task.OutputCollector; import org.apache.storm.task.TopologyContext; import org.apache.storm.topology.BasicOutputCollector; import org.apache.storm.topology.FailedException; import org.apache.storm.topology.OutputFieldsDeclarer; import org.apache.storm.topology.base.BaseBasicBolt; import org.apache.storm.tuple.Fields; import org.apache.storm.tuple.Tuple; import org.apache.storm.tuple.Values; import java.util.Map; /** * 实现Bolt,需要继承BaseBasicBolt */ public class SplitSentenceBolt extends BaseBasicBolt{ @Override public void execute(Tuple input, BasicOutputCollector collector) { try { // 通过Tuple的getValueByField获取上游传递的数据,其中"sentence"是定义的字段名称 String sentence = input.getStringByField("sentence"); // 进行分割处理 String[] words = sentence.split(" "); // 向下游输出数据 for (String word : words) { collector.emit(new Values(word)); } } catch (Exception e) { // 如果出现错误,只需要抛出异常即可 throw new FailedException(e); } } public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("word")); } }
推荐是BaseBasicBolt,这样开发更加的便利。
2.4、Storm是如何保证消息的可靠性的?
上面我们在代码层面实现了消息数据的不丢失,那么在Storm内部是如何保证消息不丢失的?下面我们看看Storm的原理。
首先,我们先理解个概念,叫:有向无环图:
这就是有向无环图。
在Strom中有一种特殊的任务,叫Acker,对每一个从Spout发出的Tupe都去跟踪它是否有 “有向无环图”,如果有,就发送消息给Spout成功消息,否则发送失败消息。
Spout中获取到成功消息,开始执行ack方法,失败,执行fail方法。
那么,Acker是如何跟踪 “有向无环图”的呢?
一个Topology的默认的Acker数为1,如果需要设置多个Acker,可以这个设置:
Config config = new Config(); config.setNumAckers(2);
2.5、如何关闭Acker机制?
如果想要关闭Acker机制,有二种方式:
第一种,设置acker数为0;
config.setNumAckers(0);
第二种,Spout向下游传递消息时,不携带msgid。
this.collector.emit(new Values(sentence));
3、Storm与其他框架整合
Storm官方支持了对其他技术框架的整合,如下:
我们重点学习storm-jdbc、storm-kafka-client、storm-redis三个整合。
3.1、Storm与JDBC整合
有些时候我们需要将Storm计算完的数据持久化到数据库,所以需要在Storm中整合JDBC进行持久化。
下面我们结合wordcount程序,将最终的单词和次数存储到数据库中。
官方文档:https://github.com/apache/storm/blob/master/docs/storm-jdbc.md
3.1.1、创建数据库
3.1.2、创建表 tb_wordcount
CREATE TABLE `tb_wordcount` ( `id` int(11) NOT NULL AUTO_INCREMENT, `word` varchar(50) NOT NULL, `count` int(11) NOT NULL, `created` datetime NOT NULL, `updated` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
3.1.3、引入storm-jdbc和mysql驱动依赖
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.34</version> </dependency> <dependency> <groupId>org.apache.storm</groupId> <artifactId>storm-jdbc</artifactId> <version>1.1.1</version> </dependency>
3.1.4、编写JdbcBoltBuilder
package cn.itcast.storm.jdbc; import com.google.common.collect.Lists; import org.apache.storm.jdbc.bolt.JdbcInsertBolt; import org.apache.storm.jdbc.common.Column; import org.apache.storm.jdbc.common.ConnectionProvider; import org.apache.storm.jdbc.common.HikariCPConnectionProvider; import org.apache.storm.jdbc.mapper.JdbcMapper; import org.apache.storm.jdbc.mapper.SimpleJdbcMapper; import org.apache.storm.topology.IRichBolt; import java.sql.Types; import java.util.HashMap; import java.util.List; import java.util.Map; public class JdbcBoltBuilder { private JdbcBoltBuilder() { } public static IRichBolt build() { // 定义数据库连接信息 Map<String, Object> hikariConfigMap = new HashMap<String, Object>(); hikariConfigMap.put("dataSourceClassName", "com.mysql.jdbc.jdbc2.optional.MysqlDataSource"); hikariConfigMap.put("dataSource.url", "jdbc:mysql://localhost:3306/storm"); hikariConfigMap.put("dataSource.user", "root"); hikariConfigMap.put("dataSource.password", "root"); ConnectionProvider connectionProvider = new HikariCPConnectionProvider(hikariConfigMap); // 定义表名,以及定义字段的映射,这里指定的是tupe中的字段名称,用于获取数据 String tableName = "tb_wordcount"; List<Column> columnSchema = Lists.newArrayList( new Column("word", Types.VARCHAR), new Column("count", Types.INTEGER)); // 定义jdbc的映射器 JdbcMapper simpleJdbcMapper = new SimpleJdbcMapper(columnSchema); // 定义插入数据的Bolt,并且指定了插入的sql语句 JdbcInsertBolt wordCountBolt = new JdbcInsertBolt(connectionProvider, simpleJdbcMapper) .withInsertQuery("INSERT INTO `tb_wordcount` VALUES (NULL, ?, ?, NOW())") .withQueryTimeoutSecs(30); return wordCountBolt; } }
3.1.5、整合到Topology中使用
package cn.itcast.storm; import cn.itcast.storm.jdbc.JdbcBoltBuilder; import org.apache.storm.Config; import org.apache.storm.LocalCluster; import org.apache.storm.StormSubmitter; import org.apache.storm.generated.AlreadyAliveException; import org.apache.storm.generated.AuthorizationException; import org.apache.storm.generated.InvalidTopologyException; import org.apache.storm.generated.StormTopology; import org.apache.storm.topology.TopologyBuilder; import org.apache.storm.tuple.Fields; public class WordCountTopology { public static void main(String[] args) { //第一步,定义TopologyBuilder对象,用于构建拓扑 TopologyBuilder topologyBuilder = new TopologyBuilder(); //第二步,设置spout和bolt topologyBuilder.setSpout("RandomSentenceSpout", new RandomSentenceSpout(), 2).setNumTasks(2); topologyBuilder.setBolt("SplitSentenceBolt", new SplitSentenceBolt(), 4).localOrShuffleGrouping("RandomSentenceSpout").setNumTasks(4); topologyBuilder.setBolt("WordCountBolt", new WordCountBolt(), 2).partialKeyGrouping("SplitSentenceBolt", new Fields("word")); topologyBuilder.setBolt("JdbcBolt", JdbcBoltBuilder.build()).shuffleGrouping("WordCountBolt"); //第三步,构建Topology对象 StormTopology topology = topologyBuilder.createTopology(); Config config = new Config(); if (args == null || args.length == 0) { // 本地模式 //第四步,提交拓扑到集群,这里先提交到本地的模拟环境中进行测试 LocalCluster localCluster = new LocalCluster(); localCluster.submitTopology("WordCountTopology", config, topology); } else { // 集群模式 config.setNumWorkers(2); // 设置工作进程数 config.setMessageTimeoutSecs(100); config.setNumAckers(2); try { //提交到集群,并且将参数作为拓扑的名称 StormSubmitter.submitTopology(args[0], config, topology); } catch (AlreadyAliveException e) { e.printStackTrace(); } catch (InvalidTopologyException e) { e.printStackTrace(); } catch (AuthorizationException e) { e.printStackTrace(); } } } }
3.1.6、测试
查询单词总数:
SELECT word,COUNT(`count`) FROM tb_wordcount GROUP BY word
3.2、Storm与Redis整合
Storm和Redis整合是非常常用的场景,Storm也支持了Redis的支持。
官方文档:https://github.com/apache/storm/blob/master/docs/storm-redis.md
3.2.1、准备Redis环境
3.2.2、导入依赖
<dependency> <groupId>org.apache.storm</groupId> <artifactId>storm-redis</artifactId> <version>1.1.1</version> </dependency>
3.2.3、创建WordCountStoreMapper
package cn.itcast.storm.redis; import org.apache.storm.redis.common.mapper.RedisDataTypeDescription; import org.apache.storm.redis.common.mapper.RedisStoreMapper; import org.apache.storm.tuple.ITuple; public class WordCountStoreMapper implements RedisStoreMapper { private RedisDataTypeDescription redisDataTypeDescription; public WordCountStoreMapper() { // 定义Redis中的数据类型 this.redisDataTypeDescription = new RedisDataTypeDescription(RedisDataTypeDescription.RedisDataType.STRING); } @Override public RedisDataTypeDescription getDataTypeDescription() { return this.redisDataTypeDescription; } @Override public String getKeyFromTuple(ITuple iTuple) { // 生成redis中的key String word = iTuple.getStringByField("word"); return "wordCount:" + word; } @Override public String getValueFromTuple(ITuple iTuple) { // 存储到redis中的值 Integer count = iTuple.getIntegerByField("count"); return String.valueOf(count); } }
3.2.4、整合到Topology中
package cn.itcast.storm.redis; import org.apache.storm.Config; import org.apache.storm.LocalCluster; import org.apache.storm.StormSubmitter; import org.apache.storm.generated.AlreadyAliveException; import org.apache.storm.generated.AuthorizationException; import org.apache.storm.generated.InvalidTopologyException; import org.apache.storm.generated.StormTopology; import org.apache.storm.redis.bolt.RedisStoreBolt; import org.apache.storm.redis.common.config.JedisPoolConfig; import org.apache.storm.topology.TopologyBuilder; import org.apache.storm.tuple.Fields; public class WordCountTopology { public static void main(String[] args) { //第一步,定义TopologyBuilder对象,用于构建拓扑 TopologyBuilder topologyBuilder = new TopologyBuilder(); //第二步,设置spout和bolt topologyBuilder.setSpout("RandomSentenceSpout", new RandomSentenceSpout()); topologyBuilder.setBolt("SplitSentenceBolt", new SplitSentenceBolt()).localOrShuffleGrouping("RandomSentenceSpout"); topologyBuilder.setBolt("WordCountBolt", new WordCountBolt()).partialKeyGrouping("SplitSentenceBolt", new Fields("word")); JedisPoolConfig poolConfig = new JedisPoolConfig.Builder() .setHost("node01").setPort(6379).build(); topologyBuilder.setBolt("RedistBolt", new RedisStoreBolt(poolConfig, new WordCountStoreMapper())) .localOrShuffleGrouping("WordCountBolt"); //第三步,构建Topology对象 StormTopology topology = topologyBuilder.createTopology(); Config config = new Config(); if (args == null || args.length == 0) { // 本地模式 //第四步,提交拓扑到集群,这里先提交到本地的模拟环境中进行测试 LocalCluster localCluster = new LocalCluster(); localCluster.submitTopology("WordCountTopology", config, topology); } else { // 集群模式 config.setNumWorkers(2); // 设置工作进程数 config.setMessageTimeoutSecs(100); config.setNumAckers(2); try { //提交到集群,并且将参数作为拓扑的名称 StormSubmitter.submitTopology(args[0], config, topology); } catch (AlreadyAliveException e) { e.printStackTrace(); } catch (InvalidTopologyException e) { e.printStackTrace(); } catch (AuthorizationException e) { e.printStackTrace(); } } } }
3.2.5、测试
3.3、Storm与Kafka整合
Storm与Kafka的整合也是非常常见的,常常用于数据读取,所以我们更多的是要关注Spout。
官方文档:https://github.com/apache/storm/blob/master/docs/storm-kafka-client.md
3.3.1、准备kafka环境
3.3.2、创建kafka-storm-topic
kafka-topics.sh --create --zookeeper node01:2181 --replication-factor 2 --partitions 3 --topic kafka-storm-topic
或者通过界面创建
3.3.3、导入整合依赖
<dependency> <groupId>org.apache.storm</groupId> <artifactId>storm-kafka-client</artifactId> <version>1.1.1</version> </dependency>
3.3.4、整合KafkaSpout到Topology
package cn.itcast.storm.kafka; import cn.itcast.storm.redis.WordCountBolt; import cn.itcast.storm.redis.WordCountStoreMapper; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.storm.Config; import org.apache.storm.LocalCluster; import org.apache.storm.StormSubmitter; import org.apache.storm.generated.AlreadyAliveException; import org.apache.storm.generated.AuthorizationException; import org.apache.storm.generated.InvalidTopologyException; import org.apache.storm.generated.StormTopology; import org.apache.storm.kafka.spout.KafkaSpout; import org.apache.storm.kafka.spout.KafkaSpoutConfig; import org.apache.storm.redis.bolt.RedisStoreBolt; import org.apache.storm.redis.common.config.JedisPoolConfig; import org.apache.storm.topology.TopologyBuilder; import org.apache.storm.tuple.Fields; public class WordCountTopology { public static void main(String[] args) { //第一步,定义TopologyBuilder对象,用于构建拓扑 TopologyBuilder topologyBuilder = new TopologyBuilder(); //第二步,设置spout和bolt KafkaSpoutConfig.Builder<String, String> kafkaSpoutBuilder = KafkaSpoutConfig.builder("node01:9092", "kafka-storm-topic"); kafkaSpoutBuilder.setGroupId("kafka-storm-topic-consumer-groupid"); //设置消费者组id // 这里设置Spout的并行度为3,原因是创建topic时,指定的partition为3 topologyBuilder.setSpout("kafka_spout", new KafkaSpout<>(kafkaSpoutBuilder.build()),3); topologyBuilder.setBolt("SplitSentenceBolt", new SplitSentenceBolt()).localOrShuffleGrouping("kafka_spout"); topologyBuilder.setBolt("WordCountBolt", new WordCountBolt()).partialKeyGrouping("SplitSentenceBolt", new Fields("word")); JedisPoolConfig poolConfig = new JedisPoolConfig.Builder() .setHost("node01").setPort(6379).build(); topologyBuilder.setBolt("RedistBolt", new RedisStoreBolt(poolConfig, new WordCountStoreMapper())) .localOrShuffleGrouping("WordCountBolt"); //第三步,构建Topology对象 StormTopology topology = topologyBuilder.createTopology(); Config config = new Config(); if (args == null || args.length == 0) { // 本地模式 //第四步,提交拓扑到集群,这里先提交到本地的模拟环境中进行测试 LocalCluster localCluster = new LocalCluster(); localCluster.submitTopology("WordCountTopology", config, topology); } else { // 集群模式 config.setNumWorkers(2); // 设置工作进程数 config.setMessageTimeoutSecs(100); config.setNumAckers(2); try { //提交到集群,并且将参数作为拓扑的名称 StormSubmitter.submitTopology(args[0], config, topology); } catch (AlreadyAliveException e) { e.printStackTrace(); } catch (InvalidTopologyException e) { e.printStackTrace(); } catch (AuthorizationException e) { e.printStackTrace(); } } } }
3.3.5、下游的Bolt获取值
通过KafkaSpout向下游发送的Tupe是这样的:
所以,我们需要通过value获取值:
String sentence = input.getStringByField("value"); System.out.println("接收到消息为 --> " + sentence);
3.3.6、测试
4、Storm在企业中的应用
内容节选自《从零开始学Storm 第2版.pdf》