kafka入门到精通

kafka入门到精通

连接视频


一、为什么使用消息队列

1、使用同步的通信方式来解决多个服务之间的通信

在这里插入图片描述

同步的通信方式会存在性能和稳定的问题。


2、使用异步的通信方式

在这里插入图片描述

针对于同步的通信方式来说,异步的方式,可以让上游快速成功,极大提高了系统的吞吐量。而且在分布式系统中,通过下游多个服务的分布式事务的保障,也能保障业务执行之后的最终一致性。

消息队列解决具体的是什么问题 – 通信问题



二、消息队列的流派

目前消息队列的中间件选项有很多种:

  • rabbitMQ:内部的可玩性(功能性)是非常强的
  • rocketMQ:阿里内部一个大神,根据kafka的内部执行原理,手写的一个消息队列中间件。性能是与kafka相比肩,除此之外,在功能上封装了更多的功能。
  • kafka:全球消息处理性能最快的一款MQ
  • zeroMQ

这些消息中间件有什么区别?

什么是 MQ

Message Queue(MQ)消息队列中间件。很多人都说:MQ 通过将消息的发送和接收分离来实现应用程序的异步和解偶,这个给人的直觉是——MQ 是异步的,用来解耦的,但是这个只是 MQ 的效果而不是目的。MQ 真正的目的是为了通讯,屏蔽底层复杂的通讯协议,定义了一套应用层的、更加简单的通讯协议。一个分布式系统中两个模块之间通讯要么是HTTP,要么是自己开发的 (rpc) TCP,但是这两种协议其实都是原始的协议。HTTP 协议很难实现两端通讯——模块 A 可以调用 BB 也可以主动调用 A,如果要做到这个两端都要背上WebServer,而且还不支持⻓连接( HTTP 2.0 的库根本找不到)。TCP 就更加原始了,粘包、心跳、私有的协议,想一想头皮就发麻。MQ 所要做的就是在这些协议之上构建一个简单的“协议”——生产者/消费者模型MQ 带给我的“协议”不是具体的通讯协议,而是更高层次通讯模型。它定义了两个对象——发送数据的叫生产者接收数据的叫消费者, 提供一个SDK 让我们可以定义自己的生产者和消费者实现消息通讯而无视底层通讯协议

1、有 Broker 的 MQ

这个流派通常有一台服务器作为 Broker,所有的消息都通过它中转。生产者把消息发送给它就结束自己的任务了,Broker 则把消息主动推送给消费者(或者消费者主动轮询)

重 Topic

Topic(主题)

整个broker,依据topic来进行消息的中转。在重topic的消息队列里必然需要topic的存在

重topicKafka、RocketMQ、ActiveMQ

kafka、JMS(ActiveMQ)就属于这个流派,生产者会发送 key 和数据到 Broker,由 Broker比较 key 之后决定给哪个消费者。这种模式是我们最常⻅的模式,是我们对 MQ 最多的印象。在这种模式下一个 topic 往往是一个比较大的概念,甚至一个系统中就可能只有一个topictopic 某种意义上就是 queue,生产者发送 key 相当于说:“hi,把数据放到 key 的队列中”

在这里插入图片描述

如上图所示,Broker 定义了三个队列,key1,key2,key3,生产者发送数据的时候会发送key1dataBroker 在推送数据的时候则推送 data(也可能把 key 带上)。

虽然架构一样但是 kafka 的性能要比 jms 的性能不知道高到多少倍,所以基本这种类型的MQ 只有 kafka 一种备选方案。如果你需要一条暴力的数据流(在乎性能而非灵活性)那么kafka 是最好的选择


轻 Topic

topic只是一种中转模式

这种的代表是 RabbitMQ(或者说是 AMQP)。生产者发送 key 和数据,消费者定义订阅的队列,Broker 收到数据之后会通过一定的逻辑计算出 key 对应的队列,然后把数据交给队列

在这里插入图片描述

这种模式下解耦了 keyqueue,在这种架构中 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进行通信。

BrokerMQ 的代表是 ZeroMQ。该作者非常睿智,他非常敏锐的意识到——MQ 是更高级的 Socket,它是解决通讯问题的。所以 ZeroMQ 被设计成了一个 “库” 而不是一个中间件,这种实现也可以达到——没有 Broker 的目的

在这里插入图片描述

节点之间通讯的消息都是发送到彼此的队列中,每个节点都既是生产者又是消费者。ZeroMQ做的事情就是封装出一套类似于 SocketAPI 可以完成发送数据,读取数据

ZeroMQ 其实就是一个跨语言的、重量级的 Actor 模型邮箱库。你可以把自己的程序想象成一个 ActorZeroMQ 就是提供邮箱功能的库;ZeroMQ 可以实现同一台机器的 RPC 通讯也可以实现不同机器的 TCPUDP 通讯,如果你需要一个强大的、灵活、野蛮的通讯能力,别犹豫 ZeroMQ



三、Kafka的基本知识

Kafka介绍

Kafka是最初由Linkedin公司开发,是一个分布式、支持分区的(partition)、多副本的 (replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理 大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、 Storm/Spark流式处理引擎,web/nginx日志、访问日志,消息服务等等,用scala语言编 写,Linkedin2010 年贡献给了Apache基金会并成为顶级开源 项目。

Kafka的使用场景

  • 日志收集:一个公司可以用Kafka收集各种服务的log,通过kafka以统一接口服务的方式 开放给各种consumer,例如hadoopHbaseSolr等。
  • 消息系统:解耦和生产者和消费者、缓存消息等。
  • 用户活动跟踪Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网⻚、 搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过 订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖 掘。
  • 运营指标Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产 各种操作的集中反馈,比如报警和报告。

Kafka基本概念

kafka是一个分布式的,分区的消息(官方称之为commit log)服务。它提供一个消息系统应该 具备的功能,但是确有着独特的设计。可以这样来说,Kafka借鉴了JMS规范的思想,但是确 并 没有完全遵循JMS规范

首先,让我们来看一下基础的消息(Message)相关术语:

名称解释
Broker消息中间件处理节点,⼀个Kafka节点就是⼀个broker,⼀个或者多个Broker可以组成⼀个Kafka集群
TopicKafka根据topic对消息进⾏归类,发布到Kafka集群的每条消息都需要指定⼀个topic
Producer消息⽣产者,向Broker发送消息的客户端
Consumer消息消费者,从Broker读取消息的客户端
ConsumerGroup每个Consumer属于⼀个特定的Consumer Group,⼀条消息可以被多个不同的Consumer Group消费,但是⼀个Consumer Group中只能有⼀个Consumer能够消费该消息
Partition物理上的概念,⼀个topic可以分为多个partition,每个partition内部消息是有序的

因此,从一个较高的层面上来看,producer通过网络发送消息到Kafka集群,然后consumer 来进行消费,如下图

在这里插入图片描述

服务端(brokers)和客户端(producerconsumer)之间通信通过 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核心配置详解:

PropertyDefaultDescription
broker.id0每个broker都可以⽤⼀个唯⼀的⾮负整数id进⾏标识;这个id可以作为broker的“名字”,你可以选择任意你喜欢的数字作为id,只要id是唯⼀的即可。
log.dirs/tmp/kafka-logskafka存放数据的路径。这个路径并不是唯⼀的,可以是多个,路径之间只需要使⽤逗号分隔即可;每当创建新partition时,都会选择在包含最少partitions的路径下进⾏。
listenersPLAINTEXT://192.168.37.128:9092server接受客户端连接的端⼝,ip配置kafka本机ip即可
zookeeper.connectlocalhost:2181zooKeeper连接字符串的格式为:hostname:port,此处hostname和port分别是ZooKeeper集群中某个节点的host和port;zookeeper如果是集群,连接⽅式为hostname1:port1, hostname2:port2,hostname3:port3
log.retention.hours168每个⽇志⽂件删除之前保存的时间。默认数据保存时间对所有topic都⼀样。
num.partitions1创建topic的默认分区数
default.replication.factor1⾃动创建topic的默认副本数量,建议设置为⼤于等于2
min.insync.replicas1当producer设置acks为-1时,min.insync.replicas指定replicas的最⼩数⽬(必须确认每⼀个repica的写数据都是成功的),如果这个数⽬没有达到,producer发送消息会产⽣异常
delete.topic.enablefalse是否允许删除主题

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服务器的某个主题消费消息

对于consumerkafka同样也携带了一个命令行客户端,会将获取到内容在命令中进行输出, 默认是消费最新的消息 。使用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、关于消息的细节

在这里插入图片描述

  • 生产者将消息发送给brokerbroker会将消息保存在本地的日记文件中。

    /usr/local/data/kafka-logs/主题-分区/00000000000000000000.log

  • 消息的保存是有序的,通过offset偏移量来描述消息的有序性

  • 消费者消费消息也是通过offset来描述当前要消费的那条消息的位置


7、单播消息

在一个kafkatopic中,启动两个消费者,一个生产者,问:生产者发送消息,这条消息是否同时会被两个消费者消费?

如果多个消费者在同一个消费组,那么只有一个消费者可以收到订阅的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

主题 - topickafka中是一个逻辑的概念,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-0test-1中的0000000.log文件中

小细节:

  • 定期将自己消费分区的offset提交给kafka内部topic__consumer_offsets,提交过去的 时候,keyconsumerGroupId+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-49kafka内部自己创建了__consumer_offsets 主题包含了 50个分区。这个主题用来存放消费者消费某个消息某个主题的偏移量。因为每个消费者都会自己维护着消费的主题的偏移量,也就是说每个消费者会把消费者的主题的偏移量自主上报给kafka中的默认主题:consumer_offset。因此kafka为了提升这个主题的并发性,默认设置了50个分区。

    • 至于提交到哪个分区:通过hash函数hash(consumerGroupId) % __consumer_offsets主题的分区数
    • 提交到该主题的内容是:keyconsumerGroupld + topic + 分区号value就是当前offset的值
  • 文件中保存的消息,默认是保存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的概念中介绍。

通过killleader后再查看主题情况:

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次

生产者同步发消息,在收到kafkaack告知发送成功之前一直处于阻塞状态

//等待消息发送成功的同步阻塞方法
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());
            }
    

    意味着:如果一次poll500条,就直接执行for循环;如果这一次没有poll500条。且时间在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、消费者的健康状态检查

消费者每隔1skafka集群发送心跳,集群发现如果有超过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集群中的brokerzk中创建临时序号节点,序号最小的节点(最先创建的节点)将作为集群的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)作为HWconsumer最多只能消费到HW所在的位置。另外每个replica都有HWleaderfollower各自负责更新自己的HW的状态。对于leader新写入的消息,consumer不能立刻消费,leader会等待该消息被所有ISR中的replicas同步后更新HW,此时消息才能被consumer消费。这样就保证了如果leader所在的broker失效,该消息仍然可以从新选举的leader中获取。

LEO是某个副本最后的消息的位置(log-end-offset
HW是已完成同步的位置,消息在写入broker时,且每个broker完成这条消息的同步后,HW才会变化。在这之前消费者是消费不到这条消息的。在同步完成后,HW更新之后,消费者才能消费到这条消息,这样的目的是防止消息的丢失。

在这里插入图片描述



十、Kafka中的优化问题

1、如何防止消息丢失

  • 生产者ack1 或者 -1/all 可以防止消息丢失,并且设置同步分区数>=2,如果要做到99.9999%,ack设成all,把min.insync.replicas配置成分区备份数
  • 消费方:把自动提交改为手动提交。

2、如何防止重复消费

在防止消息丢失的方案中,如果生产者发送完消息后,因为网络抖动,没有收到ack,但实际上broker已经收到了。
此时生产者会进行重试,于是broker就会多条相同的消息,而造成消费者的重复消费
怎么解决:

  • 生产者关闭重试:会造成消息丢失(不建议)
  • 消费者解决非幂等性消费问题
    所谓的幂等性:多次访问的结果时是一样的,对于rest的请求(get幂等】、post非幂等】、put幂等】、delete幂等】)

一条消息被消费者消费多次。如果为了消息的不重复消费,而把生产端的重试机制关闭、消费端的手动提交改成自动提交,这样反而会出现消息丢失,那么可以直接在防治消息丢失的手段上再加上消费消息时的幂等性保证,就能解决消息的重复消费问题。

幂等性如何保证:

  • mysql 插入业务id作为主键,主键是唯一的,所以一次只能插入一条
  • 使用rediszk的分布式锁(主流的方案:分布式锁)

在这里插入图片描述

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值