一、Kafka概述
Kafka 是点对点的方式
1、消息队列
消息队列内部实现原理
(1)点对点模式(一对一,消费者主动拉取数据,消息收到后消息清除)
点对点模型通常是一个基于拉取或者轮询的消息传送模型,这种模型从队列中请求信息,而不是将消息推送
到客户端。这个模型的特点是发送到队列的消息被一个且只有一个接收者接收处理,即使有多个消息监听者
也是如此。
(2)发布/订阅模式(一对多,数据生产后,推送给所有订阅者)
发布订阅模型则是一个基于推送的消息传送模型。发布订阅模型可以有多种不同的订阅者,临时订阅者只在
主动监听主题时才接收消息,而持久订阅者则监听主题的所有消息,即使当前订阅者不可用,处于离线状
态。
2、为什么需要消息队列
1) 解耦:
允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
2) 冗余:
消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列
所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息
已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。
3) 扩展性:
因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程
即可。
4) 灵活性 & 峰值处理能力:
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这
类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问
压力,而不会因为突发的超负荷的请求而完全崩溃。
5) 可恢复性:
系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消
息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
6) 顺序保证:
在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照
特定的顺序来处理。(Kafka 保证一个 Partition 内的消息的有序性)
7) 缓冲:
有助于控制和优化数据流经过系统的速度, 解决生产消息和消费消息的处理速度不一致的情况。
8) 异步通信:
很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队
列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
3、什么是 Kafka
在流式计算中, Kafka 一般用来缓存数据, Storm 通过消费 Kafka 的数据进行计算
1)Apache Kafka 是一个开源消息系统,由 Scala 写成。是由 Apache 软件基金会开发的一个开源消息系统项目
2)Kafka 最初是由 LinkedIn 公司开发,并于 2011 年初开源。 2012 年 10 月从 ApacheIncubator 毕业。该项目
的目标是为处理实时数据提供一个统一、高通量、低等待的平台。
3)Kafka 是一个分布式消息队列。 Kafka 对消息保存时根据 Topic 进行归类,发送消息者称为 Producer,消息
接受者称为 Consumer,此外 kafka 集群有多个 kafka 实例组成,每个实例(server)称为 broker。
4)无论是 kafka 集群,还是 consumer 都依赖于 zookeeper 集群保存一些 meta 信息,来保证系统可用性。
4、Kafka 架构
Kafka 整体架构图
Kafka详细架构图
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。
二、Kafka 集群部署
1、环境准备
①、集群规划
stu-kafka01 | stu-kafka02 | stu-kafka03 |
---|---|---|
zk | zk | zk |
kafka | kafka | kafka |
②、jar 包下载
http://kafka.apache.org/downloads.html,下载后放在 D:\学习\吃饭的家伙\java\file 目录下
2、Kafka集群部署
在 linux 根目录(/)下创建 software 目录,将下载下来的 kafka 包传到这个目录,在 /opt/ 下创建 module 目
录,用作 kafka 解压目录
1) 解压安装包
[root@stu-kafka01 ~]$ cd /software
[root@stu-kafka01 software]$ tar -zxvf kafka_2.11-0.11.0.0.tgz -C /opt/module/
2) 修改解压后的文件名称
[root@stu-kafka01 module]$ cd /opt/module
[root@stu-kafka01 module]$ mv kafka_2.11-0.11.0.0/ kafka
3)在/opt/module/kafka 目录下创建 logs 文件夹,用来保存 kafka 的数据
[root@stu-kafka01 module]$ cd /opt/module/kafka
[root@stu-kafka01 kafka]$ mkdir logs
4) 修改配置文件
[root@stu-kafka01 kafka]$ cd config/
[root@stu-kafka01 kafka]$ vi server.properties
#输入以下内容:
#broker 的全局唯一编号,不能重复
broker.id=0 #要修改
#删除 topic 功能使能
delete.topic.enable=true #要修改
#对应这一台虚拟机的ip地址,这一项是需要添加进去的
host.name=192.168.201.128 #要修改
#处理网络请求的线程数量
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 #要修改, 改为前面创建的 logs 路径
#topic 在当前 broker 上的分区个数
num.partitions=1
#用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
#segment 文件保留的最长时间,超时将被删除
log.retention.hours=168
#保存数据的最大容量,默认1G,超过1G将被删除
log.segment.bytes=1073741824
#配置连接 Zookeeper 集群地址,注意要打开 2181 的防火墙
zookeeper.connect=stu-kafka01:2181,stu-kafka02:2181,stu-kafka03:2181 #要修改
5)配置环境变量
[root@stu-kafka01 kafka]$ sudo vi /etc/profile
#KAFKA_HOME
export KAFKA_HOME=/opt/module/kafka
export PATH=$PATH:$KAFKA_HOME/bin
[root@stu-kafka01 kafka]$ source /etc/profile
#或者在PATH后面直接写
export PATH=/opt/module/kafka/bin: .........
6)分发安装包
[root@stu-kafka01 module]$ xsync kafka/
注意: 分发之后记得配置其他机器的环境变量
7)分别在 stu-kafka02 和 stu-kafka03 上修改配置文件/opt/module/kafka/config/server.properties
中的 broker.id=1、 broker.id=2
注: broker.id 不得重复
8) 启动集群,启动之前必须先打开 kafka 端口号 9092 的防火墙,而且还要先启动 zookeeper 集群
依次在 stu-kafka01、 stu-kafka02、 stu-kafka03 节点上启动 kafka ,& 符号可以在后台运行,如果不加就
会把信息打印到控制台
[root@stu-kafka01 module]$ cd /opt/module/kafka
[root@stu-kafka01 kafka]$ bin/kafka-server-start.sh config/server.properties &
[root@stu-kafka02 kafka]$ bin/kafka-server-start.sh config/server.properties &
[root@stu-kafka03 kafka]$ bin/kafka-server-start.sh config/server.properties &
在启动时,可能会出现下面这类型错误。此时只需修改 vim /etc/hosts ,在第一行将本机的名称加上然后重启
网络即可,重启网络 service network restart
java.net.UnknownHostException: stu-kafka01: stu-kafka01: 未知的名称或服务
这里我们不加 & 启动三个 kafka,然后在重新开一个窗口操作其他命令
9)关闭集群
[root@stu-kafka01 module]$ cd /opt/module/kafka
[root@stu-kafka01 kafka]$ bin/kafka-server-stop.sh stop
[root@stu-kafka02 kafka]$ bin/kafka-server-stop.sh stop
[root@stu-kafka03 kafka]$ bin/kafka-server-stop.sh stop
3、Kafka 命令行操作
查看启动 kafka 的进程:
jps 或者 jps -l
1)查看当前服务器中的所有 topic
cd /opt/module/kafka
[root@stu-kafka01 kafka]$ bin/kafka-topics.sh --zookeeper stu-kafka01:2181 --list
2)创建 topic ,\ 表示换行
[root@stu-kafka01 kafka]$ bin/kafka-topics.sh --zookeeper stu-kafka01:2181 \
--create --replication-factor 3 --partitions 5 --topic first
选项说明:
--topic :定义 topic 名
--replication-factor:定义副本数,副本数不能大于节点数
--partitions:定义分区数
3)删除 topic ,删除时会显示一个 bug
[root@stu-kafka01 kafka]$ bin/kafka-topics.sh --zookeeper stu-kafka01:2181 \
--delete --topic first
需要 server.properties 中设置 delete.topic.enable=true 否则只是标记删除或者直接重启。
4)发送消息
[root@stu-kafka01 kafka]$ bin/kafka-console-producer.sh \
--broker-list stu-kafka01:9092 --topic first
>hello world
>atguigu atguigu
5)消费消息
[root@stu-kafka01 kafka]$ bin/kafka-console-consumer.sh \
--zookeeper stu-kafka01:2181 --from-beginning --topic first
--from-beginning: 会把 first 主题中以往所有的数据都读取出来。 根据业务场景选择是否增加该配置。如果
不加该参数,那么消费者只能读取最新的数据
#在老版本中连接 zookeeper,就是上面的步骤,但是每次消费数据时都会有一个警告或者提示,kafka提示要我们使用
#bootstrap-server 连接,这是一个面试题,问:为什么要将 offset 维护到 kafka本地。
#答:在老版本中,读取数据时的 offset 是维护在 zookeeper 中的,那么消费消息时,除了要到kafka中获取数据
#以外,还要到 zookeeper 中获取 offset,offset的作用就是维护数据的偏移量,这样消费方要与两个服务进行通
#信,效率低。此时kafka想到这一点,就将offset也维护在自己这一侧,不再维护在 zookeeper 中。命令如下:
[root@stu-kafka01 kafka]$ bin/kafka-console-consumer.sh \
--bootstrap-server stu-kafka01:9092 --topic first --from-beginning
6)查看某个 Topic 的详情
[root@stu-kafka01 kafka]$ bin/kafka-topics.sh --zookeeper stu-kafka01:2181 \
--describe --topic first
三、Kafka工作流程分析
**Kafka核心组成 **
1、Kafka 生产过程分析
①、写入方式
producer 采用推(push) 模式将消息发布到 broker,每条消息都被追加(append) 到分区(patition)
中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 kafka 吞吐率)。
②、分区(Partition)
消息发送时都被发送到一个 topic,其本质就是一个目录,而 topic 是由一些 Partition Logs(分区日志)组成, 其
组织结构如下图所示:
我们可以看到,每个 Partition 中的消息都是有序的,生产的消息被不断追加到Partition log 上,其中的每一个
消息都被赋予了一个唯一的 offset 值。 consumer 在 zookeeper 中存放的就是偏移量
消费者消费数据可以重复消费,数据不会因为消费者消费一次而清除
1)分区的原因
(1)方便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,而一个topic 又可以有多个
Partition 组成,因此整个集群就可以适应任意大小的数据了;
(2)可以提高并发,因为可以以 Partition 为单位读写了。
2) 分区的原则
(1)指定了 patition,则直接使用;
(2)未指定 patition 但指定 key,通过对 key 的 value 进行 hash 出一个 patition;
(3)patition 和 key 都未指定,使用轮询选出一个 patition。
//DefaultPartitioner 类
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[]
valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (keyBytes == null) {
int nextValue = nextValue(topic);
List<PartitionInfo> availablePartitions =
cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {
// no partitions are available, give a non-available partition
return Utils.toPositive(nextValue) % numPartitions;
}
} else {
// hash the keyBytes to choose a partition
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
③、副本(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 中复制数据。
④、写入流程
producer 写入消息流程如下:
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
ACK应答机制机制:当 produce 发送消息时,produce 可以设置 ACK 应答机制的参数,具体有三个值,分别
为 0,1和 all。它们的意思是:
0:当 produce 发送消息时,不需要 leader 和 followers 确实消息是否到底,produce 只管写数据,这种
情况可能会存在数据丢失的情况
1:当 produce 发送消息时,只需要 leader 确认数据是否到底,不要 followers 确认,这样会导致
followers 可能没复制 leader 上的数据
all:当produce 发送消息时,要等 leader 和 follower 都确认数据到达,这种可以保证数据不会丢失
当设置为 0 时,效率比设置为 all 高 10 倍
2、Broker 保存消息
①、存储方式
物理上把 topic 分成一个或多个 patition(对应 server.properties 中的 num.partitions=3 配置),每个
patition 物理上对应一个文件夹(该文件夹存储该 patition 的所有消息和索引文件),如下: 数据保存在以
.log 结尾的文件中
[root@stu-kafka01 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
[root@stu-kafka01 logs]$ cd first-0
[root@stu-kafka01 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
②、存储策略
无论消息是否被消费, kafka 都会保留所有消息。有两种策略可以删除旧数据:
1) 基于时间: log.retention.hours=168
2) 基于大小: log.retention.bytes=1073741824
需要注意的是,因为 Kafka 读取特定消息的时间复杂度为 O(1),即与文件大小无关,所以这里删除过期文
件与提高 Kafka 性能无关。
③、Zookeeper 存储结构
上图中的备注,未看到灰色框中节点的原因是由于在消费消息时,offset 不是维护在 zookeeper 中,而是维
护在 kafka 中,使用的是新版本的消费消息语句,前面提到过
注意: producer 不在 zk 中注册, 消费者在 zk 中注册。 可以在 zookeeper 集群查看具体数据
cd /opt/module/zookeeper-3.4.10/
bin/zkCli.sh
ls /
ls /brokers
ls /brokers/ids
ls /brokers/topics
ls /consumers #查看消费者组 ID
ls /consumers/console-consumer-数字 #查看偏移量,这个数字是随机生成的
#consumer 在 zookeeper 中存放的就是偏移量,存放的是上一次消费到了哪里
....
3、Kafka 消费过程分析
kafka 提供了两套 consumer API: 高级 Consumer API 和低级 Consumer API。
①、高级 API
1) 高级 API 优点
■ 高级 API 写起来简单;
■ 不需要自行去管理 offset,系统通过 zookeeper 自行管理;
■ 不需要管理分区,副本等情况, .系统自动管理;
■ 消费者断线会自动根据上一次记录在 zookeeper 中的 offset 去接着获取数据(默认设置1 分钟更新一下
zookeeper 中存的 offset);
■ 可以使用 group 来区分对同一个 topic 的不同程序访问分离开来(不同的 group 记录不同的 offset,这样
不同程序读取同一个 topic 才不会因为 offset 互相影响)
2) 高级 API 缺点
■ 不能自行控制 offset(对于某些特殊需求来说)
■ 不能细化控制如分区、副本、 zk 等
②、低级 API
1) 低级 API 优点
■ 能够让开发者自己控制 offset,想从哪里读取就从哪里读取。
■ 自行控制连接分区,对分区自定义进行负载均衡
■ 对 zookeeper 的依赖性降低(如: offset 不一定非要靠 zk 存储,自行存储 offset 即可,比如存在文件或
者内存中)
2) 低级 API 缺点
■ 太过复杂,需要自行控制 offset,连接哪个分区,找到分区 leader 等。
③、消费者组
消费者是以 consumer group 消费者组的方式工作,由一个或者多个消费者组成一个组,共同消费一个
topic。每个分区在同一时间只能由 group 中的一个消费者读取,但是多个 group 可以同时消费这个
partition。在图中,有一个由三个消费者组成的 group,有一个消费者读取主题中的两个分区,另外两个分别
读取一个分区。某个消费者读取某个分区,也可以叫做某个消费者是某个分区的拥有者。
在这种情况下,消费者可以通过水平扩展的方式同时读取大量的消息。另外,如果一个消费者失败了,那么
其他的 group 成员会自动负载均衡读取之前失败的消费者读取的分区。
④、消费方式
consumer 采用 pull(拉) 模式从 broker 中读取数据。
push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。它的目标是尽可能
以最快速度传递消息,但是这样很容易造成 consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥
塞。而 pull 模式则可以根据 consumer 的消费能力以适当的速率消费消息 。
对于 Kafka 而言, pull 模式更合适,它可简化 broker 的设计, consumer 可自主控制消费消息的速率,同
时 consumer 可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现
不同的传输语义。pull 模式不足之处是,如果 kafka 没有数据,消费者可能会陷入循环中,一直等待数据到
达。为了避免这种情况,我们在我们的拉请求中有参数,允许消费者请求在等待数据到达的“长轮询”中进行阻塞
(并且可选地等待到给定的字节数,以确保大的传输大小)。
⑤、消费者组案例
1) 需求:测试同一个消费者组中的消费者, 同一时刻只能有一个消费者消费同一个分区的消息。
2) 案例实操
(1)在 stu-kafka01、 stu-kafka02上修改/opt/module/kafka/config/consumer.properties 配置文件中的
group.id 属性为任意组名。
[root@stu-kafka01 config]$ vi consumer.properties group.id=atguigu
(2)在 stu-kafka01、 stu-kafka02上分别启动消费者
[root@stu-kafka01 kafka]$ bin/kafka-console-consumer.sh --zookeeper stu-kafka01:2181 \
--topic first --consumer.config config/consumer.properties
[root@stu-kafka02 kafka]$ bin/kafka-console-consumer.sh --zookeeper stu-kafka01:2181 \
--topic first --consumer.config config/consumer.properties
(3)在 stu-kafka03上启动生产者
[atguigu@hadoop104 kafka]$ bin/kafka-console-producer.sh \
--broker-list hadoop102:9092 --topic first
>hello world
(4)查看 stu-kafka01 和 stu-kafka02 的接收者。
同一时刻只有一个消费者接收到消息。
四、Kafka API 实战
1、环境准备
在 kafka 中创建一个3个分区3个副本的 topic,取名为 first
①、启动 zk 和 kafka 集群,在 kafka 集群中打开一个消费者
cd /opt/module/kafka
[root@stu-kafka01 kafka]$ bin/kafka-console-consumer.sh \
--zookeeper stu-kafka01:2181 --topic first
②、导入 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>
2、Kafka 生产者 Java API
①、创建生产者(过时的 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 );
}
}
②、创建生产者(新 API)
■ 先启动 kafka 消费者,让其监听
bin/kafka-console-consumer.sh --zookeeper stu-kafka01:2181 --topic first
■ 然后执行下面代码,进行发送数据
package com.pengtxyl.kafka.producer;
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
public class CustomerProducer {
public static void main(String[] args) {
//配置信息, 配置信息中的key都保存在ProducerConfig类中,
//可以在ProducerConfig类查看所有的配置信息
Properties props = new Properties();
//kafka集群
props.put("bootstrap.servers", "192.168.100.100:9092");
//应答级别
//props.put("acks", "all");
props.put(ProducerConfig.ACKS_CONFIG, "all");
//重试次数, 当数据发送失败后重试的次数, 0表示不重试
props.put("retries", 0);
/*
* 批量大小和提交延时是指当producer(生产者)的数据大小超过16K或者
* 数据等待时间超过了指定的延时提交时间producer就会将数据发送
*/
//批量大小, 指每一次提交数据能缓存的大小
props.put("batch.size", 16384);
//提交延时
props.put("linger.ms", 1);
//缓存, 指producer总共能缓存数据的大小--32M
props.put("buffer.memory", 33554432);
//KV的序列化类
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
for(int i = 0 ; i < 10 ; i++) {
producer.send(new ProducerRecord<String, String>("first", String.valueOf(i)));
}
//关闭资源
producer.close();
}
}
③、创建生产者带回调函数(新 API)
package com.pengtxyl.kafka.producer;
import java.util.Properties;
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class CustomerProducer {
public static void main(String[] args) {
//配置信息, 配置信息中的key都保存在ProducerConfig类中,
//可以在ProducerConfig类查看所有的配置信息
Properties props = new Properties();
//kafka集群
props.put("bootstrap.servers", "192.168.100.100:9092");
//应答级别
//props.put("acks", "all");
props.put(ProducerConfig.ACKS_CONFIG, "all");
//重试次数, 当数据发送失败后重试的次数, 0表示不重试
props.put("retries", 0);
/*
* 批量大小和提交延时是指当producer(生产者)的数据大小超过16K或者
* 数据等待时间超过了指定的延时提交时间producer就会将数据发送
*/
//批量大小, 指每一次提交数据能缓存的大小
props.put("batch.size", 16384);
//提交延时
props.put("linger.ms", 1);
//缓存, 指producer总共能缓存数据的大小--32M
props.put("buffer.memory", 33554432);
//KV的序列化类
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
for(int i = 10 ; i < 20 ; i++) {
producer.send(new ProducerRecord<String, String>("first", String.valueOf(i)), new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if(metadata != null) {
System.out.println(metadata.partition() + "---" + metadata.offset());
}
}
});
}
//关闭资源
producer.close();
}
}
④、自定义分区生产者,将生产者生产的数据指定存放到哪个分区
■ 需求:将所有数据存储到 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.pengtxyl.kafka.producer;
import java.util.Map;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
public class CustomerPartitioner implements Partitioner {
@Override
public void configure(Map<String, ?> configs) {
//configs 保存了一些元数据信息, 可以使用一个 Map 容器接收此类型数据, 然后
//在其他地方读取数据
}
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//指定存放到哪个分区
return 0;
}
@Override
public void close() {
}
}
在代码中调用:将上面定义的自定义分区类传到配置信息中
package com.pengtxyl.kafka.producer;
import java.util.Properties;
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class CustomerProducer {
public static void main(String[] args) {
//配置信息, 配置信息中的key都保存在ProducerConfig类中,
//可以在ProducerConfig类查看所有的配置信息
Properties props = new Properties();
//kafka集群
props.put("bootstrap.servers", "192.168.100.100:9092");
//应答级别
//props.put("acks", "all");
props.put(ProducerConfig.ACKS_CONFIG, "all");
//重试次数, 当数据发送失败后重试的次数, 0表示不重试
props.put("retries", 0);
/*
* 批量大小和提交延时是指当producer(生产者)的数据大小超过16K或者
* 数据等待时间超过了指定的延时提交时间producer就会将数据发送
*/
//批量大小, 指每一次提交数据能缓存的大小
props.put("batch.size", 16384);
//提交延时
props.put("linger.ms", 1);
//缓存, 指producer总共能缓存数据的大小--32M
props.put("buffer.memory", 33554432);
//KV的序列化类
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//指定分区的类
props.put("partitioner.class", "com.pengtxyl.kafka.producer.CustomerPartitioner");
//创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
for(int i = 20 ; i < 30 ; i++) {
producer.send(new ProducerRecord<String, String>("first", String.valueOf(i)), new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if(metadata != null) {
System.out.println(metadata.partition() + "---" + metadata.offset());
}
}
});
}
//关闭资源
producer.close();
}
}
3)测试:
■ 在 stu-kafka02 上监控/opt/module/kafka/logs/目录下 first 主题 3 个分区的 log 日志动态变化情况
[root@stu-kafka02 first-0]$ tail -f 00000000000000000000.log
[root@stu-kafka02 first-1]$ tail -f 00000000000000000000.log
[root@stu-kafka02 first-2]$ tail -f 00000000000000000000.log
■ 发现数据都存储到指定的分区 0 了。
3、Kafka 消费者 Java API
①、高级 API
1)在控制台创建发送者
[root@stu-kafka kafka]$ bin/kafka-console-producer.sh --broker-list \
hadoop102:9092 --topic first
>hello world
2)创建消费者(过时 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()));
}
}
}
3)官方提供案例(自动维护消费情况)(新 API)
package com.pengtxyl.kafka.consumer;
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 CustomerConsumer {
public static void main(String[] args) {
//配置信息
Properties props = new Properties();
//kafka集群
props.put("bootstrap.servers", "192.168.100.100:9092");
//消费者组ID
props.put("group.id", "test");
//为了能重复消费, 设置下面的参数, earliest表示将offset设置到最早的数据, 注意: 设置为最早的
//数据并不是设置为0,这是由于有可能数据超过了保存时间等一系列因素导致数据丢失,所以最早的 offset
//可能并不是为0
props.put(ComsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
//设置自定义提交offset
props.put("enable.auto.commit", "true");
//提交延时,这里可能会出现一个问题,就是当消费方消费数据后,执行完业务操作后,
//还没来得及提交就出现服务宕机,此时可能会出现重复消费的情况
props.put("auto.commit.interval.ms", "1000");
//KV的反序列化类
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
//创建消费者对象
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
//指定Topic, 可以指定多个, 即使有的Topic不存在也可以指定,当然会报错,但是不影响执行
consumer.subscribe(Arrays.asList("second", "first", "abc"));
//也可以使用下面指定topic、partition以及offset
//consumer.seek(new TopicPartition("second", 0), 2)
while(true) {
//获取数据
ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
for(ConsumerRecord<String, String> record : consumerRecords) {
System.out.println(record.topic() + "---"
+ record.partition() + "---"
+ record.value());
}
}
}
}
②、低级 API
实现使用低级 API 读取指定 topic,指定 partition,指定 offset 的数据。
1)消费者使用低级 API 的主要步骤:
步骤 | 主要工作 |
---|---|
1 | 根据指定的分区从主题元数据中找到主副本 |
2 | 获取分区最新的消费进度 |
3 | 从主副本拉取分区的消息 |
4 | 识别主副本的变化,重试 |
2)方法描述
方法 | 描述 |
---|---|
findLeader() | 客户端向种子节点发送主题元数据,将副本集加入备用节点 |
getLastOffset() | 消费者客户端发送偏移量请求,获取分区最近的偏移量 |
run() | 消费者低级 AP I 拉取消息的主要方法 |
findNewLeader() | 当分区的主副本节点发生故障,客户将要找出新的主副本 |
3)代码
简单版:
package com.pengtxyl.kafka.consumer;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import kafka.api.FetchRequest;
import kafka.api.FetchRequestBuilder;
import kafka.cluster.BrokerEndPoint;
import kafka.javaapi.FetchResponse;
import kafka.javaapi.PartitionMetadata;
import kafka.javaapi.TopicMetadata;
import kafka.javaapi.TopicMetadataRequest;
import kafka.javaapi.TopicMetadataResponse;
import kafka.javaapi.consumer.SimpleConsumer;
import kafka.javaapi.message.ByteBufferMessageSet;
import kafka.message.MessageAndOffset;
/*
* 根据指定的Topic,Partition,offset来获取数据
*/
public class LowerConsumer {
public static void main(String[] args) {
//定义相关参数
//kafka集群
List<String> brokers = new ArrayList<>();
brokers.add("stu-kafka01");
// brokers.add("stu-kafka02");
// brokers.add("stu-kafka03");
//端口号
int port = 9092;
//主题
String topic = "second";
//分区
int partition = 0;
//offset
long offset = 2;
LowerConsumer lowerConsumer = new LowerConsumer();
lowerConsumer.getData(brokers, port, topic, partition, offset);
}
//找分区leader
@SuppressWarnings({ "deprecation", "unused" })
private BrokerEndPoint findLeader(List<String> brokers, int port, String topic, int partition) {
for(String broker : brokers) {
//创建获取分区leader的消费者对象
SimpleConsumer getLeader = new SimpleConsumer(broker, port, 1000, 1024*4, "getLeader");
//创建一个主题元数据信息请求
//TopicMetadataRequest request = new TopicMetadataRequest(Arrays.asList(topic));
TopicMetadataRequest request = new TopicMetadataRequest(Collections.singletonList(topic));
//获取主题元数据返回值
TopicMetadataResponse response = getLeader.send(request);
//解析元数据返回值
List<TopicMetadata> topicsMetadata = response.topicsMetadata();
//遍历主题元数据
for(TopicMetadata topicMetadata : topicsMetadata) {
//获取多个分区的元数据信息
List<PartitionMetadata> partitionsMetadata = topicMetadata.partitionsMetadata();
for(PartitionMetadata partitionMetadata : partitionsMetadata) {
if(partitionMetadata.partitionId() == partition) {
return partitionMetadata.leader();
}
}
}
}
return null;
}
//获取数据
@SuppressWarnings("deprecation")
private void getData(List<String> brokers, int port, String topic, int partition, long offset) {
BrokerEndPoint leader = findLeader(brokers, port, topic, partition);
if(leader != null) {
//获取数据的消费者对象
SimpleConsumer consumer = new SimpleConsumer(leader.host(), port, 1000, 1024*4, "getData");
//创建获取数据的对象, 可以添加多个 addFetch, 100 表示的是字节数, 并不是多少条数
FetchRequest request = new FetchRequestBuilder().addFetch(topic, partition, offset, 100).build();
//获取数据返回值
FetchResponse fetchResponse = consumer.fetch(request);
//解析返回值
ByteBufferMessageSet messageAndOffsets = fetchResponse.messageSet(topic, partition);
//遍历并打印
for(MessageAndOffset messageAndOffset : messageAndOffsets) {
long offset1 = messageAndOffset.offset();
ByteBuffer payload = messageAndOffset.message().payload();
byte[] bytes = new byte[payload.limit()];
payload.get(bytes);
System.out.println(offset1 + "---" + new String(bytes));
}
} else {
return ;
}
}
}
复杂版:
package com.pengtxyl.kafka.consumer;
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.100.100");
seeds.add("192.168.100.101");
seeds.add("192.168.100.102");
// 端口
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) {
ie.printStackTrace();
}
}
}
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;
}
}
五、Kafka producer拦截器(interceptor)
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
可能抛出的异常记录到错误日志中而非在向上传递。这在使用过程中要特别留意。
2、拦截器案例
1)需求:
实现一个简单的双 interceptor 组成的拦截链。第一个 interceptor 会在消息发送前将时间戳信息加到消息
value 的最前部;第二个 interceptor 会在消息发送后更新成功发送消息数或失败发送消息数。
2)案例实操
■ 增加时间戳拦截器
package com.pengtxyl.kafka.intercetor;
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 TimeIntercetor implements ProducerInterceptor<String, String> {
@Override
public void configure(Map<String, ?> configs) {
}
@Override
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
return new ProducerRecord<String, String>(record.topic(), record.key(),
System.currentTimeMillis() + ", " + record.value());
}
@Override
public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
}
@Override
public void close() {
}
}
■ 统计发送消息成功和发送失败消息数,并在 producer 关闭时打印这两个计数器
package com.pengtxyl.kafka.intercetor;
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 CountIntercetor implements ProducerInterceptor<String, String>{
private int successCount = 0;
private int errorCount = 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(null == exception) {
successCount++;
} else {
errorCount++;
}
}
@Override
public void close() {
System.out.println("发送成功" + successCount + "条数据");
System.out.println("发送失败" + errorCount + "条数据");
}
}
■ producer 主程序
package com.pengtxyl.kafka.producer;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public class CustomerProducer {
public static void main(String[] args) {
//配置信息, 配置信息中的key都保存在ProducerConfig类中,
//可以在ProducerConfig类查看所有的配置信息
Properties props = new Properties();
//kafka集群
props.put("bootstrap.servers", "192.168.100.100:9092");
//应答级别
//props.put("acks", "all");
props.put(ProducerConfig.ACKS_CONFIG, "all");
//重试次数, 当数据发送失败后重试的次数, 0表示不重试
props.put("retries", 0);
/*
* 批量大小和提交延时是指当producer(生产者)的数据大小超过16K或者
* 数据等待时间超过了指定的延时提交时间producer就会将数据发送
*/
//批量大小, 指每一次提交数据能缓存的大小
props.put("batch.size", 16384);
//提交延时
props.put("linger.ms", 1);
//缓存, 指producer总共能缓存数据的大小--32M
props.put("buffer.memory", 33554432);
//KV的序列化类
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//指定分区的类
props.put("partitioner.class", "com.pengtxyl.kafka.producer.CustomerPartitioner");
//构建拦截器链
List<String> list = new ArrayList<>();
list.add("");
list.add("");
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, list);
//创建生产者对象
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
for(int i = 20 ; i < 30 ; i++) {
producer.send(new ProducerRecord<String, String>("first", String.valueOf(i)), new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if(metadata != null) {
System.out.println(metadata.partition() + "---" + metadata.offset());
}
}
});
}
//关闭资源
producer.close();
}
}
■ 测试
□ 在 kafka 上启动消费者, 然后运行客户端 java 程序
[root@stu-kafka01 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
□ 观察 java 平台控制台输出数据如下:
发送成功10条数据
发送失败0条数据
六、Kafka Streams
1、概述
①、Kafka Streams
Kafka Streams。 Apache Kafka 开源项目的一个组成部分。是一个功能强大,易于使用的库。用于在 Kafka 上
构建高可分布式、拓展性,容错的应用程序。
②、Kafka Streams 特点
1)功能强大
高扩展性,弹性,容错
2)轻量级
无需专门的集群
一个库,而不是框架
3)完全集成
100%的 Kafka 0.10.0 版本兼容
易于集成到现有的应用程序
4)实时性
毫秒级延迟
并非微批处理
窗口允许乱序数据
允许迟到数据
③、为什么要有 Kafka Stream
当前已经有非常多的流式处理系统,最知名且应用最多的开源流式处理系统有 SparkStreaming 和
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 作为流式处理类库,直接提供具体的类给开发者调用,
整个应用的运行方式主要由开发者控制,方便使用和调试。
第二,虽然 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 可以在线动态调整并行度。
2、Kafka Stream数据清洗案例
①、需求:
实时处理单词带有”>>>”前缀的内容。 例如输入”atguigu>>>ximenqing”, 最终处理成“ximenqing”
②、需求分析:
③、案例实操
■ 创建一个工程, 并添加 jar 包
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
<version>0.11.0.2</version>
</dependency>
■ 创建主类
package com.pengtxyl.kafka.stream;
import java.util.Properties;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.processor.Processor;
import org.apache.kafka.streams.processor.ProcessorSupplier;
import org.apache.kafka.streams.processor.TopologyBuilder;
public class KafkaStream {
public static void main(String[] args) {
//创建拓扑对象
TopologyBuilder builder = new TopologyBuilder();
//创建配置文件
Properties properties = new Properties();
properties.put("bootstrap.servers", "192.168.100.100:9092");
properties.put("application.id", "kafkaStream");
//构建拓扑结构
builder.addSource("SOURCE", "first")
.addProcessor("PROCESSOR", new ProcessorSupplier() {
@Override
public Processor get() {
return new LogProcessor();
}
}, "SOURCE")
.addSink("SINK", "second", "PROCESSOR");
KafkaStreams kafkaStreams = new KafkaStreams(builder, properties);
kafkaStreams.start();
}
}
■ 具体业务处理
package com.pengtxyl.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 processorContext) {
context = processorContext;
}
@Override
public void process(byte[] key, byte[] value) {
//获取一行数据
String line = new String(value);
//去除">>>"
line = line.replaceAll(">>>", "");
value = line.getBytes();
context.forward(key, value);
}
@Override
public void punctuate(long timestamp) {
}
@Override
public void close() {
}
}
■ 运行程序
■ 在 hadoop104 上启动生产者
[root@stu-kafka01 kafka]$ bin/kafka-console-producer.sh \
--broker-list hadoop102:9092 --topic first
>hello>>>world
>h>>>atguigu
>hahaha
■ 在 hadoop103 上启动消费者
[root@stu-kafka01 kafka]$ bin/kafka-console-consumer.sh \
--zookeeper hadoop102:2181 --from-beginning --topic second
world
atguigu
hahaha