kafka入门到精通
一、为什么使用消息队列
1、使用同步的通信方式来解决多个服务之间的通信
同步的通信方式会存在性能和稳定的问题。
2、使用异步的通信方式
针对于同步的通信方式来说,异步的方式,可以让上游快速成功,极大提高了系统的吞吐量。而且在分布式系统中,通过下游多个服务的分布式事务的保障,也能保障业务执行之后的最终一致性。
消息队列解决具体的是什么问题 – 通信问题。
二、消息队列的流派
目前消息队列的中间件选项有很多种:
- rabbitMQ:内部的可玩性(功能性)是非常强的
- rocketMQ:阿里内部一个大神,根据kafka的内部执行原理,手写的一个消息队列中间件。性能是与kafka相比肩,除此之外,在功能上封装了更多的功能。
- kafka:全球消息处理性能最快的一款MQ
- zeroMQ
这些消息中间件有什么区别?
什么是 MQ
Message Queue(MQ),消息队列中间件。很多人都说:MQ 通过将消息的发送和接收分离来实现应用程序的异步和解偶,这个给人的直觉是——MQ 是异步的,用来解耦的,但是这个只是 MQ 的效果而不是目的。MQ 真正的目的是为了通讯,屏蔽底层复杂的通讯协议,定义了一套应用层的、更加简单的通讯协议。一个分布式系统中两个模块之间通讯要么是HTTP,要么是自己开发的 (rpc) TCP,但是这两种协议其实都是原始的协议。HTTP 协议很难实现两端通讯——模块 A 可以调用 B,B 也可以主动调用 A,如果要做到这个两端都要背上WebServer,而且还不支持⻓连接( HTTP 2.0 的库根本找不到)。TCP 就更加原始了,粘包、心跳、私有的协议,想一想头皮就发麻。MQ 所要做的就是在这些协议之上构建一个简单的“协议”——生产者/消费者模型。MQ 带给我的“协议”不是具体的通讯协议,而是更高层次通讯模型。它定义了两个对象——发送数据的叫生产者;接收数据的叫消费者, 提供一个SDK 让我们可以定义自己的生产者和消费者实现消息通讯而无视底层通讯协议
1、有 Broker 的 MQ
这个流派通常有一台服务器作为 Broker,所有的消息都通过它中转。生产者把消息发送给它就结束自己的任务了,Broker 则把消息主动推送给消费者(或者消费者主动轮询)
重 Topic
Topic(主题)
整个broker,依据topic来进行消息的中转。在重topic的消息队列里必然需要topic的存在
重topic:Kafka、RocketMQ、ActiveMQ
kafka、JMS(ActiveMQ)就属于这个流派,生产者会发送 key 和数据到 Broker,由 Broker比较 key 之后决定给哪个消费者。这种模式是我们最常⻅的模式,是我们对 MQ 最多的印象。在这种模式下一个 topic 往往是一个比较大的概念,甚至一个系统中就可能只有一个topic,topic 某种意义上就是 queue,生产者发送 key 相当于说:“hi,把数据放到 key 的队列中”
如上图所示,Broker 定义了三个队列,key1,key2,key3,生产者发送数据的时候会发送key1 和 data,Broker 在推送数据的时候则推送 data(也可能把 key 带上)。
虽然架构一样但是 kafka 的性能要比 jms 的性能不知道高到多少倍,所以基本这种类型的MQ 只有 kafka 一种备选方案。如果你需要一条暴力的数据流(在乎性能而非灵活性)那么kafka 是最好的选择
轻 Topic
topic只是一种中转模式。
这种的代表是 RabbitMQ(或者说是 AMQP)。生产者发送 key 和数据,消费者定义订阅的队列,Broker 收到数据之后会通过一定的逻辑计算出 key 对应的队列,然后把数据交给队列
这种模式下解耦了 key 和 queue,在这种架构中 queue 是非常轻量级的(在 RabbitMQ 中它的上限取决于你的内存),消费者关心的只是自己的 queue;生产者不必关心数据最终给谁只要指定 key 就行了,中间的那层映射在 AMQP 中叫 exchange(交换机)。
AMQP 中有四种 exchange
- Direct exchange:key 就等于 queue
- Fanout exchange:无视 key,给所有的 queue 都来一份
- Topic exchange:key 可以用“宽字符”模糊匹配 queue
- Headers exchange:无视 key,通过查看消息的头部元数据来决定发给那个
queue(AMQP 头部元数据非常丰富而且可以自定义)
这种结构的架构给通讯带来了很大的灵活性,我们能想到的通讯方式都可以用这四种exchange 表达出来。如果你需要一个企业数据总线(在乎灵活性)那么 RabbitMQ 绝对的值得一用
2、无 Broker 的 MQ
在生产者和消费者之间没有使用broker,例如 zeroMQ,直接使用socket进行通信。
无 Broker 的 MQ 的代表是 ZeroMQ。该作者非常睿智,他非常敏锐的意识到——MQ 是更高级的 Socket,它是解决通讯问题的。所以 ZeroMQ 被设计成了一个 “库” 而不是一个中间件,这种实现也可以达到——没有 Broker 的目的
节点之间通讯的消息都是发送到彼此的队列中,每个节点都既是生产者又是消费者。ZeroMQ做的事情就是封装出一套类似于 Socket 的 API 可以完成发送数据,读取数据
ZeroMQ 其实就是一个跨语言的、重量级的 Actor 模型邮箱库。你可以把自己的程序想象成一个 Actor,ZeroMQ 就是提供邮箱功能的库;ZeroMQ 可以实现同一台机器的 RPC 通讯也可以实现不同机器的 TCP、UDP 通讯,如果你需要一个强大的、灵活、野蛮的通讯能力,别犹豫 ZeroMQ
三、Kafka的基本知识
Kafka介绍
Kafka是最初由Linkedin公司开发,是一个分布式、支持分区的(partition)、多副本的 (replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理 大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、 Storm/Spark流式处理引擎,web/nginx日志、访问日志,消息服务等等,用scala语言编 写,Linkedin于 2010 年贡献给了Apache基金会并成为顶级开源 项目。
Kafka的使用场景
- 日志收集:一个公司可以用Kafka收集各种服务的log,通过kafka以统一接口服务的方式 开放给各种consumer,例如hadoop、Hbase、Solr等。
- 消息系统:解耦和生产者和消费者、缓存消息等。
- 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网⻚、 搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过 订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖 掘。
- 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产 各种操作的集中反馈,比如报警和报告。
Kafka基本概念
kafka是一个分布式的,分区的消息(官方称之为commit log)服务。它提供一个消息系统应该 具备的功能,但是确有着独特的设计。可以这样来说,Kafka借鉴了JMS规范的思想,但是确 并 没有完全遵循JMS规范。
首先,让我们来看一下基础的消息(Message)相关术语:
名称 | 解释 |
---|---|
Broker | 消息中间件处理节点,⼀个Kafka节点就是⼀个broker,⼀个或者多个Broker可以组成⼀个Kafka集群 |
Topic | Kafka根据topic对消息进⾏归类,发布到Kafka集群的每条消息都需要指定⼀个topic |
Producer | 消息⽣产者,向Broker发送消息的客户端 |
Consumer | 消息消费者,从Broker读取消息的客户端 |
ConsumerGroup | 每个Consumer属于⼀个特定的Consumer Group,⼀条消息可以被多个不同的Consumer Group消费,但是⼀个Consumer Group中只能有⼀个Consumer能够消费该消息 |
Partition | 物理上的概念,⼀个topic可以分为多个partition,每个partition内部消息是有序的 |
因此,从一个较高的层面上来看,producer通过网络发送消息到Kafka集群,然后consumer 来进行消费,如下图
服务端(brokers)和客户端(producer、consumer)之间通信通过 TCP协议 来完成。
1、Kafka的安装
Ubuntu系统下安装
su root
【切换成root操作】
部署一台zookeeper服务器
下载zookeeper,官网下载地址:http://mirror.bit.edu.cn/apache/zookeeper/
cd /usr/local
mkdir zookeeper
cd zookeeper
chmod 777 /usr/local/zookeeper
上传压缩包
tar -zxvf apache-zookeeper-3.7.1-bin.tar.gz
【解压缩包】
cd /usr/local/zookeeper/apache-zookeeper-3.7.1-bin/conf
cp zoo_sample.cfg zoo.cfg
【复制一份取名为zoo.cfg】
切换到bin目录下,执行启动zookeeper命令:
./zkServer.sh start
安装jdk
mkdir /jdk
【创建安装jdk目录】
cd /jdk
chmod 777 /jdk
上传jdk压缩包
tar xf jdk-8u161-linux-x64.tar.gz
【解压】
sudo vi /etc/profile
【添加配置文件】,加入一下配置
export JAVA_HOME=/jdk/jdk1.8.0_161
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH=${JAVA_HOME}/bin:$PATH
source /etc/profile
【使配置生效】
java -version
【测试】
下载kafka的安装包
官网地址:http://kafka.apache.org/downloads
上传到服务器上
路径:/usr/local/kafka
解压缩压缩包
tar -zxvf kafka_2.11-2.4.0.tgz
进入到config目录内,修改server.properties
cd /usr/local/kafka/kafka_2.11-2.4.0/config
#broker.id属性在kafka集群中必须要是唯一
broker.id= 0
#kafka部署的机器ip和提供服务的端口号
listeners=PLAINTEXT://192.168.37.128:9092
#kafka的消息存储文件
log.dir=/usr/local/data/kafka-logs
#kafka连接zookeeper的地址
zookeeper.connect=192.168.37.128:2181
vim server.properties
进入到bin目录,执行一下命令来启动kafka服务器(带有配置文件)
./kafka-server-start.sh -daemon ../config/server.properties
-daemon
:守护后台运行
ps -aux | grep server.properties
效验kafka是否启动成功
进入到zk内查看是否有kafka的节点:/brokers/ids/0
./zkCli.sh -server 192.168.37.128:2181
ls /
ls /brokers
ls /brokers/ids
2、Kafka中的一些基本概念
kafka中没有这么复杂的概念
server.properties核心配置详解:
Property | Default | Description |
---|---|---|
broker.id | 0 | 每个broker都可以⽤⼀个唯⼀的⾮负整数id进⾏标识;这个id可以作为broker的“名字”,你可以选择任意你喜欢的数字作为id,只要id是唯⼀的即可。 |
log.dirs | /tmp/kafka-logs | kafka存放数据的路径。这个路径并不是唯⼀的,可以是多个,路径之间只需要使⽤逗号分隔即可;每当创建新partition时,都会选择在包含最少partitions的路径下进⾏。 |
listeners | PLAINTEXT://192.168.37.128:9092 | server接受客户端连接的端⼝,ip配置kafka本机ip即可 |
zookeeper.connect | localhost:2181 | zooKeeper连接字符串的格式为:hostname:port,此处hostname和port分别是ZooKeeper集群中某个节点的host和port;zookeeper如果是集群,连接⽅式为hostname1:port1, hostname2:port2,hostname3:port3 |
log.retention.hours | 168 | 每个⽇志⽂件删除之前保存的时间。默认数据保存时间对所有topic都⼀样。 |
num.partitions | 1 | 创建topic的默认分区数 |
default.replication.factor | 1 | ⾃动创建topic的默认副本数量,建议设置为⼤于等于2 |
min.insync.replicas | 1 | 当producer设置acks为-1时,min.insync.replicas指定replicas的最⼩数⽬(必须确认每⼀个repica的写数据都是成功的),如果这个数⽬没有达到,producer发送消息会产⽣异常 |
delete.topic.enable | false | 是否允许删除主题 |
3、创建topic
topic是什么概念?topic可以实现消息的分类,不同消费者订阅不同的topic。
执行以下命令创建名为“test”的topic,这个topic只有一个partition,并且备份因子也设置为1
通过kafka命令向zk中创建一个主题
./kafka-topics.sh --create --zookeeper 192.168.37.128:2181 --replication-factor 1 --partitions 1 --topic test
查看当前kafka内有哪些topic(所有的主题)
./kafka-topics.sh --list --zookeeper 192.168.37.128:2181
4、发送消息
把消息发送给broker中的某个topic,打开一个kafka发送消息的客户端,然后开始用客户端向kafka服务器发送消息
kafka自带了一个producer命令客户端,可以从本地文件中读取内容,或者我们也可以以命令行中直接输入内容,并将这些内容以消息的形式发送到kafka集群中。在默认情况下,每一个行会被当做成一个独立的消息。使用kafka的发送消息的客户端,指定发送到的kafka服务器地址和topic
./kafka-console-producer.sh --broker-list 192.168.37.128:9092 --topic test
5、消费消息
打开一个消费消息的客户端,向kafka服务器的某个主题消费消息
对于consumer,kafka同样也携带了一个命令行客户端,会将获取到内容在命令中进行输出, 默认是消费最新的消息 。使用kafka的消费者消息的客户端,从指定kafka服务器的指定 topic中消费消息
-
方式一:从当前主题中的最后一条消息的offset(偏移量位置) +1 开始消费
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092 --topic test
-
方式二:从当前主题中的第一条消息开始消费
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092 --from-beginning --topic test
几个注意点:
- 消息会被存储
- 消息是顺序存储
- 消息是有偏移量的
- 消费时可以指明偏移量进行消费
6、关于消息的细节
-
生产者将消息发送给
broker
,broker
会将消息保存在本地的日记文件中。/usr/local/data/kafka-logs/主题-分区/00000000000000000000.log
-
消息的保存是有序的,通过
offset
偏移量来描述消息的有序性 -
消费者消费消息也是通过
offset
来描述当前要消费的那条消息的位置
7、单播消息
在一个kafka的topic中,启动两个消费者,一个生产者,问:生产者发送消息,这条消息是否同时会被两个消费者消费?
如果多个消费者在同一个消费组,那么只有一个消费者可以收到订阅的topic中的消息,换言之,同一个消费组中只能有一个消费者收到一个topic中的消息。
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092 --consumer-property group.id=testGroup --topic test
group.id=testGroup
:设置组
生产者发送消息;
./kafka-console-producer.sh --broker-list 192.168.37.128:9092 --topic test
消费者1:
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092 --consumer-property group.id=testGroup --topic test
生产者发送消息,消费者1接收:
消费者2:
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092 --consumer-property group.id=testGroup --topic test
生产者发送消息,消费者1 和 消费者2 的情况
生产者
消费者1
消费者2
8、多播消息
停止消费者2:【更换消费组名】, 再启动
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092 --consumer-property group.id=testGroup1 --topic test
生产者发送消息:
消费者1
消费者2
不同的消费组订阅同一个topic,那么不同的消费组中只有一个消费者收到消息。实际上也是多个消费组中的多个消费者收到同一个消息。
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092 --consumer-property group.id=testGroup1 --topic test
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092 --consumer-property group.id=testGroup2 --topic test
下图就是描述多播和单播消息的区别:
9、查看消息组的详细消息
查看当前主题下有哪些消费组:
./kafka-consumer-groups.sh --bootstrap-server 192.168.37.128:9092 --list
查询消费组中的具体信息:比如当前偏移量,最后一条消息偏移量、堆积的消息数量
./kafka-consumer-groups.sh --bootstrap-server 192.168.37.128:9092 --describe --group testGroup
停止消费1 和 消费2
向生产者发送消息
在查询消费组中的具体信息:
重点关注以下信息:
- Currennt-offset: 最后被消费消息的偏移量
- Log-end-offset: 消息总量(最后一条消息的偏移量)
- Lag: 积压了多少条消息
四、Kafka中的主题和分区的概念
1、主题Topic
主题 - topic 在kafka中是一个逻辑的概念,kafka通过topic将消息进行分类。不同的topic会被订阅该topic的消费者消费。
但是有一个问题,如果说这个topic中的消息非常非常多,多到需要几个T来存储,因为消息是会被保存到log日记文件中的。为了解决这个文件过大的问题,kafka提出了Partition分区的概念。
2、分区Partition
1)分区概念
通过 partition
将一个topic
中的消息分区来存储。这样的好处有多个:
- 分区存储,可以减少统一存储文件过大的问题
- 提高了读写的吞吐量:读和写可以同时在多个分区中进行
一个主题中的消息量是非常大的,因此可以通过分区的设置,来分布式存储这些消息。比如一个topic创建了 3 个分区。那么topic中的消息就会分别存放在这三个分区中。
2)创建多分区的主题
为一个主题创建多个分区
./kafka-topics.sh --create --zookeeper 192.168.37.128:2181 --replication-factor 1 --partitions 2 --topic test1
可以通过这样的命令查看topic的分区信息
./kafka-topics.sh --describe --zookeeper 192.168.37.128:2181 --topic test1
分区的作用:
- 可以分布式存储
- 可以并行写
实际上是存在data/kafka-logs/test-0
和 test-1中的0000000.log
文件中
小细节:
- 定期将自己消费分区的offset提交给kafka内部topic:__consumer_offsets,提交过去的 时候,key是consumerGroupId+topic+分区号,value就是当前offset的值,kafka会定 期清理topic里的消息,最后就保留最新的那条数据
因为 __consumer_offsets可能会接收高并发的请求,kafka默认给其分配 50 个分区(可以 通过 offsets.topic.num.partitions设置),这样可以通过加机器的方式抗大并发。
通过如下公式可以选出 consumer消费的 offset要提交到 __consumer_offsets的哪个分区
公式:hash(consumerGroupId) % __consumer_offsets主题的分区数
3、kafka中消息日记文件中保存的内容
-
000000.log
:这个文件中保存的就是消息
-__consumer_offsets-49
:kafka内部自己创建了__consumer_offsets
主题包含了 50个分区。这个主题用来存放消费者消费某个消息某个主题的偏移量。因为每个消费者都会自己维护着消费的主题的偏移量,也就是说每个消费者会把消费者的主题的偏移量自主上报给kafka中的默认主题:consumer_offset
。因此kafka为了提升这个主题的并发性,默认设置了50个分区。- 至于提交到哪个分区:通过hash函数:
hash(consumerGroupId) % __consumer_offsets主题的分区数
- 提交到该主题的内容是:key是
consumerGroupld + topic + 分区号
,value就是当前offset
的值
- 至于提交到哪个分区:通过hash函数:
-
文件中保存的消息,默认是保存7天。7天到后消息会把删除。
五、Kafka集群操作
1、搭建kafka集群(三个broker)
准备 3 个server.properties文件
每个文件中的这些内容要调整
server.properties
broker.id=0
listeners=PLAINTEXT://192.168.32.128:9092
log.dir=/usr/local/kafka/data/kafka-logs
server1.properties
broker.id=1
listeners=PLAINTEXT://192.168.32.128:9093
log.dir=/usr/local/kafka/data/kafka-logs-1
server2.properties
broker.id=2
listeners=PLAINTEXT://192.168.32.128:9094
log.dir=/usr/local/kafka/data/kafka-logs-2
cp server.properties server1.properties
cp server.properties server2.properties
然后分别修改配置信息
使用如下命令来启动 3 台服务器
./kafka-server-start.sh -daemon ../config/server.properties
./kafka-server-start.sh -daemon ../config/server1.properties
./kafka-server-start.sh -daemon ../config/server2.properties
效验是否启动成功
搭建完后通过查看zk中的/brokers/ids
看是否启动成功
cd /usr/local/zookeeper/apache-zookeeper-3.7.1-bin/bin
./zkCli.sh
2、副本的概念
在创建主题时,除了指明了主题的分区数以外,还指明了副本数,那么副本是一个什么概念呢?
副本是为主题中的分区创建多个备份,多个副本在kafka集群的多个broker
中,会有一个副本作为leader
,其他是follower
。
副本是对分区的备份。在集群中,不同的副本会被部署在不同的broker上。下面例子:创建 1个主题, 2 个分区、 3 个副本。
./kafka-topics.sh --create --zookeeper 192.168.37.128:2181 --replication-factor 3 --partitions 2 --topic my-replicated-topic
查看通topic情况
./kafka-topics.sh --describe --zookeeper 192.168.37.128:2181 --topic my-replicated-topic
通过查看主题信息,其中的关键数据:
- replicas:当前副本存在的
broker
节点 - leader:副本里的概念(kafka的写和读的操作,都发生在
leader
负责把数据同步给follower
。当leader
挂了,经过主从选举,从多个follower
中选举产生一个新的leader
)
每个partition
都有一个broker
作为leader
。
消息发送方要把消息发给哪个broker
?就看副本的leader
是在哪个broker
上面。副本里的leader
专门用来接收消息。
接收到消息,其他follower
通过poll
的方式来同步数据。 - follower:(接收
leader
的同步数据)leader
处理所有针对这个partition
的读写请求,而follower
被动复制leader
,不提供读写(主要是为了保证多副本数据与消费的一致性),如果leader
所在的broker
挂掉,那么就会进行新leader
的选举,至于怎么选,在之后的controller
的概念中介绍。
通过kill掉leader后再查看主题情况:
kill掉leader:ps -aux | grep server.properties
查看topic情况: ./kafka-topics.sh --describe --zookeeper 192.168.37.128:2181 --topic my-replicated-topic
- isr: 可以同步和已同步的节点会被存入到
isr
集合中。这里有一个细节:如果isr
中的节点性能较差,会被踢出isr
集合。
此时,broker
、主题(topic
)、分区(partition
)、副本 这些概念就全部展现了;
集群中有多个broker
,创建主题时可以指明主题有多少个分区(把消息拆分到不同的分区中存储),可以为分区创建多个副本,不同的副本存放在不同的broker
里。
3、关于集群操作
1)向集群发送消息
./kafka-console-producer.sh --broker-list 192.168.37.128:9092,192.168.37.128:9093,192.168.37.128:9094 --topic my-replicated-topic
2)从集群中消费消息
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092,192.168.37.128:9093,192.168.37.128:9094 --from-beginning --topic my-replicated-topic
3)指定消费组来消费消息
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092,192.168.37.128:9093,192.168.37.128:9094 --from-beginning --consumer-property group.id=testGroup1 --topic my-replicated-topic
./kafka-console-consumer.sh --bootstrap-server 192.168.37.128:9092,192.168.37.128:9093,192.168.37.128:9094 --from-beginning --consumer-property group.id=testGroup2 --topic my-replicated-topic
4)分区消费组的集群中细节
图中Kafka集群有两个broker,每个broker中有多个partition。一个partition只能被一个消费组里的某一个消费者消费,从而保证消费顺序。Kafka只在partition的范围内保证消息消费的局部顺序性,不能在同一个topic中的多个partition中保证总的消费顺序性。一个消费者可以消费多个partition。
消费组中消费者的数量不能比一个topic中的partition数量多,否则多出来的消费者消费不到消息。
- 一个
partition
只能被一个消费者消费,目的是为了保证消费的顺序性,但是多个partition
的多个消费者的总的顺序是得不到保证的,那怎么做到消费的总顺序性呢? partition
的数量决定了消费组中消费的数量,建议同一个消费组总消费者的数量不要超过partition
的数量,否则多的消费这消费不到消息- 如果消费者挂了,那么会触发
rebalance
机制(后面介绍),会让其他消费者来消费该分区
六、kafka的java客户端-生产者的实现
1、引入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.4.1</version>
</dependency>
2、生产者发送消息的基本实现
Order类:
package com.zzp.kafka.entity;
public class Order {
private Long orderId;
private int count;
public Order() {
}
public Order(Long orderId, int count) {
this.orderId = orderId;
this.count = count;
}
public Long getOrderId() {
return orderId;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
MyProducer类:
package com.zzp.kafka.producer;
import com.alibaba.fastjson.JSON;
import com.zzp.kafka.entity.Order;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* 消息的发送方
*/
public class MyProducer {
//设置主题 topic
private final static String TOPIC_NAME = "my-replicated-topic";
public static void main(String[] args) throws ExecutionException,InterruptedException {
Properties props = new Properties();
//kafka地址,如果是集群用逗号隔开,比如:192.168.37.128:9092,192.168.37.128:9093
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.37.128:9092");
/*
发出消息持久化机制参数
(1)ack=0:表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但是最容易丢失消息。
(2)ack=1:至少要等待leader已经成功将数据写入到本地log,但是不需要等待所有follower是否写入,就可以继续发送下一
条消息,这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失
(3)ack=-1或all:需要等待 min.insync.replicas(默认为1,推荐配置大于等于2)这个参数配置的副本个数都成功写入日记。这种策略会保证
只要有一个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。
*/
//props.put(ProducerConfig.ACKS_CONFIG,"1");
/*
发送失败会重试,默认重试间隔100ms,重试保证消息发送的可靠性,但是也可能造成消息重复发送,比如网络抖动,所以需要在
接收者那边做好消息接收的幂等性处理
*/
//props.put(ProducerConfig.RETRIES_CONFIG,3);
//重试间隔设置
//props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG,300);
//设置发送消息的本地缓存区,如果设置了该缓存区,消息会先发送到本地缓存区,可以提高消息发送性能,默认值是33554432,即32M
// props.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
/*
kafka本地线程会从缓冲区取数据,批量发送到broker,
设置批量发送消息的大小,默认值是16384,即16kb,就是说一个broker满了16kb就发生出去
*//*
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
*//*
默认值是0,意思就是消息必须立即被发送,但这样会影响性能
一般设置10毫秒左右,就是这个消息发送完后会进入本地一个batch,如果10毫秒内,这个batch满了16kb就会随batch一起被发送出去
如果10毫秒内,batch没满,那么必须把消息发送出去,不能让消息的发送延迟时间太长
*//*
props.put(ProducerConfig.LINGER_MG_CONFIG, 10); */
//把发送的key从字符串序列化为字节数组
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//把发送消息value字符串序列化为字节数组
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//发送消息的客户端
Producer<String,String> producer = new KafkaProducer<>(props);
//要发送5条消息
int msgNum = 5;
final CountDownLatch countDownLatch = new CountDownLatch(msgNum);
for(int i = 1; i <= 100000000L; i++){
Order order = new Order((long) i, i);
//指定发送分区
/*
ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME,
0, order.getOrderId().toString(), JSON.toJSONString(order));
*/
//未指定发送区,具体发送的分区计算公式:hash(key)%partitionNum
ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME,
order.getOrderId().toString(), JSON.toJSONString(order));
/*try {
//等待消息发送成功的同步阻塞方式
RecordMetadata metadata = producer.send(producerRecord).get();
//=====阻塞=======
System.out.println("同步方式发送消息结果:" + "topic-" + metadata.topic() +
"|partition-" + metadata.partition() + "|offset-" + metadata.offset());
}catch (InterruptedException e){
e.printStackTrace();
//1.记录日记,预警系统 +1
//2.设置时间间隔1s 同步的方式再次发送,如果还不行 日记预警 人工介入
Thread.sleep(1000);
try {
//等待消息发送成功的同步阻塞方法
RecordMetadata metadata = producer.send(producerRecord).get();
}catch (Exception e1){
//人工介入
}
}catch (ExecutionException e){
e.printStackTrace();
}*/
//异步回调方式发送消息
producer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if(exception != null){
System.out.println("发送消息失败:" + exception.getStackTrace());
}
if(metadata != null){
System.out.println("异步方式发送消息结果:" + "topic-" + metadata.topic() + "|partition-"
+ metadata.partition() + "|offset-" + metadata.offset());
}
countDownLatch.countDown();//-1 = 4 ==0 5个回复全部执行完毕了
}
});
}
//判断countDownLatch是不是0,如果不是0,就等待5秒
countDownLatch.await(5, TimeUnit.SECONDS);
producer.close();
}
}
MySimpleProducer类:
package com.zzp.kafka.producer;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
public class MySimpleProducer {
//设置主题 topic
private final static String TOPIC_NAME = "my-replicated-topic";
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.设置参数
Properties props = new Properties();
//kafka地址,如果是集群用逗号隔开,比如:192.168.37.128:9092,192.168.37.128:9093
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.37.128:9092");
//把发送的key从字符串序列化为字节数组
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//把发送消息value字符串序列化为字节数组
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
//2.创建生产者的客户端
Producer<String,String> producer = new KafkaProducer<>(props);
//3.创建消息
//key:作用决定了往哪个分区上发,value: 具体要发送消息内容
ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME,
"myKey", "hello kafka");
//4.发送消息,得到消息发送的元数据并输出
RecordMetadata metadata = producer.send(producerRecord).get();
System.out.println("同步方式发送消息结果:" + "topic-" + metadata.topic() +
"|partition-" + metadata.partition() + "|offset-" + metadata.offset());
}
}
这里使用MySimpleProducer类来测试:
再次发送;
3、发送消息到指定分区上
ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME, 0 , order.getOrderId().toString(), JSON.toJSONString(order));
4、未指定分区,则会通过业务key的hash运算,算出消息往哪个分区上发
//未指定发送分区,具体发送的分区计算公式:hash(key)%partitionNum
ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME,
order.getOrderId().toString(), JSON.toJSONString(order));
5、同步发送
如果生产者发送消息没有收到ack
,生产者会阻塞,阻塞到3s的时间,如果还没有收到消息,会进行重试。重试的次数3次。
生产者同步发消息,在收到kafka的ack
告知发送成功之前一直处于阻塞状态
//等待消息发送成功的同步阻塞方法
RecordMetadata metadata = producer.send(producerRecord).get();
System.out.println("同步方式发送消息结果:" + "topic-" +metadata.topic() + "|partition-"
+ metadata.partition() + "|offset-" +metadata.offset());
6、异步发消息
异步发送,生产者发送完消息就可以执行之后的业务,broker
在收到消息后异步调用生产者提供的callback
回调方法。
//5.异步回调方式发送消息
producer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if(exception != null){
System.out.println("发送消息失败:" + exception.getStackTrace());
}
if(metadata != null){
System.out.println("异步方式发送消息结果:" + "topic-" + metadata.topic() + "|partition-"
+ metadata.partition() + "|offset-" + metadata.offset());
}
}
});
7、关于生产者的ack参数配置
在同步发送的前提下,生产者在获得集群返回的ack
之前会一直阻塞。那么集群什么时候返回ack
呢?
在同步发消息的场景下:生产者发动broker
上后,ack
会有 3 种不同的选择:
- acks=0: 表示
producer
不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但是最容易丢消息。 - acks=1(默认值): 至少要等待
leader
已经成功将数据写入本地log
,但是不需要等待所有follower
是否成功写入。就可以继续发送下一条消息。这种情况下,如果follower
没有成功备份数据,而此时leader
又挂掉,则消息会丢失。 - acks=-1或all: 需要等待
min.insync.replicas
(默认为 1 ,推荐配置大于等于2) 这个参数配置的副本个数都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。
code:
props.put(ProducerConfig.ACKS_CONFIG, "1");
下面是关于ack配置重试(如果没有收到ack
,就开启重试)的配置
/*
发送失败会重试,默认重试间隔100ms,重试保证消息发送的可靠性,但是也可能造成消息重复发送,比如网络抖动,所以需要在
接收者那边做好消息接收的幂等性处理
*/
props.put(ProducerConfig.RETRIES_CONFIG,3);
//重试间隔设置
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG,300);
8、关于消息发送的缓冲区
kafka默认会创建一个消息缓冲区,用来存储要发送的消息,缓冲区是32M
//设置发送消息的本地缓存区,如果设置了该缓存区,消息会先发送到本地缓存区,可以提高消息发送性能,默认值是33554432,即32M
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
kafka本地线程会去缓冲区中一次拉16kb的数据,发送到broker
//kafka本地线程会从缓冲区取数据,批量发送到broker,
//设置批量发送消息的大小,默认值是16384,即16kb,就是说一个broker满了16kb就发生出去
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
如果线程拉不到16kb的数据,间隔10ms也会将自己拉到的数据发送到broker
// 默认值是0,意思就是消息必须立即被发送,但这样会影响性能
//一般设置10毫秒左右,就是这个消息发送完后会进入本地一个batch,如果10毫秒内,这个batch满了16kb就会随batch一起被发送出去
//如果10毫秒内,batch没满,那么必须把消息发送出去,不能让消息的发送延迟时间太长
props.put(ProducerConfig.LINGER_MG_CONFIG, 10);
七、Java客户端消费者的实现细节
1、消费者的基本实现
package com.zzp.kafka.consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Properties;
public class MyConsumer {
//消费的主题
private final static String TOPIC_NAME = "my-replicated-topic";
//消费组
private final static String CONSUMER_GROUP_NAME = "testGroup";
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.37.128:9092");
// 消费分组名
props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
//是否自动提交offset,默认就是true
//props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
//自动提交offset的间隔时间
//props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");
//props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
/*
当消费主题的是一个新的消费组,或者指定offset的消费方式,offset不存在,那么应该如何消费
latest(默认):只消费自己启动之后发送到主题的消费
earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer,seekToBeginning(每次都从头开始消费)
*/
//props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");
//consumer给broker发送心跳的间隔时间
//props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG,1000);
//kafka如果超过10秒没有收到消费者的心跳,则会把消费者踢出消费组,进行rebalance,把分区分配给其他消费者
//props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);
//一次poll最大拉取消息的条数,可以根据消费速度快慢设置
//props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
//如果两次poll的时间如果超出了30s的时间间隔,kafka会认为其消费能力过弱,将其分区分配给其他消费者
//props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
//1.创建一个消费者的客户端
KafkaConsumer<String, String> consumer = new KafkaConsumer<String,String>(props);
//2.消费者订阅主题列表
consumer.subscribe(Arrays.asList(TOPIC_NAME));
while (true) {
/*
* 3.poll() API 是拉取消息的⻓轮询
*/
ConsumerRecords<String, String> records =consumer.poll(Duration.ofMillis( 1000 ));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("收到消息:partition = %d,offset = %d, key =%s, value = %s%n", record.partition(),
record.offset(), record.key(), record.value());
}
//所有的消息已消费完
/*if (records.count() > 0 ) {//有消息
// 手动同步提交offset,当前线程会阻塞直到offset提交成功
// 一般使用同步提交,因为提交之后一般也没有什么逻辑代码了
consumer.commitAsync();//====阻塞===== 提交成功
// 手动异步提交offset,当前线程提交offset不会阻塞,可以继续处理后面的程序逻辑
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
if (exception != null) {
System.err.println("Commit failed for " + offsets);
System.err.println("Commit failed exception: " +exception.getStackTrace());
}
}
});
}*/
}
}
2、关于消费者自动提交和手动提交offset
1)提交的内容
消费者无论是自动提交还是手动提交,都需要所属的消费组+消费的某个主题+消费的某个分区及消费的偏移量,这样的信息提交到集群的 _consumer_offsets
主题里面。
2)自动提交
消费者poll
消息下来以后就会自动提交offset
设置自动提交参数 - 默认
// 是否自动提交offset,默认就是true
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
// 自动提交offset的间隔时间
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000")
注意:自动提交会丢消息。因为消费者在消费前提交offset
,有可能提交完后还没消费时消费者挂了。
消费者poll
到消息后默认情况下,会自动向broker
的 _consumer_offsets
主题提交当前主题-分区消费的偏移量。
自动提交会丢消息: 因为如果消费者还没消费完poll
下来的消息就自动提交了偏移量,那么此 时消费者挂了,于是下一个消费者会从已提交的offset
的下一个位置开始消费消息。之前未被消费的消息就丢失掉了。
3)手动提交
需要把自动提交的配置改成false
设置手动提交参数
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
在消费完消息后进行手动提交
手动同步提交
在消费完消息后调用同步提交方法,当集群返回ack
前一直阻塞,返回ack
后表示提交成功,执行之后的逻辑
if (records.count() > 0 ) {
// 手动同步提交offset,当前线程会阻塞直到offset提交成功
// 一般使用同步提交,因为提交之后一般也没有什么逻辑代码了
consumer.commitSync();
}
手动异步提交
在消息消费完后提交,不需要等待集群ack
,直接执行之后的逻辑,可以设置一个回调方法,供集群调用
if (records.count() > 0 ) {
// 手动异步提交offset,当前线程提交offset不会阻塞,可以继续处理后面的程序逻辑
consumer.commitAsync(new OffsetCommitCallback() {
@Override
public void onComplete(Map<TopicPartition, OffsetAndMetadata>offsets, Exception exception) {
if (exception != null) {
System.err.println("Commit failed for " + offsets);
System.err.println("Commit failed exception: " +exception.getStackTrace());
}
}
});
}
3、长轮询poll消息
-
消费者建立了与broker之间的⻓连接,开始poll消息
-
默认一次poll 500条消息
//一次poll最大拉取消息的条数,可以根据消费速度快慢设置 props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500 );
可以根据消费速度的快慢来设置,因为如果两次poll的时间如果超出了30s的时间间隔,kafka会认为其消费能力过弱,将其踢出消费组。将分区分配给其他消费者。
可以通过这个值进行设置:
//如果两次poll的时间如果超出了30s的时间间隔,kafka会认为其消费能力过弱,将其分区分配给其他消费者
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000 );
-
如果每隔1s内没有poll到任何消息,则继续去poll消息,循环往复,直到poll到消息。如果超出了1s,则此次⻓轮询结束。
while (true) { /* * poll() API 是拉取消息的⻓轮询 */ ConsumerRecords<String, String> records =consumer.poll(Duration.ofMillis( 1000 )); for (ConsumerRecord<String, String> record : records) { System.out.printf("收到消息:partition = %d,offset = %d, key =%s, value = %s%n", record.partition(), record.offset(), record.key(), record.value()); }
意味着:如果一次
poll
到500条,就直接执行for
循环;如果这一次没有poll
到500条。且时间在1秒内,那么长轮询继续poll
,要么到500条,要么到1s;如果多次poll
都没有达到500条,且1秒时间到了,那么直接执行for
循环 -
消费者发送心跳的时间间隔
//consumer给broker发送心跳的间隔时间 props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000 );
-
kafka如果超过 10 秒没有收到消费者的心跳,则会把消费者踢出消费组,进行rebalance,把分区分配给其他消费者。
//kafka如果超过10秒没有收到消费者的心跳,则会把消费者踢出消费组,进行rebalance,把分区分配给其他消费者 props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000 );
4、消费者的健康状态检查
消费者每隔1s向kafka集群发送心跳,集群发现如果有超过10s没有续约的消费者,将被踢出消费组,触发改消费组的rebalance
机制,将该分区交给消费组的其他消费者进行消费。
//consumer给broker发送心跳的间隔时间
props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG,1000);
//kafka如果超过10秒没有收到消费者的心跳,则会把消费者踢出消费组,进行rebalance,把分区分配给其他消费者
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);
5、指定分区消费
//消费指定主题和分区
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
6、消息回溯消费(从头消费)
//消费指定主题和分区
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
//指定分区从头开始消费
consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME,0)));
7、指定offset消费
//消费指定主题和分区
consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
//指定分区从偏移量offset开始消费
consumer.seek(new TopicPartition(TOPIC_NAME, 0), 10);
8、从指定时间点消费
根据时间,去所有的partition
中确定该时间对应的offset
,然后去所有的partition
中找到该offset
之后的消息开始消费。
//从指定时间点消费 获取主题下的所有分区
List<PartitionInfo> topicPartitions = consumer.partitionsFor(TOPIC_NAME);
//从 1 小时前开始消费
long fetchDataTime = new Date().getTime() - 1000 * 60 * 60 ;
Map<TopicPartition, Long> map = new HashMap<>();
for (PartitionInfo par : topicPartitions) {
map.put(new TopicPartition(TOPIC_NAME, par.partition()),fetchDataTime);
}
Map<TopicPartition, OffsetAndTimestamp> parMap =consumer.offsetsForTimes(map);
for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry :parMap.entrySet()) {
TopicPartition key = entry.getKey();
OffsetAndTimestamp value = entry.getValue();
if (key == null || value == null) continue;
Long offset = value.offset();
System.out.println("partition-" + key.partition() +"|offset-" + offset);
System.out.println();
//根据消费里的timestamp确定offset
if (value != null) {
consumer.assign(Arrays.asList(key));
//从偏移量开始消费
consumer.seek(key, offset);
}
}
9、新消费组的消费偏移量
当消费主题的是一个新的消费组,或者指定offset的消费方式,offset不存在,那么应该如何消费?
- latest(默认) :只消费自己启动之后发送到主题的消息
- earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer.seekToBeginning(每次都从头开始消费)
新消费组中的消费者在启动以后,默认会从当前分区的最后一条消息的offset+1
开始消费(消费新消息)。可以通过一下配置的设置,让新的消费者第一次从头开始消费。之后开始消费新的消息(最后消费的位置的偏移量 +1)
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
八、Springboot中使用Kafka
1、引入依赖
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
2、编写配置文件
application.yml
配置文件
server:
port: 8080
spring:
kafka:
bootstrap-servers: 192.168.37.128:9092 # 如果是集群地址 用逗号隔开
producer: # 生产者
retries: 3 # 设置大于0的值,则客户端会将发送失败的记录重新发送
batch-size: 16384 # kafka本地线程会从缓冲区取数据,批量发送到broker,默认值是16384
buffer-memory: 33554432 # 设置发送消息的本地缓存区,默认值是33554432,即32M
acks: 1
# 指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer: # 消费者
group-id: default-group # 消费组
enable-auto-commit: false # 自动提交改成false 即 手动提交
auto-offset-reset: earliest # 第一次消费组从头开始消费
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
max-poll-records: 500 # 一次拉取的数量
listener:
# 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
# RECORD
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
# BATCH
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
# TIME
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
# COUNT
# TIME | COUNT 有一个条件满足时提交
# COUNT_TIME
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
# MANUAL
# 手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种
# MANUAL_IMMEDIATE
ack-mode: MANUAL_IMMEDIATE
redis:
host: 127.0.0.1
3、编写消息生产者
@RestController
public class MyKafkaController {
private final static String TOPIC_NAME = "my-replicated-topic";
@Autowired
private KafkaTemplate<String,String> kafkaTemplate;
@RequestMapping("/send")
public String sendMsg(){
kafkaTemplate.send(TOPIC_NAME,0,"key","this is a message!");
return "send success!";
}
}
4、编写消费者
@Component
public class MyConsumer {
@KafkaListener(topics = "my-replicated-topic",groupId = "MyGroup1")
public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
String value = record.value();
System.out.println("value=" + value);
System.out.println("record=" + record);
//手动提交offset
ack.acknowledge();
}
}
测试:
5、消费者中配置消费主题、分区和偏移量
@Component
public class MyConsumer {
@KafkaListener(groupId = "testGroup", topicPartitions = {
@TopicPartition(topic = "topic1", partitions = {"0", "1"}),
@TopicPartition(topic = "topic2", partitions = "0",
partitionOffsets = @PartitionOffset(partition = "1",initialOffset = "100"))
},concurrency = "3")//concurrency就是同组下的消费者个数,就是并发消费数,建议小于等于分区总数
public void listenGroup(ConsumerRecord<String, String> record,Acknowledgment ack) {
String value = record.value();
System.out.println("value=" + value);
System.out.println("record=" + record);
//手动提交offset
ack.acknowledge();
}
}
九、kafka集群中的Controller、Rebalance、HW
1、Controller
集群中谁来充当controller?
Kafka集群中的broker在zk中创建临时序号节点,序号最小的节点(最先创建的节点)将作为集群的controller,负责管理整个集群中的所有分区和副本的状态:
- 当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。
- 当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。
- 当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责让新分区被其他节点感知到。
2、Rebalance机制
前提是:消费者没有指明分区消费。
当消费组里消费者和分区的关系发生变化,那么就会触发rebalance机制。
这个机制会重新调整消费者消费哪个分区。
在触发rebalance机制之前,消费者消费哪个分区有三种策略:
- range:通过公示来计算某个消费者消费哪个分区:前面的消费者是分区总数/消费数量 +1,之后的消费者是分区总数/消费数量
- 轮询:大家轮着消费
- sticky:粘合策略,在触发了rebalance后,在消费者消费的原分区不变的基础上进行调整。如果这个策略没有开启,那么就要进行全部的重新分配。建议开启
3、HW和LEO
HW俗称高水位,HighWatermark的缩写,取一个partition对应的ISR中最小的LEO(log-end-offset)作为HW,consumer最多只能消费到HW所在的位置。另外每个replica都有HW,leader和follower各自负责更新自己的HW的状态。对于leader新写入的消息,consumer不能立刻消费,leader会等待该消息被所有ISR中的replicas同步后更新HW,此时消息才能被consumer消费。这样就保证了如果leader所在的broker失效,该消息仍然可以从新选举的leader中获取。
LEO是某个副本最后的消息的位置(log-end-offset
)
HW是已完成同步的位置,消息在写入broker
时,且每个broker
完成这条消息的同步后,HW才会变化。在这之前消费者是消费不到这条消息的。在同步完成后,HW更新之后,消费者才能消费到这条消息,这样的目的是防止消息的丢失。
十、Kafka中的优化问题
1、如何防止消息丢失
- 生产者: ack是 1 或者 -1/all 可以防止消息丢失,并且设置同步分区数>=2,如果要做到99.9999%,
ack
设成all
,把min.insync.replicas
配置成分区备份数 - 消费方:把自动提交改为手动提交。
2、如何防止重复消费
在防止消息丢失的方案中,如果生产者发送完消息后,因为网络抖动,没有收到ack
,但实际上broker
已经收到了。
此时生产者会进行重试,于是broker
就会多条相同的消息,而造成消费者的重复消费
怎么解决:
- 生产者关闭重试:会造成消息丢失(不建议)
- 消费者解决非幂等性消费问题:
所谓的幂等性:多次访问的结果时是一样的,对于rest
的请求(get
【幂等】、post
【非幂等】、put
【幂等】、delete
【幂等】)
一条消息被消费者消费多次。如果为了消息的不重复消费,而把生产端的重试机制关闭、消费端的手动提交改成自动提交,这样反而会出现消息丢失,那么可以直接在防治消息丢失的手段上再加上消费消息时的幂等性保证,就能解决消息的重复消费问题。
幂等性如何保证:
- mysql 插入业务id作为主键,主键是唯一的,所以一次只能插入一条
- 使用redis或zk的分布式锁(主流的方案:分布式锁)
3、如何做到消息的顺序消费
- 发送方:在发送时将
ack
不能设置 0 ,关闭重试,使用同步发送,等到发送成功再发送下一条。确保消息是顺序发送的。使用同步的发送 - 接收方:消息是发送到一个分区中,只能有一个消费组的消费者来接收消息。因此,kafka的顺序消费会牺牲掉性能。但是比如rocketmq在这一块有专门的功能已设计好。
4、如何解决消息积压问题
消息积压问题出现:消息的消费者的消费速度远赶不上生产者的生产的消息,导致kafka中有大量的数据没有被消息。随着没有被消费的数据堆积越多,消费者寻址的性能会越来越差,最后导致整个kafka对外提供的服务的性能越来越差,从而造成其他服务访问速度变慢,造成服务雪崩。
消息积压会导致很多问题,比如磁盘被打满、生产端发消息导致kafka性能过慢,就容易出现服务雪崩,就需要有相应的手段:
- 在这个消费者中,使用多线程,充分利用机器的性能进行消费消息。
- 通过业务的架构设计,提升业务层消费的性能。
- 创建多个消费组,多个消费者,部署到其他机器上,一起消费,提高消费者的消费速度。
- 创建一个消费者,该消费者在kafka另建一个主题,配上多个分区,对个分区配上多个消费者。该消费者将
poll
下来的消息,不进行消费,直接转到新建的主题上。此时,新的主题的多个分区的多个消费者就开始一起消费。— 不常用
5、实现延迟队列的效果
- kafka中创建相应的主题
- 消费者消费该主题的消息(轮询)
- 消费者消费时判断消息创建时间和当前时间是否超过30分钟(前提是订单没支付)
- 如果是:去数据库中修改订单状态为已取消
- 如果否:记录当前消息的
offset
,并不再继续消费之后的消息。等待1分钟后,再次向kafka拉取该offset
及之后的消息,继续进行判断,以此反复。
延迟队列的应用场景:在订单创建成功后如果超过 30 分钟没有付款,则需要取消订单,此时可用延时队列来实现
-
创建多个topic,每个topic表示延时的间隔
- topic_5s: 延时5s执行的队列
- topic_1m: 延时 1 分钟执行的队列
- topic_30m: 延时 30 分钟执行的队列
-
消息发送者发送消息到相应的topic,并带上消息的发送时间
-
消费者订阅相应的topic,消费时轮询消费整个topic中的消息
- 如果消息的发送时间,和消费的当前时间超过预设的值,比如 30 分钟
- 如果消息的发送时间,和消费的当前时间没有超过预设的值,则不消费当前的offset及之后的offset的所有消息都消费
- 下次继续消费该offset处的消息,判断时间是否已满足预设值
十一、Kafka-eagle监控平台
先安装jdk
安装Kafka-eagle,官网下载压缩包:http://www.kafka-eagle.org/
给kafka-eagle配置环境变量
export KE_HOME=/usr/local/kafka-eagle/xxxx
export PATH=$PATH:$KE_HOME/bin:$JAVA_HOME/bin
vim /etc/profile
source /etc/profile
【使配置文件生效】
需要修改kafka-eagle内部的配置文件: config/system-config.properties
# 配置zk 去掉cluster2
efak.zk.cluster.alias=cluster1
cluster1.zk.list=192.168.37.128:2181
# cluster2.zk.list=xdn10:2181,xdn11:2181,xdn12:2181
# 配置mysql 先注释之前的数据库配置 更改成mysql数据库
kafka.eagle.driver=com.mysql.cj.jdbc.Driver
kafka.eagle.url=jdbc:mysql://192.168.37.128:3306/ke?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
kafka.eagle.username=root
kafka.eagle.password=123456
进入到bin目录,为ke.sh增加可执行的权限
chmod +x ke.sh
启动kafka-eagle
./ke.sh start