Storm高级部分

 

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处理消息后该做什么?

由图可见:

  1. Spout需要实现ack方法和fail方法。

  2. Spout发出消息需要发送一个消息id(msgid)。

  3. 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》

4.1、Storm三大经典应用

 

猜你喜欢

转载自blog.csdn.net/qq_41571974/article/details/83243593
今日推荐