两天公司要学习kafka,结合之前的storm,做了一个简单的集成,之前也参考了网上的例子一些例子,发现或多或少都有一些问题。所以自己做了一个。

这个是网上其他人遇到的问题,给摘录一下,防止以后自己和大家出现:

这几天工作需要使用storm+kafka,基本场景是应用出现错误,发送日志到kafka的某个topic,storm订阅该topic,然后进行后续处理。场景非常简单,但是在学习过程中,遇到一个奇怪的异常情况:使用KafkaSpout读取topic数据时,没有向ZK写offset数据,致使每次都从头开始读取。纠结了两天,终于碰巧找到原因:应该使用BaseBasicBolt作为bolt的父类,而不是BaseRichBolt

通过本文记录一下这种情况,后文中根据上述场景提供几个简单的例子。基础理论查看storm笔记:storm基本概念,或查看Storm 简介

基本订阅

基本场景:订阅kafka的某个topic,然后在读取的消息前加上自定义的字符串,然后写回到kafka另外一个topic。

从Kafka读取数据的Spout使用storm.kafka.KafkaSpout,向Kafka写数据的Bolt使用storm.kafka.bolt.KafkaBolt。中间进行进行数据处理的Bolt定义为TopicMsgBolt。闲言少叙,奉上代码:


public class TopicMsgTopology {
public static void main(String[] args) throws Exception {
// 配置Zookeeper地址
BrokerHosts brokerHosts = new ZkHosts("zk1:2181,zk2:2281,zk3:2381");
// 配置Kafka订阅的Topic,以及zookeeper中数据节点目录和名字
SpoutConfig spoutConfig = new SpoutConfig(brokerHosts, "msgTopic1", "/topology/root", "topicMsgTopology");
// 配置KafkaBolt中的kafka.broker.properties
Config conf = new Config();
Properties props = new Properties();
// 配置Kafka broker地址
props.put("metadata.broker.list", "dev2_55.wfj-search:9092");
// serializer.class为消息的序列化类
props.put("serializer.class", "kafka.serializer.StringEncoder");
conf.put("kafka.broker.properties", props);
// 配置KafkaBolt生成的topic
conf.put("topic", "msgTopic2");
spoutConfig.scheme = new SchemeAsMultiScheme(new MessageScheme());
TopologyBuilder builder = new TopologyBuilder();
builder.setSpout("msgKafkaSpout", new KafkaSpout(spoutConfig));
builder.setBolt("msgSentenceBolt", new TopicMsgBolt()).shuffleGrouping("msgKafkaSpout");
builder.setBolt("msgKafkaBolt", new KafkaBolt<String, Integer>()).shuffleGrouping("msgSentenceBolt");
if (args.length == 0) {
String topologyName = "kafkaTopicTopology";
LocalCluster cluster = new LocalCluster();
cluster.submitTopology(topologyName, conf, builder.createTopology());
Utils.sleep(100000);
cluster.killTopology(topologyName);
cluster.shutdown();
} else {
conf.setNumWorkers(1);
StormSubmitter.submitTopology(args[0], conf, builder.createTopology());
}
}
}

storm.kafka.ZkHosts构造方法的参数是zookeeper标准配置地址的形式(ZooKeeper环境搭建可以查看ZooKeeper安装部署),zk1、zk2、zk3在本地配置了host,因为服务器使用的伪分布式模式,因此几个端口号不是默认的2181。

storm.kafka.SpoutConfig构造方法第一个参数为上述的storm.kafka.ZkHosts对象,第二个为待订阅的topic名称,第三个参数zkRoot为写读取topic时的偏移量offset数据的节点(zk node),第四个参数为该节点上的次级节点名(有个地方说这个是spout的id)。

backtype.storm.Config对象是配置storm的topology(拓扑)所需要的基础配置。

backtype.storm.spout.SchemeAsMultiScheme的构造方法输入的参数是订阅kafka数据的处理参数,这里的MessageScheme是自定义的,代码如下:

public class MessageScheme implements Scheme {
private static final Logger logger = LoggerFactory.getLogger(MessageScheme.class); @Override
public List<Object> deserialize(byte[] ser) {
try {
String msg = new String(ser, "UTF-8");
logger.info("get one message is {}", msg);
return new Values(msg);
} catch (UnsupportedEncodingException ignored) {
return null;
}
} @Override
public Fields getOutputFields() {
return new Fields("msg");
}
}

MessageScheme类中getOutputFields方法是KafkaSpout向后发送tuple(storm传输数据的最小结构)的名字,需要与接收数据的Bolt中统一(在这个例子中可以不统一,因为后面直接取第0条数据,但是在wordCount的那个例子中就需要统一了)。

TopicMsgBolt类是从storm.kafka.KafkaSpout接收数据的Bolt,对接收到的数据进行处理,然后向后传输给storm.kafka.bolt.KafkaBolt。代码如下:

public class TopicMsgBolt extends BaseBasicBolt {
private static final Logger logger = LoggerFactory.getLogger(TopicMsgBolt.class); @Override
public void execute(Tuple input, BasicOutputCollector collector) {
String word = (String) input.getValue(0);
String out = "Message got is '" + word + "'!";
logger.info("out={}", out);
collector.emit(new Values(out));
} @Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("message"));
}
}

此处需要特别注意的是,要使用backtype.storm.topology.base.BaseBasicBolt对象作为父类,否则不会在zk记录偏移量offset数据。

需要编写的代码已完成,接下来就是在搭建好的storm、kafka中进行测试:

# 创建topic
./bin/kafka-topics.sh --create --zookeeper zk1:2181,zk2:2281,zk3:2381 --replication-factor 1 --partitions 1 --topic msgTopic1
./bin/kafka-topics.sh --create --zookeeper zk1:2181,zk2:2281,zk3:2381 --replication-factor 1 --partitions 1 --topic msgTopic2

接下来需要分别对msgTopic1、msgTopic2启动producer(生产者)与consumer(消费者):

# 对msgTopic1启动producer,用于发送数据
./bin/kafka-console-producer.sh --broker-list dev2_55.wfj-search:9092 --topic msgTopic1
# 对msgTopic2启动consumer,用于查看发送数据的处理结果
./bin/kafka-console-consumer.sh --zookeeper zk1:2181,zk2:2281,zk3:2381 --topic msgTopic2 --from-beginning

然后将打好的jar包上传到storm的nimbus(可以使用远程上传或先上传jar包到nimbus节点所在服务器,然后本地执行):

# ./bin/storm jar topology TopicMsgTopology.jar cn.howardliu.demo.storm.kafka.topicMsg.TopicMsgTopology TopicMsgTopology

待对应的worker启动好之后,就可以在msgTopic1的producer对应终端输入数据,然后在msgTopic2的consumer对应终端查看输出结果了。

有几点需要注意的:

  1. 必须先创建msgTopic1、msgTopic2两个topic;
  2. 定义的bolt必须使用BaseBasicBolt作为父类,不能够使用BaseRichBolt,否则无法记录偏移量;
  3. zookeeper最好使用至少三个节点的分布式模式或伪分布式模式,否则会出现一些异常情况;
  4. 在整个storm下,spout、bolt的id必须唯一,否则会出现异常。
  5. TopicMsgBolt类作为storm.kafka.bolt.KafkaBolt前的最后一个Bolt,需要将输出数据名称定义为message,否则KafkaBolt无法接收数据。

wordCount

简单的输入输出做完了,来点复杂点儿的场景:从某个topic定于消息,然后根据空格分词,统计单词数量,然后将当前输入的单词数量推送到另一个topic。

首先规划需要用到的类:

  1. 从KafkaSpout接收数据并进行处理的backtype.storm.spout.Scheme子类;
  2. 数据切分bolt:SplitSentenceBolt
  3. 计数bolt:WordCountBolt
  4. 报表bolt:ReportBolt
  5. topology定义:WordCountTopology
  6. 最后再加一个原样显示订阅数据的bolt:SentenceBolt

backtype.storm.spout.Scheme子类可以使用上面已经定义过的MessageScheme,此处不再赘述。

SplitSentenceBolt是对输入数据进行分割,简单的使用String类的split方法,然后将每个单词命名为“word”,向后传输,代码如下:

public class SplitSentenceBolt extends BaseBasicBolt {
@Override
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
outputFieldsDeclarer.declare(new Fields("word"));
} @Override
public void execute(Tuple input, BasicOutputCollector collector) {
String sentence = input.getStringByField("msg");
String[] words = sentence.split(" ");
Arrays.asList(words).forEach(word -> collector.emit(new Values(word)));
}
}

SentenceBolt是从KafkaSpout接收数据,然后直接输出。在拓扑图上就是从输入分叉,一个进入SplitSentenceBolt,一个进入SentenceBolt。这种结构可以应用在Lambda架构中,代码如下:

public class SentenceBolt extends BaseBasicBolt {
private static final Logger logger = LoggerFactory.getLogger(SentenceBolt.class); @Override
public void execute(Tuple tuple, BasicOutputCollector basicOutputCollector) {
String msg = tuple.getStringByField("msg");
logger.info("get one message is {}", msg);
basicOutputCollector.emit(new Values(msg));
} @Override
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
outputFieldsDeclarer.declare(new Fields("sentence"));
}
}

WordCountBolt是对接收到的单词进行汇总统一,然后将单词“word”及其对应数量“count”向后传输,代码如下:

public class WordCountBolt extends BaseBasicBolt {
private Map<String, Long> counts = null; @Override
public void prepare(Map stormConf, TopologyContext context) {
this.counts = new ConcurrentHashMap<>();
super.prepare(stormConf, context);
} @Override
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
outputFieldsDeclarer.declare(new Fields("word", "count"));
} @Override
public void execute(Tuple input, BasicOutputCollector collector) {
String word = input.getStringByField("word");
Long count = this.counts.get(word);
if (count == null) {
count = 0L;
}
count++;
this.counts.put(word, count);
collector.emit(new Values(word, count));
}
}

ReportBolt是对接收到的单词及数量进行整理,拼成json格式,然后继续向后传输,代码如下:

public class ReportBolt extends BaseBasicBolt {
@Override
public void execute(Tuple input, BasicOutputCollector collector) {
String word = input.getStringByField("word");
Long count = input.getLongByField("count");
String reportMessage = "{'word': '" + word + "', 'count': '" + count + "'}";
collector.emit(new Values(reportMessage));
} @Override
public void declareOutputFields(OutputFieldsDeclarer outputFieldsDeclarer) {
outputFieldsDeclarer.declare(new Fields("message"));
}
}

最后是定义topology(拓扑)WordCountTopology,代码如下:

public class WordCountTopology {
private static final String KAFKA_SPOUT_ID = "kafkaSpout";
private static final String SENTENCE_BOLT_ID = "sentenceBolt";
private static final String SPLIT_BOLT_ID = "sentenceSplitBolt";
private static final String WORD_COUNT_BOLT_ID = "sentenceWordCountBolt";
private static final String REPORT_BOLT_ID = "reportBolt";
private static final String KAFKA_BOLT_ID = "kafkabolt";
private static final String CONSUME_TOPIC = "sentenceTopic";
private static final String PRODUCT_TOPIC = "wordCountTopic";
private static final String ZK_ROOT = "/topology/root";
private static final String ZK_ID = "wordCount";
private static final String DEFAULT_TOPOLOGY_NAME = "sentenceWordCountKafka"; public static void main(String[] args) throws Exception {
// 配置Zookeeper地址
BrokerHosts brokerHosts = new ZkHosts("zk1:2181,zk2:2281,zk3:2381");
// 配置Kafka订阅的Topic,以及zookeeper中数据节点目录和名字
SpoutConfig spoutConfig = new SpoutConfig(brokerHosts, CONSUME_TOPIC, ZK_ROOT, ZK_ID);
spoutConfig.scheme = new SchemeAsMultiScheme(new MessageScheme()); TopologyBuilder builder = new TopologyBuilder();
builder.setSpout(KAFKA_SPOUT_ID, new KafkaSpout(spoutConfig));
builder.setBolt(SENTENCE_BOLT_ID, new SentenceBolt()).shuffleGrouping(KAFKA_SPOUT_ID);
builder.setBolt(SPLIT_BOLT_ID, new SplitSentenceBolt()).shuffleGrouping(KAFKA_SPOUT_ID);
builder.setBolt(WORD_COUNT_BOLT_ID, new WordCountBolt()).fieldsGrouping(SPLIT_BOLT_ID, new Fields("word"));
builder.setBolt(REPORT_BOLT_ID, new ReportBolt()).shuffleGrouping(WORD_COUNT_BOLT_ID);
builder.setBolt(KAFKA_BOLT_ID, new KafkaBolt<String, Long>()).shuffleGrouping(REPORT_BOLT_ID); Config config = new Config();
Map<String, String> map = new HashMap<>();
map.put("metadata.broker.list", "dev2_55.wfj-search:9092");// 配置Kafka broker地址
map.put("serializer.class", "kafka.serializer.StringEncoder");// serializer.class为消息的序列化类
config.put("kafka.broker.properties", map);// 配置KafkaBolt中的kafka.broker.properties
config.put("topic", PRODUCT_TOPIC);// 配置KafkaBolt生成的topic if (args.length == 0) {
LocalCluster cluster = new LocalCluster();
cluster.submitTopology(DEFAULT_TOPOLOGY_NAME, config, builder.createTopology());
Utils.sleep(100000);
cluster.killTopology(DEFAULT_TOPOLOGY_NAME);
cluster.shutdown();
} else {
config.setNumWorkers(1);
StormSubmitter.submitTopology(args[0], config, builder.createTopology());
}
}
}

除了上面提过应该注意的地方,此处还需要注意,storm.kafka.SpoutConfig定义的zkRoot与id应该与第一个例子中不同(至少保证id不同,否则两个topology将使用一个节点记录偏移量)。

基本场景是应用出现错误,发送日志到kafka的某个topic,storm订阅该topic,然后进行后续处理。场景非常简单,但是在学习过程中,遇到一个奇怪的异常情况:使用KafkaSpout读取topic数据时,没有向ZK写offset数据,致使每次都从头开始读取。纠结了两天,终于碰巧找到原因:应该使用BaseRichBolt

基本订阅 :

基本场景:订阅kafka的某个topic,然后在读取的消息前加上自定义的字符串,然后写回到kafka另外一个topic。  从Kafka读取数据的Spout使用storm.kafka.KafkaSpout,向Kafka写数据的Bolt使用storm.kafka.bolt.KafkaBolt。中间进行进行数据处理的Bolt定义为TopicMsgBolt。

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
import backtype.storm.Config;
import backtype.storm.LocalCluster;
import backtype.storm.StormSubmitter;
import backtype.storm.spout.SchemeAsMultiScheme;
import backtype.storm.topology.IBasicBolt;
import backtype.storm.topology.TopologyBuilder;
import backtype.storm.utils.Utils;
import storm.kafka.BrokerHosts;
import storm.kafka.KafkaSpout;
import storm.kafka.SpoutConfig;
import storm.kafka.ZkHosts;
import storm.kafka.bolt.KafkaBolt;
 
import java.util.Properties;
 
public class TopicMsgTopology {
    public static void main(String[] args) throws Exception {
        // 配置Zookeeper地址
        BrokerHosts brokerHosts = new ZkHosts("localhost:2181");
        // 配置Kafka订阅的Topic,以及zookeeper中数据节点目录和名字
        SpoutConfig spoutConfig = new SpoutConfig(brokerHosts, "msgTopic1""/topology/root1""topicMsgTopology");
        // 配置KafkaBolt中的kafka.broker.properties
        Config conf = new Config();
        Properties props = new Properties();
        // 配置Kafka broker地址
        props.put("metadata.broker.list""localhost:9092");
        // serializer.class为消息的序列化类
        props.put("serializer.class""kafka.serializer.StringEncoder");
        conf.put("kafka.broker.properties", props);
        // 配置KafkaBolt生成的topic
        conf.put("topic""msgTopic2");
        spoutConfig.scheme = new SchemeAsMultiScheme(new MessageScheme());
        TopologyBuilder builder = new TopologyBuilder();
        builder.setSpout("msgKafkaSpout"new KafkaSpout(spoutConfig));
        builder.setBolt("msgSentenceBolt", (IBasicBolt) new TopicMsgBolt()).shuffleGrouping("msgKafkaSpout");
        builder.setBolt("msgKafkaBolt"new KafkaBolt<String, Integer>()).shuffleGrouping("msgSentenceBolt");
        if (args.length == 0) {
            String topologyName = "kafkaTopicTopology";
            LocalCluster cluster = new LocalCluster();
            cluster.submitTopology(topologyName, conf, builder.createTopology());
            Utils.sleep(100000);
            cluster.killTopology(topologyName);
            cluster.shutdown();
        else {
            conf.setNumWorkers(1);
            StormSubmitter.submitTopology(args[0], conf, builder.createTopology());
        }
    }
}

没有向ZK写offset数据的更多相关文章

  1. Netty中如何写大型数据

    因为网络饱和的可能性,如何在异步框架中高效地写大块的数据是一个特殊的问题.由于写操作是非阻塞的,所以即使没有写出所有的数据,写操作也会在完成时返回并通知ChannelFuture.当这种情况发生时,如 ...

  2. 通过hive向写elasticsearch的写如数据

    通过hive向写elasticsearch的写如数据 hive 和 elasticsearch 的整合可以参考官方的文档: ES-hadoop的hive整合 : https://www.elastic ...

  3. CAD在网页中绘图,并为新绘的对象写扩展数据和读取扩展数据

    在网页中绘图,并为新绘的对象写扩展数据和读取扩展数据.下面帮助的完整例子,在控件安装目录的 Sample\Ie\iedemo.htm 中. 主要用到函数说明: _DMxDrawX::InsertBlo ...

  4. CAD在网页绘一个直线,得到直线id,再调该得到直线对象,然写扩展数据

    IMxDrawDatabase::ObjectIdToObject 实体id返回实体对象. 参数 说明 [in] LONGLONG lId 实体id JS代码,中绘一个直线,得到直线id,再调该得到直 ...

  5. 什么?我往Redis写的数据怎么没了?

    大概是因为int没有因为change方法而改变原值,所以就说它传过去的是自身的值,因而叫值传递:User对象经过change方法后,对象的数据变了,就认为是因为实参和形参指向的是同一片内存空间,内存空 ...

  6. Kafka+SparkStreaming+Zookeeper(ZK存储Offset,解决checkpoint问题)

    创建一个topic ./kafka-topics.sh --create --zookeeper 192.168.1.244:2181,192.168.1.245:2181,192.168.1.246 ...

  7. 自己写的数据交换工具——从Oracle到Elasticsearch

    先说说需求的背景,由于业务数据都在Oracle数据库中,想要对它进行数据的分析会非常非常慢,用传统的数据仓库-->数据集市这种方式,集市层表会非常大,查询的时候如果再做一些group的操作,一个 ...

  8. ELK系列~log4-nxlog-Fluentd-elasticsearch写json数据需要注意的几点

    经验与实践 前两篇文章里我们介绍了nxlog的日志收集和转发<ELK系列~Nxlog日志收集加转发(解决log4日志换行导致json转换失败问题)>,今天我们主要总结一下,在与log4和f ...

  9. 在做MVC和WebApi写返回数据时,可以这样定义

    public class Messages { /// <summary> /// 返回包含是否成功以及消息字符结果 /// </summary> /// <param ...

随机推荐

  1. java实现邮箱发送邮件功能

    邮箱验证是一个很常见的功能了,基本上每个网站都会用的到,java也有专门的jar来处理邮件发送等服务,这里只是简单的实现一下发送邮件的功能,具体jar包就不再提供了,我会把所有需要引用的包都贴出来,方 ...

  2. Python中关于Lambda函数的使用总结

    lambda表达式是一种匿名函数,对应python中的自定义函数def,是定义某个函数时比较高级的一种写法.作为python初学者,本文整理了lambda的一些基本用法和特点. lambda和def的 ...

  3. PAT甲级题分类汇编——序言

    今天开个坑,分类整理PAT甲级题目(https://pintia.cn/problem-sets/994805342720868352/problems/type/7)中1051~1100部分.语言是 ...

  4. SpringBoot学习(五)—— springboot快速整合Druid

    Druid连接池 简介 由阿里巴巴开源的druid连接池是目前综合实力最突出的数据库连接池,而且还提供了监控日志功能,能够分析SQL执行情况. 引入druid连接池 pom.xml中加入 <de ...

  5. Spring高级进阶:BeanFactoryPostProcessor

    BeanFactoryPostProcessor是实现spring容器功能扩展的重要接口,例如修改bean属性值,实现bean动态代理等.很多框架都是通过此接口实现对spring容器的扩展,例如myb ...

  6. python练习:面向对象1

    面向对象习题: 一:定义一个学生类.有下面的类属性: 1 姓名 2 年龄 3 成绩(语文,数学,英语)[每课成绩的类型为整数] 类方法: 1 获取学生的姓名:get_name() 返回类型:str 2 ...

  7. MySQL 乱码问题解决

    修改 配置文件 只需留下 my.ini文件,然后修改其编码配置. 配置如下 # Example MySQL config file for large systems. # # This is for ...

  8. TCP粘包/拆包(Netty权威指南)

    无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个“流”协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片 ...

  9. Axios使用拦截器全局处理请求重试

    Axios拦截器 Axios提供了拦截器的接口,让我们能够全局处理请求和响应.Axios拦截器会在Promise的then和catch调用前拦截到. 请求拦截示例 axios.interceptors ...

  10. Nginx跨域访问场景配置和防盗链

    跨域访问控制 跨域访问 为什么浏览器禁止跨域访问 不安全,容易出现CSRF攻击! 如果黑客控制的网站B在响应头里添加了让客户端去访问网站A的恶意信息,就会出现CSRF攻击 Nginx如何配置跨域访问 ...