消息队列之Kafka入门到进阶

img

1 Kafka概述

1.1 消息队列

image-20201111212446563

(1)点对点模式(一对一,消费者主动拉取数据,消息收到后消息清除)

点对点模型通常是一个基于拉取或者轮询的消息传送模型,这种模型从队列中请求信息,而不是将消息推送到客户端。这个模型的特点是发送到队列的消息被一个且只有一个接收者接收处理,即使有多个消息监听者也是如此。

(2)发布/订阅模式(一对多,数据生产后,推送给所有订阅者)

发布订阅模型则是一个基于推送的消息传送模型。发布订阅模型可以有多种不同的订阅者,临时订阅者只在主动监听主题时才接收消息,而持久订阅者则监听主题的所有消息,即使当前订阅者不可用,处于离线状态。

 

1.2 为什么需要消息队列

1)解耦:

  允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。

2)冗余:

消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。

3)扩展性:

因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。

4)灵活性 & 峰值处理能力:

在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。

5)可恢复性:

系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。

6)顺序保证:

在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。(Kafka保证一个Partition内的消息的有序性)

7)缓冲:

有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。

8)异步通信:

很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。

 

1.3 什么是Kafka

在流式计算中,Kafka一般用来缓存数据,Storm通过消费Kafka的数据进行计算。

1)Apache Kafka是一个开源消息系统,由Scala写成。是由Apache软件基金会开发的一个开源消息系统项目。

2)Kafka最初是由LinkedIn公司开发,并于2011年初开源。2012年10月从Apache Incubator毕业。该项目的目标是为处理实时数据提供一个统一、高通量、低等待的平台。

3)Kafka是一个分布式消息队列。**Kafka对消息保存时根据Topic进行归类,发送消息者称为Producer,消息接受者称为Consumer,此外kafka集群有多个kafka实例组成,每个实例(server)称为broker。

4)无论是kafka集群,还是consumer都依赖于zookeeper集群保存一些meta信息,来保证系统可用性。

 

1.4 Kafka架构

Kafka整体架构图:

image-20201111212641669

Kafka详细架构图:

image-20201111212749115

1)Producer :消息生产者,就是向kafka broker发消息的客户端;

2)Consumer :消息消费者,向kafka broker取消息的客户端;

3)Topic :可以理解为一个队列;

4) Consumer Group (CG):这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制(不是真的复制,是概念上的)到所有的CG,但每个partion只会把消息发给该CG中的一个consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic;

5)Broker :一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic;

6)Partition:为了实现扩展性,一个非常大的topic可以分布到多个broker(即服务器)上,一个topic可以分为多个partition,每个partition是一个有序的队列。partition中的每条消息都会被分配一个有序的id(offset)。kafka只保证按一个partition中的顺序将消息发给consumer,不保证一个topic的整体(多个partition间)的顺序;

7)Offset:kafka的存储文件都是按照offset.kafka来命名,用offset做名字的好处是方便查找。例如你想找位于2049的位置,只要找到2048.kafka的文件即可。当然the first offset就是00000000000.kafka。

 

2 Kafka集群部署

2.1 环境准备

2.1.1 集群规划

hadoop102hadoop103hadoop104
zkzkzk
kafkakafkakafka

 

2.1.2 jar包下载

http://kafka.apache.org/downloads.html

image-20201111215853577

目前稳定版本为2.6,该学期教程是基于kafka0.11.0.0

2.2 Kafka集群部署

1)解压安装包

[atguigu@hadoop102 software]$ tar -zxvf kafka_2.11-0.11.0.0.tgz -C /opt/module/

2)修改解压后的文件名称

[atguigu@hadoop102 module]$ mv kafka_2.11-0.11.0.0/ kafka

3)在/opt/module/kafka目录下创建logs文件夹

[atguigu@hadoop102 kafka]$ mkdir logs

4)修改配置文件

[atguigu@hadoop102 kafka]$ cd config/ ​ [atguigu@hadoop102 config]$ vi server.properties

输入以下内容:

#broker的全局唯一编号,不能重复 ​ broker.id=0 ​ #删除topic功能使能 ​ delete.topic.enable=true ​ #处理网络请求的线程数量 ​ num.network.threads=3 ​ #用来处理磁盘IO的现成数量 ​ num.io.threads=8 ​ #发送套接字的缓冲区大小 ​ socket.send.buffer.bytes=102400 ​ #接收套接字的缓冲区大小 ​ socket.receive.buffer.bytes=102400 ​ #请求套接字的缓冲区大小 ​ socket.request.max.bytes=104857600 ​ #kafka运行日志存放的路径     ​ log.dirs=/opt/module/kafka/logs ​ #topic在当前broker上的分区个数 ​ num.partitions=1 ​ #用来恢复和清理data下数据的线程数量 ​ num.recovery.threads.per.data.dir=1 ​ #segment文件保留的最长时间,超时将被删除 ​ log.retention.hours=168 ​ #配置连接Zookeeper集群地址 ​ zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181

5)配置环境变量

[atguigu@hadoop102 module]$ sudo vi /etc/profile ​ ​ #KAFKA_HOME ​ export KAFKA_HOME=/opt/module/kafka ​ export PATH=$PATH:$KAFKA_HOME/bin

 

[atguigu@hadoop102 module]$ source /etc/profile

6)分发安装包

[atguigu@hadoop102 module]$ xsync kafka/

注意:分发之后记得配置其他机器的环境变量

7)分别在hadoop103和hadoop104上修改配置文件/opt/module/kafka/config/server.properties中的broker.id=1、broker.id=2

注:broker.id不得重复

8)启动集群

依次在hadoop102、hadoop103、hadoop104节点上启动kafka

[atguigu@hadoop102 kafka]$ bin/kafka-server-start.sh config/server.properties & ​ [atguigu@hadoop103 kafka]$ bin/kafka-server-start.sh config/server.properties & ​ [atguigu@hadoop104 kafka]$ bin/kafka-server-start.sh config/server.properties &

9)关闭集群

[atguigu@hadoop102 kafka]$ bin/kafka-server-stop.sh stop ​ [atguigu@hadoop103 kafka]$ bin/kafka-server-stop.sh stop ​ [atguigu@hadoop104 kafka]$ bin/kafka-server-stop.sh stop

 

2.3 Kafka命令行操作

1)查看当前服务器中的所有topic

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --zookeeper hadoop102:2181 --list

2)创建topic

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --zookeeper hadoop102:2181 \ ​ --create --replication-factor 3 --partitions 1 --topic first

选项说明:

--topic 定义topic名

--replication-factor 定义副本数

--partitions 定义分区数

3)删除topic

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --zookeeper hadoop102:2181 \ ​ --delete --topic first

需要server.properties中设置delete.topic.enable=true否则只是标记删除或者直接重启。

4)发送消息

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh \ ​ --broker-list hadoop102:9092 --topic first ​ \>hello world ​ \>atguigu atguigu

 

5)消费消息

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh \ ​ --zookeeper hadoop102:2181 --from-beginning --topic first

--from-beginning:会把first主题中以往所有的数据都读取出来。根据业务场景选择是否增加该配置。

6)查看某个Topic的详情

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --zookeeper hadoop102:2181 \ ​ --describe --topic first

 

3 Kafka工作流程分析

image-20201111220845169

3.1 Kafka生产过程分析

3.1.1 写入方式

producer采用推(push)模式将消息发布到broker,每条消息都被追加(append)到分区(patition)中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障kafka吞吐率)。

3.1.2 分区(Partition)

消息发送时都被发送到一个topic,其本质就是一个目录,而topic是由一些Partition Logs(分区日志)组成,其组织结构如下图所示:

image-20201111220932251

 

image-20201111220943017

我们可以看到,每个Partition中的消息都是有序的,生产的消息被不断追加到Partition log上,其中的每一个消息都被赋予了一个唯一的offset值。

1)分区的原因

(1)方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;

(2)可以提高并发,因为可以以Partition为单位读写了。

2)分区的原则

(1)指定了patition,则直接使用;

(2)未指定patition但指定key,通过对key的value进行hash出一个patition;

(3)patition和key都未指定,使用轮询选出一个patition。

 

3.1.3 副本(Replication)

同一个partition可能会有多个replication(对应 server.properties 配置中的 default.replication.factor=N)。没有replication的情况下,一旦broker 宕机,其上所有 patition 的数据都不可被消费,同时producer也不能再将数据存于其上的patition。引入replication之后,同一个partition可能会有多个replication,而这时需要在这些replication之间选出一个leader,producer和consumer只与这个leader交互,其它replication作为follower从leader 中复制数据。

 

3.1.4 写入流程

producer写入消息流程如下:

image-20201111221251487

1)producer先从zookeeper的 "/brokers/.../state"节点找到该partition的leader

2)producer将消息发送给该leader

3)leader将消息写入本地log

4)followers从leader pull消息,写入本地log后向leader发送ACK

5)leader收到所有ISR中的replication的ACK后,增加HW(high watermark,最后commit 的offset)并向producer发送ACK

 

3.2 Broker 保存消息

3.2.1 存储方式

物理上把topic分成一个或多个patition(对应 server.properties 中的num.partitions=3配置),每个patition物理上对应一个文件夹(该文件夹存储该patition的所有消息和索引文件),如下:

[atguigu@hadoop102 logs]$ ll drwxrwxr-x. 2 atguigu atguigu  4096 8月   6 14:37 first-0 drwxrwxr-x. 2 atguigu atguigu  4096 8月   6 14:35 first-1 drwxrwxr-x. 2 atguigu atguigu  4096 8月   6 14:37 first-2 [atguigu@hadoop102 logs]$ cd first-0 [atguigu@hadoop102 first-0]$ ll -rw-rw-r--. 1 atguigu atguigu 10485760 8月   6 14:33 00000000000000000000.index -rw-rw-r--. 1 atguigu atguigu      219 8月   6 15:07 00000000000000000000.log -rw-rw-r--. 1 atguigu atguigu 10485756 8月   6 14:33 00000000000000000000.timeindex -rw-rw-r--. 1 atguigu atguigu        8 8月   6 14:37 leader-epoch-checkpoint ​

3.2.2 存储策略

无论消息是否被消费,kafka都会保留所有消息。有两种策略可以删除旧数据:

1)基于时间:log.retention.hours=168

2)基于大小:log.retention.bytes=1073741824

需要注意的是,因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高 Kafka 性能无关。

 

3.2.3 Zookeeper存储结构

image-20201111221534351

注意:producer不在zk中注册,消费者在zk中注册。

 

3.3 Kafka消费过程分析

kafka提供了两套consumer API:高级Consumer API和低级Consumer API。

3.3.1 高级API

1)高级API优点

高级API 写起来简单

不需要自行去管理offset,系统通过zookeeper自行管理。

不需要管理分区,副本等情况,.系统自动管理。

消费者断线会自动根据上一次记录在zookeeper中的offset去接着获取数据(默认设置1分钟更新一下zookeeper中存的offset)

可以使用group来区分对同一个topic 的不同程序访问分离开来(不同的group记录不同的offset,这样不同程序读取同一个topic才不会因为offset互相影响)

2)高级API缺点

不能自行控制offset(对于某些特殊需求来说)

不能细化控制如分区、副本、zk等

 

3.3.2 低级API

1)低级 API 优点

能够让开发者自己控制offset,想从哪里读取就从哪里读取。

自行控制连接分区,对分区自定义进行负载均衡

对zookeeper的依赖性降低(如:offset不一定非要靠zk存储,自行存储offset即可,比如存在文件或者内存中)

2)低级API缺点

太过复杂,需要自行控制offset,连接哪个分区,找到分区leader 等。

 

3.3.3 消费者组

image-20201111231304674

消费者是以consumer group消费者组的方式工作,由一个或者多个消费者组成一个组,共同消费一个topic。每个分区在同一时间只能由group中的一个消费者读取,但是多个group可以同时消费这个partition。在图中,有一个由三个消费者组成的group,有一个消费者读取主题中的两个分区,另外两个分别读取一个分区。某个消费者读取某个分区,也可以叫做某个消费者是某个分区的拥有者。

在这种情况下,消费者可以通过水平扩展的方式同时读取大量的消息。另外,如果一个消费者失败了,那么其他的group成员会自动负载均衡读取之前失败的消费者读取的分区。

 

3.3.4 消费方式

consumer采用pull(拉)模式从broker中读取数据。

push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。

对于Kafka而言,pull模式更合适,它可简化broker的设计,consumer可自主控制消费消息的速率,同时consumer可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。

pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直等待数据到达。为了避免这种情况,我们在我们的拉请求中有参数,允许消费者请求在等待数据到达的“长轮询”中进行阻塞(并且可选地等待到给定的字节数,以确保大的传输大小)。

 

3.3.5 消费者组案例

1)需求:测试同一个消费者组中的消费者,同一时刻只能有一个消费者消费。

2)案例实操

(1)在hadoop102、hadoop103上修改/opt/module/kafka/config/consumer.properties配置文件中的group.id属性为任意组名。

[atguigu@hadoop103 config]$ vi consumer.properties group.id=atguigu

(2)在hadoop102、hadoop103上分别启动消费者

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh \ --zookeeper hadoop102:2181 --topic first --consumer.config config/consumer.properties [atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --zookeeper hadoop102:2181 --topic first --consumer.config config/consumer.properties

(3)在hadoop104上启动生产者

[atguigu@hadoop104 kafka]$ bin/kafka-console-producer.sh \ --broker-list hadoop102:9092 --topic first \>hello world

(4)查看hadoop102和hadoop103的接收者。

同一时刻只有一个消费者接收到消息。

 

4 Kafka API实战

4.1 环境准备

1)启动zk和kafka集群,在kafka集群中打开一个消费者

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh \ --zookeeper hadoop102:2181 --topic first

2)导入pom依赖

<dependencies> ​   <!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients --> ​   <dependency> ​     <groupId>org.apache.kafka</groupId> ​     <artifactId>kafka-clients</artifactId> ​     <version>0.11.0.0</version> ​   </dependency> ​   <!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka --> ​   <dependency> ​     <groupId>org.apache.kafka</groupId> ​     <artifactId>kafka_2.12</artifactId> ​     <version>0.11.0.0</version> ​   </dependency> ​ </dependencies>

 

4.2 Kafka生产者Java API

4.2.1 创建生产者(过时的API)

package com.atguigu.kafka; ​ import java.util.Properties; ​ import kafka.javaapi.producer.Producer; ​ import kafka.producer.KeyedMessage; ​ import kafka.producer.ProducerConfig; ​ ​ public class OldProducer { ​ ​   @SuppressWarnings("deprecation") ​   public static void main(String[] args) { ​     ​     Properties properties = new Properties(); ​     properties.put("metadata.broker.list", "hadoop102:9092"); ​     properties.put("request.required.acks", "1"); ​     properties.put("serializer.class", "kafka.serializer.StringEncoder"); ​     ​     Producer<Integer, String> producer = new Producer<Integer,String>(new ProducerConfig(properties)); ​     ​     KeyedMessage<Integer, String> message = new KeyedMessage<Integer, String>("first", "hello world"); ​     producer.send(message ); ​  } ​ }

 

4.2.2 创建生产者(新API)

package com.atguigu.kafka; import java.util.Properties; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; public class NewProducer { public static void main(String[] args) { Properties props = new Properties(); // Kafka服务端的主机名和端口号 props.put("bootstrap.servers", "hadoop103:9092"); // 等待所有副本节点的应答 props.put("acks", "all"); // 消息发送最大尝试次数 props.put("retries", 0); // 一批消息处理大小 props.put("batch.size", 16384); // 请求延时 props.put("linger.ms", 1); // 发送缓存区内存大小 props.put("buffer.memory", 33554432); // key序列化 props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // value序列化 props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer<String, String> producer = new KafkaProducer<>(props); for (int i = 0; i < 50; i++) { producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), "hello world-" + i)); } producer.close(); } }

 

4.2.3 创建生产者带回调函数(新API)

package com.atguigu.kafka; import java.util.Properties; import org.apache.kafka.clients.producer.Callback; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; public class CallBackProducer { public static void main(String[] args) { Properties props = new Properties(); // Kafka服务端的主机名和端口号 props.put("bootstrap.servers", "hadoop103:9092"); // 等待所有副本节点的应答 props.put("acks", "all"); // 消息发送最大尝试次数 props.put("retries", 0); // 一批消息处理大小 props.put("batch.size", 16384); // 增加服务端请求延时 props.put("linger.ms", 1); // 发送缓存区内存大小 props.put("buffer.memory", 33554432); // key序列化 props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // value序列化 props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props); for (int i = 0; i < 50; i++) { kafkaProducer.send(new ProducerRecord<String, String>("first", "hello" + i), new Callback() { @Override public void onCompletion(RecordMetadata metadata, Exception exception) { if (metadata != null) { System.err.println(metadata.partition() + "---" + metadata.offset()); } } }); } kafkaProducer.close(); } }

 

4.2.4 自定义分区生产者

0)需求:将所有数据存储到topic的第0号分区上

1)定义一个类实现Partitioner接口,重写里面的方法(过时API)

package com.atguigu.kafka; import java.util.Map; import kafka.producer.Partitioner; public class CustomPartitioner implements Partitioner { public CustomPartitioner() { super(); } @Override public int partition(Object key, int numPartitions) { // 控制分区 return 0; } }

2)自定义分区(新API)

package com.atguigu.kafka; import java.util.Map; import org.apache.kafka.clients.producer.Partitioner; import org.apache.kafka.common.Cluster; public class CustomPartitioner implements Partitioner { @Override public void configure(Map<String, ?> configs) { } @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { // 控制分区 return 0; } @Override public void close() { } }

3)在代码中调用

package com.atguigu.kafka; import java.util.Properties; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerRecord; public class PartitionerProducer { public static void main(String[] args) { Properties props = new Properties(); // Kafka服务端的主机名和端口号 props.put("bootstrap.servers", "hadoop103:9092"); // 等待所有副本节点的应答 props.put("acks", "all"); // 消息发送最大尝试次数 props.put("retries", 0); // 一批消息处理大小 props.put("batch.size", 16384); // 增加服务端请求延时 props.put("linger.ms", 1); // 发送缓存区内存大小 props.put("buffer.memory", 33554432); // key序列化 props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // value序列化 props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 自定义分区 props.put("partitioner.class", "com.atguigu.kafka.CustomPartitioner"); Producer<String, String> producer = new KafkaProducer<>(props); producer.send(new ProducerRecord<String, String>("first", "1", "atguigu")); producer.close(); } }

4)测试

(1)在hadoop102上监控/opt/module/kafka/logs/目录下first主题3个分区的log日志动态变化情况

[atguigu@hadoop102 first-0]$ tail -f 00000000000000000000.log

[atguigu@hadoop102 first-1]$ tail -f 00000000000000000000.log

[atguigu@hadoop102 first-2]$ tail -f 00000000000000000000.log

(2)发现数据都存储到指定的分区了。

 

4.3 Kafka消费者Java API

4.3.1 高级API

0)在控制台创建发送者

[atguigu@hadoop104 kafka]$ bin/kafka-console-producer.sh \ --broker-list hadoop102:9092 --topic first >hello world

1)创建消费者(过时API)

package com.atguigu.kafka.consume; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import kafka.consumer.Consumer; import kafka.consumer.ConsumerConfig; import kafka.consumer.ConsumerIterator; import kafka.consumer.KafkaStream; import kafka.javaapi.consumer.ConsumerConnector; public class CustomConsumer { @SuppressWarnings("deprecation") public static void main(String[] args) { Properties properties = new Properties(); properties.put("zookeeper.connect", "hadoop102:2181"); properties.put("group.id", "g1"); properties.put("zookeeper.session.timeout.ms", "500"); properties.put("zookeeper.sync.time.ms", "250"); properties.put("auto.commit.interval.ms", "1000"); // 创建消费者连接器 ConsumerConnector consumer = Consumer.createJavaConsumerConnector(new ConsumerConfig(properties)); HashMap<String, Integer> topicCount = new HashMap<>(); topicCount.put("first", 1); Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap = consumer.createMessageStreams(topicCount); KafkaStream<byte[], byte[]> stream = consumerMap.get("first").get(0); ConsumerIterator<byte[], byte[]> it = stream.iterator(); while (it.hasNext()) { System.out.println(new String(it.next().message())); } } }

2)官方提供案例(自动维护消费情况)(新API)

package com.atguigu.kafka.consume; import java.util.Arrays; import java.util.Properties; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; import org.apache.kafka.clients.consumer.KafkaConsumer; public class CustomNewConsumer { public static void main(String[] args) { Properties props = new Properties(); // 定义kakfa 服务的地址,不需要将所有broker指定上 props.put("bootstrap.servers", "hadoop102:9092"); // 制定consumer group props.put("group.id", "test"); // 是否自动确认offset props.put("enable.auto.commit", "true"); // 自动确认offset的时间间隔 props.put("auto.commit.interval.ms", "1000"); // key的序列化类 props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); // value的序列化类 props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); // 定义consumer KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); // 消费者订阅的topic, 可同时订阅多个 consumer.subscribe(Arrays.asList("first", "second","third")); while (true) { // 读取数据,读取超时时间为100ms ConsumerRecords<String, String> records = consumer.poll(100); for (ConsumerRecord<String, String> record : records) System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value()); } } }

 

4.3.2 低级API

实现使用低级API读取指定topic,指定partition,指定offset的数据。

1)消费者使用低级API 的主要步骤:

步骤主要工作
1根据指定的分区从主题元数据中找到主副本
2获取分区最新的消费进度
3从主副本拉取分区的消息
4识别主副本的变化,重试

2)方法描述:

findLeader()客户端向种子节点发送主题元数据,将副本集加入备用节点
getLastOffset()消费者客户端发送偏移量请求,获取分区最近的偏移量
run()消费者低级AP I拉取消息的主要方法
findNewLeader()当分区的主副本节点发生故障,客户将要找出新的主副本

3)代码:

package com.atguigu; ​ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; ​ import kafka.api.FetchRequest; import kafka.api.FetchRequestBuilder; import kafka.api.PartitionOffsetRequestInfo; import kafka.cluster.BrokerEndPoint; import kafka.common.ErrorMapping; import kafka.common.TopicAndPartition; import kafka.javaapi.FetchResponse; import kafka.javaapi.OffsetResponse; import kafka.javaapi.PartitionMetadata; import kafka.javaapi.TopicMetadata; import kafka.javaapi.TopicMetadataRequest; import kafka.javaapi.consumer.SimpleConsumer; import kafka.message.MessageAndOffset; public class SimpleExample { ​  private List<String> m_replicaBrokers = new ArrayList<>();  public SimpleExample() { ​    m_replicaBrokers = new ArrayList<>(); ​  } ​  public static void main(String args[]) {    SimpleExample example = new SimpleExample();    // 最大读取消息数量    long maxReads = Long.parseLong("3");    // 要订阅的topic    String topic = "test1";    // 要查找的分区    int partition = Integer.parseInt("0");    // broker节点的ip    List<String> seeds = new ArrayList<>();    seeds.add("192.168.9.102");    seeds.add("192.168.9.103");    seeds.add("192.168.9.104");    // 端口    int port = Integer.parseInt("9092"); ​    try { ​      example.run(maxReads, topic, partition, seeds, port); ​    } catch (Exception e) {      System.out.println("Oops:" + e);      e.printStackTrace();    }  }  public void run(long a_maxReads, String a_topic, int a_partition, List<String> a_seedBrokers, int a_port) throws Exception { ​    // 获取指定Topic partition的元数据 ​    PartitionMetadata metadata = findLeader(a_seedBrokers, a_port, a_topic, a_partition); ​    if (metadata == null) { ​      System.out.println("Can't find metadata for Topic and Partition. Exiting"); ​      return; ​    } ​    if (metadata.leader() == null) { ​      System.out.println("Can't find Leader for Topic and Partition. Exiting"); ​      return; ​    } ​    String leadBroker = metadata.leader().host(); ​    String clientName = "Client_" + a_topic + "_" + a_partition; ​ ​    SimpleConsumer consumer = new SimpleConsumer(leadBroker, a_port, 100000, 64 * 1024, clientName); ​    long readOffset = getLastOffset(consumer, a_topic, a_partition, kafka.api.OffsetRequest.EarliestTime(), clientName); ​    int numErrors = 0; ​    while (a_maxReads > 0) { ​      if (consumer == null) { ​        consumer = new SimpleConsumer(leadBroker, a_port, 100000, 64 * 1024, clientName); ​      } ​      FetchRequest req = new FetchRequestBuilder().clientId(clientName).addFetch(a_topic, a_partition, readOffset, 100000).build(); ​      FetchResponse fetchResponse = consumer.fetch(req); ​ ​      if (fetchResponse.hasError()) { ​        numErrors++; ​        // Something went wrong! ​        short code = fetchResponse.errorCode(a_topic, a_partition); ​        System.out.println("Error fetching data from the Broker:" + leadBroker + " Reason: " + code); ​        if (numErrors > 5) ​          break; ​        if (code == ErrorMapping.OffsetOutOfRangeCode()) { ​          // We asked for an invalid offset. For simple case ask for ​          // the last element to reset ​          readOffset = getLastOffset(consumer, a_topic, a_partition, kafka.api.OffsetRequest.LatestTime(), clientName); ​          continue; ​        } ​        consumer.close(); ​        consumer = null; ​        leadBroker = findNewLeader(leadBroker, a_topic, a_partition, a_port); ​        continue; ​      } ​      numErrors = 0; ​ ​      long numRead = 0; ​      for (MessageAndOffset messageAndOffset : fetchResponse.messageSet(a_topic, a_partition)) { ​        long currentOffset = messageAndOffset.offset(); ​        if (currentOffset < readOffset) { ​          System.out.println("Found an old offset: " + currentOffset + " Expecting: " + readOffset); ​          continue; ​        } ​        readOffset = messageAndOffset.nextOffset(); ​        ByteBuffer payload = messageAndOffset.message().payload(); ​ ​        byte[] bytes = new byte[payload.limit()]; ​        payload.get(bytes); ​        System.out.println(String.valueOf(messageAndOffset.offset()) + ": " + new String(bytes, "UTF-8")); ​        numRead++; ​        a_maxReads--; ​      } ​ ​      if (numRead == 0) { ​        try { ​          Thread.sleep(1000); ​        } catch (InterruptedException ie) { ​        } ​      } ​    } ​    if (consumer != null) ​      consumer.close(); ​  } ​ ​  public static long getLastOffset(SimpleConsumer consumer, String topic, int partition, long whichTime, String clientName) { ​    TopicAndPartition topicAndPartition = new TopicAndPartition(topic, partition); ​    Map<TopicAndPartition, PartitionOffsetRequestInfo> requestInfo = new HashMap<TopicAndPartition, PartitionOffsetRequestInfo>(); ​    requestInfo.put(topicAndPartition, new PartitionOffsetRequestInfo(whichTime, 1)); ​    kafka.javaapi.OffsetRequest request = new kafka.javaapi.OffsetRequest(requestInfo, kafka.api.OffsetRequest.CurrentVersion(), clientName); ​    OffsetResponse response = consumer.getOffsetsBefore(request); ​ ​    if (response.hasError()) { ​      System.out.println("Error fetching data Offset Data the Broker. Reason: " + response.errorCode(topic, partition)); ​      return 0; ​    } ​    long[] offsets = response.offsets(topic, partition); ​    return offsets[0]; ​  } ​ ​ ​  private String findNewLeader(String a_oldLeader, String a_topic, int a_partition, int a_port) throws Exception { ​    for (int i = 0; i < 3; i++) { ​      boolean goToSleep = false; ​      PartitionMetadata metadata = findLeader(m_replicaBrokers, a_port, a_topic, a_partition); ​      if (metadata == null) { ​        goToSleep = true; ​      } else if (metadata.leader() == null) { ​        goToSleep = true; ​      } else if (a_oldLeader.equalsIgnoreCase(metadata.leader().host()) && i == 0) { ​        // first time through if the leader hasn't changed give ​        // ZooKeeper a second to recover ​        // second time, assume the broker did recover before failover, ​        // or it was a non-Broker issue ​        // ​        goToSleep = true; ​      } else { ​        return metadata.leader().host(); ​      } ​      if (goToSleep) { ​            Thread.sleep(1000); ​      } ​    } ​    System.out.println("Unable to find new leader after Broker failure. Exiting"); ​    throw new Exception("Unable to find new leader after Broker failure. Exiting"); ​  } ​ ​  private PartitionMetadata findLeader(List<String> a_seedBrokers, int a_port, String a_topic, int a_partition) { ​    PartitionMetadata returnMetaData = null; ​    loop: ​    for (String seed : a_seedBrokers) { ​      SimpleConsumer consumer = null; ​      try { ​        consumer = new SimpleConsumer(seed, a_port, 100000, 64 * 1024, "leaderLookup"); ​        List<String> topics = Collections.singletonList(a_topic); ​        TopicMetadataRequest req = new TopicMetadataRequest(topics); ​        kafka.javaapi.TopicMetadataResponse resp = consumer.send(req); ​ ​        List<TopicMetadata> metaData = resp.topicsMetadata(); ​        for (TopicMetadata item : metaData) { ​          for (PartitionMetadata part : item.partitionsMetadata()) { ​            if (part.partitionId() == a_partition) { ​              returnMetaData = part; ​                break loop; ​            } ​          } ​        } ​      } catch (Exception e) { ​        System.out.println("Error communicating with Broker [" + seed + "] to find Leader for [" + a_topic + ", " + a_partition + "] Reason: " + e); ​      } finally { ​        if (consumer != null) ​          consumer.close(); ​      } ​    } ​    if (returnMetaData != null) { ​      m_replicaBrokers.clear(); ​      for (BrokerEndPoint replica : returnMetaData.replicas()) { ​        m_replicaBrokers.add(replica.host()); ​      } ​    } ​    return returnMetaData; ​  } ​ }

 

5 Kafka producer拦截器(interceptor)

5.1 拦截器原理

Producer拦截器(interceptor)是在Kafka 0.10版本被引入的,主要用于实现clients端的定制化控制逻辑。

对于producer而言,interceptor使得用户在消息发送前以及producer回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。同时,producer允许用户指定多个interceptor按序作用于同一条消息从而形成一个拦截链(interceptor chain)。Intercetpor的实现接口是org.apache.kafka.clients.producer.ProducerInterceptor,其定义的方法包括:

(1)configure(configs)

获取配置信息和初始化数据时调用。

(2)onSend(ProducerRecord):

该方法封装进KafkaProducer.send方法中,即它运行在用户主线程中。Producer确保在消息被序列化以及计算分区前调用该方法。用户可以在该方法中对消息做任何操作,但最好保证不要修改消息所属的topic和分区,否则会影响目标分区的计算

(3)onAcknowledgement(RecordMetadata, Exception):

该方法会在消息被应答或消息发送失败时调用,并且通常都是在producer回调逻辑触发之前。onAcknowledgement运行在producer的IO线程中,因此不要在该方法中放入很重的逻辑,否则会拖慢producer的消息发送效率

(4)close:

关闭interceptor,主要用于执行一些资源清理工作

如前所述,interceptor可能被运行在多个线程中,因此在具体实现时用户需要自行确保线程安全。另外倘若指定了多个interceptor,则producer将按照指定顺序调用它们,并仅仅是捕获每个interceptor可能抛出的异常记录到错误日志中而非在向上传递。这在使用过程中要特别留意。

5.2 拦截器案例

1)需求:

实现一个简单的双interceptor组成的拦截链。第一个interceptor会在消息发送前将时间戳信息加到消息value的最前部;第二个interceptor会在消息发送后更新成功发送消息数或失败发送消息数。

image-20201112165815731

2)案例实操

(1)增加时间戳拦截器

package com.atguigu.kafka.interceptor; import java.util.Map; import org.apache.kafka.clients.producer.ProducerInterceptor; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; public class TimeInterceptor implements ProducerInterceptor<String, String> { @Override public void configure(Map<String, ?> configs) { } @Override public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) { // 创建一个新的record,把时间戳写入消息体的最前部 return new ProducerRecord(record.topic(), record.partition(), record.timestamp(), record.key(), System.currentTimeMillis() + "," + record.value().toString()); } @Override public void onAcknowledgement(RecordMetadata metadata, Exception exception) { } @Override public void close() { } }

(2)统计发送消息成功和发送失败消息数,并在producer关闭时打印这两个计数器

package com.atguigu.kafka.interceptor; import java.util.Map; import org.apache.kafka.clients.producer.ProducerInterceptor; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.clients.producer.RecordMetadata; public class CounterInterceptor implements ProducerInterceptor<String, String>{ private int errorCounter = 0; private int successCounter = 0; @Override public void configure(Map<String, ?> configs) { } @Override public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) { return record; } @Override public void onAcknowledgement(RecordMetadata metadata, Exception exception) { // 统计成功和失败的次数 if (exception == null) { successCounter++; } else { errorCounter++; } } @Override public void close() { // 保存结果 System.out.println("Successful sent: " + successCounter); System.out.println("Failed sent: " + errorCounter); } }

(3)producer主程序

package com.atguigu.kafka.interceptor; import java.util.ArrayList; import java.util.List; import java.util.Properties; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; public class InterceptorProducer { public static void main(String[] args) throws Exception { // 1 设置配置信息 Properties props = new Properties(); props.put("bootstrap.servers", "hadoop102:9092"); props.put("acks", "all"); props.put("retries", 0); props.put("batch.size", 16384); props.put("linger.ms", 1); props.put("buffer.memory", 33554432); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); // 2 构建拦截链 List<String> interceptors = new ArrayList<>(); interceptors.add("com.atguigu.kafka.interceptor.TimeInterceptor"); interceptors.add("com.atguigu.kafka.interceptor.CounterInterceptor"); props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors); String topic = "first"; Producer<String, String> producer = new KafkaProducer<>(props); // 3 发送消息 for (int i = 0; i < 10; i++) { ProducerRecord<String, String> record = new ProducerRecord<>(topic, "message" + i); producer.send(record); } // 4 一定要关闭producer,这样才会调用interceptor的close方法 producer.close(); } }

3)测试

(1)在kafka上启动消费者,然后运行客户端java程序。

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh \ --zookeeper hadoop102:2181 --from-beginning --topic first

 

1501904047034,message0 1501904047225,message1 1501904047230,message2 1501904047234,message3 1501904047236,message4 1501904047240,message5 1501904047243,message6 1501904047246,message7 1501904047249,message8 1501904047252,message9

(2)观察java平台控制台输出数据如下:

Successful sent: 10 Failed sent: 0

 

6 Kafka Streams

6.1 概述

6.1.1 Kafka Streams

Kafka Streams。Apache Kafka开源项目的一个组成部分。是一个功能强大,易于使用的库。用于在Kafka上构建高可分布式、拓展性,容错的应用程序。

6.1.2 Kafka Streams特点

1)功能强大

高扩展性,弹性,容错

2)轻量级

无需专门的集群

一个库,而不是框架

3)完全集成

100%的Kafka 0.10.0版本兼容

易于集成到现有的应用程序

4)实时性

毫秒级延迟

并非微批处理

窗口允许乱序数据

允许迟到数据

6.1.3 为什么要有Kafka Stream

当前已经有非常多的流式处理系统,最知名且应用最多的开源流式处理系统有Spark Streaming和Apache Storm。Apache Storm发展多年,应用广泛,提供记录级别的处理能力,当前也支持SQL on Stream。而Spark Streaming基于Apache Spark,可以非常方便与图计算,SQL处理等集成,功能强大,对于熟悉其它Spark应用开发的用户而言使用门槛低。另外,目前主流的Hadoop发行版,如Cloudera和Hortonworks,都集成了Apache Storm和Apache Spark,使得部署更容易。

既然Apache Spark与Apache Storm拥用如此多的优势,那为何还需要Kafka Stream呢?主要有如下原因。

第一,Spark和Storm都是流式处理框架,而Kafka Stream提供的是一个基于Kafka的流式处理类库。框架要求开发者按照特定的方式去开发逻辑部分,供框架调用。开发者很难了解框架的具体运行方式,从而使得调试成本高,并且使用受限。而Kafka Stream作为流式处理类库,直接提供具体的类给开发者调用,整个应用的运行方式主要由开发者控制,方便使用和调试。

image-20201112165842987

第二,虽然Cloudera与Hortonworks方便了Storm和Spark的部署,但是这些框架的部署仍然相对复杂。而Kafka Stream作为类库,可以非常方便的嵌入应用程序中,它对应用的打包和部署基本没有任何要求。

第三,就流式处理系统而言,基本都支持Kafka作为数据源。例如Storm具有专门的kafka-spout,而Spark也提供专门的spark-streaming-kafka模块。事实上,Kafka基本上是主流的流式处理系统的标准数据源。换言之,大部分流式系统中都已部署了Kafka,此时使用Kafka Stream的成本非常低。

第四,使用Storm或Spark Streaming时,需要为框架本身的进程预留资源,如Storm的supervisor和Spark on YARN的node manager。即使对于应用实例而言,框架本身也会占用部分资源,如Spark Streaming需要为shuffle和storage预留内存。但是Kafka作为类库不占用系统资源。

第五,由于Kafka本身提供数据持久化,因此Kafka Stream提供滚动部署和滚动升级以及重新计算的能力。

第六,由于Kafka Consumer Rebalance机制,Kafka Stream可以在线动态调整并行度。

6.2 Kafka Stream数据清洗案例

0)需求:

实时处理单词带有”>>>”前缀的内容。例如输入”atguigu>>>ximenqing”,最终处理成“ximenqing”

1)需求分析:

 

2)案例实操

(1)创建一个工程,并添加jar包

(2)创建主类

package com.atguigu.kafka.stream; import java.util.Properties; import org.apache.kafka.streams.KafkaStreams; import org.apache.kafka.streams.StreamsConfig; import org.apache.kafka.streams.processor.Processor; import org.apache.kafka.streams.processor.ProcessorSupplier; import org.apache.kafka.streams.processor.TopologyBuilder; public class Application { public static void main(String[] args) { // 定义输入的topic String from = "first"; // 定义输出的topic String to = "second"; // 设置参数 Properties settings = new Properties(); settings.put(StreamsConfig.APPLICATION_ID_CONFIG, "logFilter"); settings.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092"); StreamsConfig config = new StreamsConfig(settings); // 构建拓扑 TopologyBuilder builder = new TopologyBuilder(); builder.addSource("SOURCE", from) .addProcessor("PROCESS", new ProcessorSupplier<byte[], byte[]>() { @Override public Processor<byte[], byte[]> get() { // 具体分析处理 return new LogProcessor(); } }, "SOURCE") .addSink("SINK", to, "PROCESS"); // 创建kafka stream KafkaStreams streams = new KafkaStreams(builder, config); streams.start(); } }

(3)具体业务处理

package com.atguigu.kafka.stream; import org.apache.kafka.streams.processor.Processor; import org.apache.kafka.streams.processor.ProcessorContext; public class LogProcessor implements Processor<byte[], byte[]> { private ProcessorContext context; @Override public void init(ProcessorContext context) { this.context = context; } @Override public void process(byte[] key, byte[] value) { String input = new String(value); // 如果包含“>>>”则只保留该标记后面的内容 if (input.contains(">>>")) { input = input.split(">>>")[1].trim(); // 输出到下一个topic context.forward("logProcessor".getBytes(), input.getBytes()); }else{ context.forward("logProcessor".getBytes(), input.getBytes()); } } @Override public void punctuate(long timestamp) { } @Override public void close() { } }

(4)运行程序

(5)在hadoop104上启动生产者

[atguigu@hadoop104 kafka]$ bin/kafka-console-producer.sh \ --broker-list hadoop102:9092 --topic first >hello>>>world >h>>>atguigu >hahaha

(6)在hadoop103上启动消费者

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh \ ​ --zookeeper hadoop102:2181 --from-beginning --topic second ​ ​ world ​ atguigu ​ hahaha

 

7 补充

7.1 kafka一直在rebalance问题

关于Kafka的三个配置

session.timeout.ms=10000 // 单位:毫秒,kafka会有一个心跳线程来同步服务端,告诉服务端自己是正常可用的,默认是3秒发送一次心跳,超过session.timeout.ms(默认10秒)服务端没有收到心跳就会认为当前消费者失效。 ​ max.poll.interval.ms=300000 // 单位:毫秒,决定了获取消息后提交偏移量的最大时间,超过设定的时间(默认5分钟),服务端也会认为该消费者失效。 ​ max.poll.records=500 //合理设置每次poll的消息消费的数量,默认是500,如果数量过多,导致一次poll的操作返回的消息记录无法在指定时间内完成,则会出现rebalance,即kafka服务端认为消费者失效,会重新分配分区,导致偏移量没有提交,从而会导致重复消费。

img

有朋友遇到了一个问题,发现kafka一直在rebalance,(当rebalance后,之前该consumer拥有的分区和offset信息就失效了,同时导致不断的报auto offset commit failed。)通过改变如下两个配置解决了问题,

例如:确保获取300条消息在400秒之内消费完成,提交偏移量offset,并再次去poll()。这样就不会出现这个问题。

#把这个调改大些,默认是300秒 max.poll.interval.ms=400000 #把这个改小些,默认是500条消息 max.poll.records=300

 

好了,本次内容就是这些,学无止境,关注我,我们一起学习进步。如果觉得内容还可以,帮忙点个赞,点个在看呗,谢谢~我们下期见。

资料获取:关注公众号【良辰】,回复关键字Kafka获取,可获取笔记和学习视频。

良辰公众号二维码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值