kafka学习笔记

前言

kafka官网:https://kafka.apache.org/

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

kafka使用SSL认证会降低至少20%~30%的性能,生产环境一般都通过配置防火墙来保证安全性。

 

关键术语

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的内部消息都是有序的。

 

kafka原理图如下:

 

 

  

 kafka使用场景

①、日志收集:可以用kafka收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,比如:Hadoop、HBASE、Solr等;

②、消息系统:解耦和生产者、消费者、缓存消息等;

③、用户活动跟踪:kafka经常被用来记录web用户或者app用户的各种活动,比如浏览网页、搜索、点击等操作,这些操作被各服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到Hadoop、数据仓库中做离线分析和信息挖掘;

④、运营指标:kafka也经常用来记录运营监控数据,包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。

 

 

kafka基本使用

安装前的环境准备

由于Kafka是用Scala语言开发的,运行在JVM上,所以在安装Kafka之前需要先安装JDK,执行命令:yum install java-1.8.0-openjdk* -y

Kafka依赖zookeeper,所以需要先安装zookeeper,执行命令如下:

wget https://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.5.8/apache-zookeeper-3.5.8-bin.tar.gz

tar -zxvf apache-zookeeper-3.5.8-bin.tar.gz

cd apache-zookeeper-3.5.8-bin

cp conf/zoo_sample.cfg conf/zoo.cfg

 

#启动zookeeper

bin/zkServer.sh start

bin/zkCli.sh

#查看zk的根目录相关节点

ls /

 

下载kafka安装包

下载2.4.1 release版本并解压:

#2.11是scala的版本,2.4.1是kafka的版本

wget https://mirror.bit.edu.cn/apache/kafka/2.4.1/kafka_2.11-2.4.1.tgz

tar -xzf kafka_2.11-2.4.1.tgz

cd kafka_2.11-2.4.1

 

修改配置

修改配置文件config/server.properties:

#broker.id属性在kafka集群中必须唯一,且必须是非负整数

broker.id=0

#kafka部署的机器ip和提供服务的端口号 也就是生产者和消费者访问的地址

listeners=PLAINTEXT://IP:PORT

#kafka存放数据的路径,这个路径可以是多个,用逗号分隔;每当创建新的partition时,都会选择在包含partition最少的路径下创建。

log.dirs=/usr/local/data/kafka.logs

#kafka连接zookeeper的地址 如果zookeeper是集群,用逗号隔开,如:IP:PORT,IP:PORT

zookeeper.connect=IP:PORT

#每个日志文件保存的时间,默认数据保存时间对所有topic都一样

log.retention.hours=168

#是否允许删除主题

delete.topic.enable=false

 

启动服务

bin/kafka-server-start.sh -daemon config/server.properties

-daemon表示在后台进程运行,否则ssh客户端退出后就会停止服务。

需要注意:在启动kafka时会使用Linux主机名关联的ip地址,所以需要把主机名和Linux的IP映射配置到本地hosts里,用vim /etc/hosts

或者使用启动命令bin/kafka-server-start.sh config/server.properties & 这个命令启动只要后台有日志变化就会打印出来

 

进入zookeeper目录通过zookeeper客户端查看zookeeper的目录树

bin/zkCli.sh

#查看zk的根目录kafka相关节点

ls /

#查看kafka节点

ls /brokers/ids

 

停止kafka服务

bin/kafka-server-stop.sh

 

创建主题

创建一个名字为test的topic,这个topic只有一个partition,并且备份银子也设置为1:bin/kafka-topics.sh --create --zookeeper IP:PORT --replication-factor 1 --partitions 1 --topic test

查看目前存在的主题

bin/kafka-topics.sh --list --zookeeper IP:PORT

除了手动创建topic,当producer发布一个消息到某个指定的topic时,如果这个topic不存在,就会自动创建。

 

删除主题

bin/kafka-topics.sh --delete --topic test --zookeeper IP:PORT

 

发送消息

kafka自带了一个producer命令客户端,可以从本地文件中读取内容,或者我们也可以在命令行中输入内容,并且把这些信息发送到kafka集群中。默认情况下,每一行会被当做一条独立的消息。

首先运行发布消息的脚本,然后再命令中输入要发送的消息内容:

bin/kafka-console-producer.sh --broker-list IP:PORT --topic test

 

消费消息

kafka的消息消费完不会删除,因为是存在文件中的(消息默认保存在磁盘一周,时间可配置);kafka的消息消费是通过偏移量消费的,每个消费者都会维护自己的消费偏移量。

对于consumer,kafka同样也自带一个命令行客户端,会把获取到的消息在命令中输出,默认是消费最新的消息:

bin/kafka-console-consumer.sh --bootstrap-server IP:PORT --topic test

如果想消费之前的消息,可以通过--from-beginning参数指定:

bin/kafka-console-consumer.sh --bootstrap-server IP:PORT --from-beginning --topic test

消费多主题

bin/kafka-console-consumer.sh --bootstrap-server IP:PORT --whitelist "test|test-2"

单播消费

一条消息只能被一个消费者消费的模式,类似queue模式,只需让所有消费者在同一个消费组里即可。分别在两个客户端执行下方消费命令,然后发送消息,结果只有一个客户端能收到消息:

bin/kafka-console-consumer.sh --bootstrap-server IP:PORT --consumer-property group.id=testGroup --topic test

多播消费

一条消息能被多个消费者消费的模式,类似publish-subscribe模式,针对kafka同一条消息只能被同一个消费组下的某一个消费者消费的特性,要实现多播消费只要保证这些消费者属于不同的消费组即可。我们再增加一个消费者,该消费者属于testGroup-2消费组,结果两个客户端都能收到消息:

 bin/kafka-console-consumer.sh  --bootstrap-server IP:PORT --consumer-property group.id=testGroup-2 --topic test

查看消费组名

bin/kafka-consumer-groups.sh --bootstrap-server IP:PORT --list

查看消费组的消费偏移量

bin/kafka-consumer-groups.sh --bootstrap-server IP:PORT --describe --group testGroup

回车后展示的信息中名词解释:

current-offset:当前消费组的已消费偏移量(不同的消费者只影响所属消费组的已消费偏移量)

log-end-offset:主题对应分区消息的结束偏移量(有新接收到的消息结束偏移量会增加)

lag:当前消费组未消费的消息数

 

主题Topic和消息日志Log

可以把Topic理解为一个类别的名称,同类消息发送到同一个Topic下;对于每一个Topic,可以有多个分区(Partition)日志文件:

Partition是一个有序的消息序列,这些消息按顺序添加到一个叫做commit log的文件中,每个partition中的消息都有一个唯一的编号,称为offset,用来唯一标识某个分区中的消息。

每个partition都对应一个commit log文件,一个partition中消息的offset都是唯一的,但是不同的partition中消息的offset可能是相同的。

kafka一般不会删除消息,不管这些消息是否被消费,kafka只会根据配置的日志保留时间(log.retention.hours)来确定消息多久被删除,默认是保留最近一周的日志消息。kafka的性能与保留的消息数据量大小没有关系,所以保存大量的数据消息日志信息也不会有什么影响,但是具体保存多久要针对具体的业务场景而定,不然数据太大占用磁盘空间也会变大。

每个consumer都是基于自己在commit log中的消费进度(offset)来工作的,在kafka中,消费offset由消费者自己来维护;一般会按照顺序逐条消费commit log中的消息,也可以人为指定offset来重复消费某些消息或者跳过某些消息。

这意味着kafka中consumer对集群的影响是非常小的,添加一个或者减少一个consumer,对集群或其他consumer都没有影响,因为每个consumer都维护各自的消费offset。

创建topic时默认只有一个partition,但是可以通过设置创建多个partition;一个partition相当于一个queue;设置partition的目的是为了分布式存储,减轻一个queue的压力,提高消息的读写速度;消息发到某个topic的分区,这个消息就会存到这个分区对应的commit log文件中,这也是kafka高性能的原因之一。

 创建多个分区的主题

bin/kafka-topics.sh --create --zookeeper IP:PORT --replication-factor 1 --partition 2 --topic test1

查看topic的情况

bin/kafka-topics.sh --describe --zookeeper IP:PORT --topic test1

回车后展示的内容解释:

第一行是所有分区的概要信息。之后的每一行是每一个partition的信息。

leader节点负责给定partition的所有读写请求;

replicas表示某个partition在哪几个broker上存在备份,不管这个节点是不是leader,甚至这个节点挂了也会列出;

isr是replicas的一个子集,它只列出当前还存活的并且已经同步备份了该partition的节点。

消息日志文件主要存放在分区文件夹里以log为扩展名的日志文件中

对已创建的topic增加分区数量(目前kafka不支持减少分区):

bin/kafka-topics.sh -alter --partitions 3 --zookeeper IP:PORT --topic test

 

Kafka集群

对于kafka来说,单台机器也叫集群,多台机器也叫集群,因为kafka的集群是针对分区而言的。这里再启动2个broker实例(由于前面已经启动了一个broker,这样集群中就有三个broker了)。

首先建立各broker的配置文件:

cp config/server.properties config/server-1.properties

cp config/server.properties config/server-2.properties

配置文件修改内容:

config/server-1.properties

#broker.id属性在kafka集群中必须唯一

broker.id=1

#kafka部署的机器IP和提供服务的端口号

listeners=PLAINTEXT://IP:PORT

#日志存储路径

log.dirs=/usr/local/data/kafka-logs-1

#kafka连接zookeeper的地址,要把多个kafka实例组成集群,对应连接的zookeeper必须相同

zookeeper.connect=IP:PORT

config/server-2.properties

#broker.id属性在kafka集群中必须唯一

broker.id=2

#kafka部署的机器IP和提供服务的端口号

listeners=PLAINTEXT://IP:PORT

#日志存储路径

log.dirs=/usr/local/data/kafka-logs-2

#kafka连接zookeeper的地址,要把多个kafka实例组成集群,对应连接的zookeeper必须相同

zookeeper.connect=IP:PORT

 

启动前面配置好的两个broker实例:

bin/kafka-server-start.sh -daemon config/server-1.properties

bin/kafka-server-start.sh -daemon config/server-2.properties

查看zookeeper确认集群节点是否都注册成功(kafka节点的元信息存储在zookeeper中):

ls /brokers/ids

 

新建一个topic,副本数设置为3,分区数设置为2(副本是针对分区的,这里3个副本2个分区就是说每个分区都有3个副本,放在不同的节点中,目的是为了容灾;副本也分leader副本和follower副本,消息都是写在leader副本中的,leader副本写完消息后会同步给follower副本;如果leader副本挂了,会从follower副本中选举出一个新的leader副本;leader和follower是针对分区的,broker没有leader和follower的概念):

bin/kafka-topics.sh --create --zookeeper IP:PORT --replication-factor 3 --partitions 2 --topic my-replicated-topic

查看topic的情况:

bin/kafka-topics.sh --describe --zookeeper IP:PORT --topic my-replicated-topic

 

架构上来讲,kafka是不支持顺序消费的,因为分区是没有顺序的,可能存在多台服务器中。如果一定要保持顺序,那么给一个topic就设置一个分区,一个消费组一个消费者就可以,但是这样kafka的性能就没发挥出来。

 

kafka日志存储持久化的机制

kafka一个分区的消息数据对应存储在一个文件夹下,以topic名称+分区号命名,消息在分区内是分段(segment)存储的,每个段的消息都存储在不一样的log文件里,这种特性方便旧的分段文件被快速删除。kafka规定了一个段位的log文件最大为1G,做这个限制是为了方便把log文件加载到内存中操作。

# 部分消息的offset索引文件,kafka每次往分区发4K(可配置)消息就会记录一条当前消息的offset到index文件中。如果要定位消息的offset,会先在这个文件中快速定位,再去log文件找具体的消息内容。

xxxxxx.index

# 消息存储文件,主要存offset和消息内容

xxxxxx.log

# 消息的发送时间索引文件,kafka每次往分区发4K(可配置)消息就会记录一条当前消息的发送时间戳与对应的offset到timeindex文件中。如果需要按照时间来定位消息的offset,会先在这个文件里查找。

xxxxxx.timeindex

文件名的数字代表了这个日志段文件里包含的起始offset,也就说明这个分区至少写了接近多少条数据了。

kafka broker有个参数:log.segment.bytes 这个参数限定了每个日志段文件的大小,最大就是1GB。

一个日志段文件满了,就会自动开一个新的日志段文件来写入,避免单个文件过大,影响文件的读写性能,这个过程叫做log rolling,正在被写入的那个日志段文件,叫做active log segment。

 

通过Java客户端访问Kafka

基础用法之添加maven依赖

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>2.4.1</version>
</dependency>

 

基础用法之生产者

 1 package com.kafka.base;
 2 
 3 import com.alibaba.fastjson.JSON;
 4 import org.apache.kafka.clients.producer.KafkaProducer;
 5 import org.apache.kafka.clients.producer.Producer;
 6 import org.apache.kafka.clients.producer.ProducerConfig;
 7 import org.apache.kafka.clients.producer.ProducerRecord;
 8 import org.apache.kafka.common.serialization.StringSerializer;
 9 
10 import java.util.Properties;
11 import java.util.concurrent.CountDownLatch;
12 import java.util.concurrent.TimeUnit;
13 
14 /**
15  * @description: 消息生产者
16  * @author: YangWanYi
17  * @create: 2022-07-04 10:19
18  **/
19 public class MsgProducer {
20 
21     // 主题名
22     private final static String TOPIC_NAME = "test-topic";
23 
24     public static void main(String[] args) throws InterruptedException {
25         Properties properties = new Properties();
26         // 配置kafka服务端地址
27         properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.65.60:9999,192.168.65.60:9998");
28         /*
29             设置发出消息的持久化机制 默认是1
30             acks=0:表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但最容易丢失消息;
31             acks=1:至少要等待leader已经成功把数据写入本地log,但是不需要等待所有followers成功写入,就可以继续发送下一条消息。这种情况下,如果follower没有成功备份数据,而此时leader又恰好挂掉了,消息就会丢失;
32             acks=-1或all:需要等待min.insync.replicas(默认是1,推荐配置大于等于2)这个参数配置的副本个数都成功写入日志,才可以继续发送下一条消息。这种策略只要有一个备份存活就不会丢失数据。这是最强的数据保证,一般除非金融级别,或者跟钱有关的场景才会使用这种配置。
33          */
34         properties.put(ProducerConfig.ACKS_CONFIG, "1");
35         // 消息发送失败会重试,默认重试间隔100ms,重试能保证消息发送的可靠性,但是也可能造成消息重复发送,比如网络抖动的情况,所以需要在接收者(消费者)那边做好消息接收的幂等性处理。
36         properties.put(ProducerConfig.RETRIES_CONFIG, 3);
37         // 重试间隔设置 单位毫秒
38         properties.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);
39         // 设置发送消息的本地缓冲区 如果设置了该缓冲区,消息会先发送到本地缓冲区,可以提高消息发送性能,默认值是33554432,即32MB。
40         properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
41         // kafka本地线程会从缓冲区取数据,批量发送到broker。设置批量发送消息的大小,默认是16384,即16KB,表示一个batch满了16KB就发送出去。
42         properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
43         /*
44             默认值是0,表示消息必须立即被发送,这会影响性能。
45             一般设置10毫秒左右,就是说这个消息发送完后会进入本地的一个batch,如果10ms内这个batch满了16KB就会随batch一起被发送出去;
46             如果10ms内batch没满,那也要把消息发送出去,不能让消息的发送延迟时间太长。
47          */
48         properties.put(ProducerConfig.LINGER_MS_CONFIG, 10);
49         // 把发送的key从字符串序列化为字节数组
50         properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
51         // 把发送的value从字符串序列化为字节数组
52         properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
53 
54         Producer<String, String> producer = new KafkaProducer<>(properties); // 传入配置信息到producer对象中
55         int msgNum = 5;
56         final CountDownLatch countDownLatch = new CountDownLatch(msgNum); // 指定CountDownLatch的数量 和需要发送消息的数量一致
57         for (int i = 0; i < msgNum; i++) {
58             Command command = new Command("testName" + i, "detail"); // 构建自定义的消息对象
59             // 指定发送主题和分区
60 //            ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, 0, command.getName(), JSON.toJSONString(command));
61             // 仅指定主题不指定分区 具体发送的分区计算方式:hash(key)%partitionNum  Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
62             ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME, command.getName(), JSON.toJSONString(command));
63             // 等待消息发送成功的同步阻塞方法(send方法都是异步的 这里通过get方法同步)
64             /*RecordMetadata recordMetadata = producer.send(producerRecord).get();
65             System.out.println("同步方式发送消息的结果:" + "topic-" + recordMetadata.topic() + ",partition-" + recordMetadata.partition() + ",offset-" + recordMetadata.offset());*/
66             // 异步回调方式发送消息
67             producer.send(producerRecord, (recordMetadata, e) -> {
68                 if (null != e) {
69                     System.out.println("消息发送失败:" + e.getStackTrace());
70                 }
71                 if (null != recordMetadata) {
72                     System.out.println("异步方式发送消息的结果:" + "topic-" + recordMetadata.topic() + ",partition-" + recordMetadata.partition() + ",offset-" + recordMetadata.offset());
73                 }
74                 countDownLatch.countDown(); // 发送成功就CountDownLatch就减去1
75             });
76             // TODO 处理业务逻辑  如果有业务逻辑建议使用异步 如果没有业务逻辑 可以使用同步
77         }
78         countDownLatch.await(5, TimeUnit.SECONDS); // 保证所有消息都收到成功通知才释放资源
79         producer.close(); // 关掉生产者对象 释放资源
80     }
81 }

 

基础用法之消费者

  1 package com.kafka.base;
  2 
  3 import org.apache.kafka.clients.consumer.*;
  4 import org.apache.kafka.common.PartitionInfo;
  5 import org.apache.kafka.common.TopicPartition;
  6 import org.apache.kafka.common.serialization.StringSerializer;
  7 
  8 import java.time.Duration;
  9 import java.util.*;
 10 
 11 /**
 12  * @description: 消息消费端
 13  * @author: YangWanYi
 14  * @create: 2022-07-04 16:12
 15  **/
 16 public class MsgConsumer {
 17 
 18     // 主题名
 19     private final static String TOPIC_NAME = "test-topic";
 20     // 消费组
 21     private final static String CONSUMER_GROUP_NAME = "test-group";
 22 
 23     public static void main(String[] args) {
 24         Properties properties = new Properties();
 25         // 配置kafka服务端地址
 26         properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.65.60:9999,192.168.65.60:9998");
 27         // 设置消费分组名
 28         properties.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
 29         /*
 30             是否自动提交offset 默认是true 不提交offset的话,每次启动consumer都会重复消费。
 31             一般都设置为false,手动提交。因为自动提交容易出现消息丢失或重复消费的情况。
 32             比如在自动提交间隔1秒的时间内消费端程序还没执行完就提交了,并且消费端挂了,消息就会丢失;
 33             或者消费端在0.2秒的时候就已经执行完了后消费端挂了,再次启动就会重复消费。
 34          */
 35         properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
 36 //        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
 37         // 自动提交offset的间隔时间 单位毫秒 ENABLE_AUTO_COMMIT_CONFIG设置为false就不需要设置这个参数
 38 //        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
 39         /*
 40             配置当消费主题的是一个新的消费组,或者指定offset的消费方式,offset不存在时应该如何消费
 41             latest:只消费自己启动之后发送到主题的消息(默认的)
 42             earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer.seekToBeginning(每次都从头开始消费)
 43          */
 44         properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
 45         // consumer给broker发送心跳的间隔时间,broker接收到心跳时如果有Rebalance发生,会通过心跳响应把Rebalance方案下发给consumer,这个时间可以稍微短一点。
 46         properties.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000);
 47         // 服务端broker多久感知不到一个consumer心跳就认为这个consumer故障了,会把它踢出消费组。对应的partition也会被重新分配给其他的consumer,默认是10S。
 48         properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000);
 49         // 一次poll最大拉取消息的数量 如果消费者处理速度很快,可以设置大点;如果处理速度一般,可以设置小点。
 50         properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
 51         /*
 52             如果两次poll操作间隔超过了这个时间,broker就会认为这个consumer处理能力太弱了,会把它踢出消费组,把分区分配给别的consumer消费。
 53             如果线上出现莫名其妙不消费被踢出的情况可以排查这个参数,也可以看看处理逻辑是否可以优化性能,或者把MAX_POLL_RECORDS_CONFIG设置小一点。
 54          */
 55         properties.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000);
 56         // 反序列化 需要和生产者对应序列化器
 57         properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
 58         properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
 59 
 60         KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties); // 传入配置信息到consumer对象中
 61         consumer.subscribe(Arrays.asList(TOPIC_NAME)); // 订阅主题 可以批量订阅多个主题 从上次提交的offset开始消费
 62 
 63         // 指定分区消费
 64 //        consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
 65 
 66         // 消息回溯消费
 67 //        consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
 68 //        consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
 69 
 70         // 指定offset消费
 71 //        consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0)));
 72 //        consumer.seek(new TopicPartition(TOPIC_NAME, 0), 10);
 73 
 74         // 从指定时间点开始消费
 75 //        List<PartitionInfo> partitionInfos = consumer.partitionsFor(TOPIC_NAME);
 76         /*
 77             从1小时前开始消费
 78             没有指定到时间的seek方法,最终都是找到offset偏移量去消费,所以这里其实是根据时间找offset。
 79          */
 80 //        long fetchDataTime = new Date().getTime() - 1000 * 60 * 60;
 81 //        Map<TopicPartition, Long> map = new HashMap<>();
 82 //        for (PartitionInfo partitionInfo : partitionInfos) {
 83 //            map.put(new TopicPartition(TOPIC_NAME, partitionInfo.partition()), fetchDataTime);
 84 //        }
 85 //        Map<TopicPartition, OffsetAndTimestamp> poMap = consumer.offsetsForTimes(map);
 86 //        for (Map.Entry<TopicPartition, OffsetAndTimestamp> entry : poMap.entrySet()) {
 87 //            TopicPartition key = entry.getKey();
 88 //            OffsetAndTimestamp value = entry.getValue();
 89 //            if (null == key || null == value) {
 90 //                continue;
 91 //            }
 92 //            long offset = value.offset();
 93 //            System.out.println("partition=" + key.partition() + ",offset=" + offset);
 94 //            // 根据timestamp确定offset
 95 //            consumer.assign(Arrays.asList(key));
 96 //            consumer.seek(key, offset); // 没有指定到时间的seek方法 最终都是找到offset偏移量去消费
 97 //        }
 98 
 99         while (true) {
100             // poll()方法是拉取消息的长轮询 如果1S内没有拉到消息会反复拉取 拉到后马上返回
101             ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
102             for (ConsumerRecord<String, String> record : records) {
103                 System.out.println("收到消息:partition=" + record.partition() + ",offset=" + record.offset() + ",key=" + record.key() + ",value=" + record.value());
104             }
105             /*
106                 拉一批消息一次处理完再同步提交 提交的offset会写到broker主题对应的分区中
107                 这种相对每次消费一条消息就提交一次offset效率要快很多
108                 相对自动提交也少了很多问题,尽管还可能会出现重复消费的情况,比如这一批消息中只消费了几条客户端就挂掉了,还没来得及提交offset,那么下次启动客户端还会消费那几条数据,导致重复消费。
109                 但是这种情况关系不大,因为一般核心的业务在消费端会保证它的幂等性。
110              */
111             if (records.count() > 0) {
112                 // 手动同步提交offset,当前线程会阻塞直到offset提交成功。一般使用同步提交,因为提交之后一般也没什么业务逻辑了。
113                 try {
114                     consumer.commitSync(); // 相对来说 手动同步提交使用较多
115                 } catch (Exception e) {
116                     consumer.commitSync(); // 提交失败 再次提交
117                     e.printStackTrace();
118                 }
119 
120                 // 手动异步提交offset,当前线程提交offset不会阻塞,可以继续处理后面的业务逻辑。
121                /* consumer.commitAsync((map, e) -> {
122                     if (null == e) {
123                         System.out.println("offset提交失败:" + map);
124                         e.printStackTrace();
125                     }
126                 });*/
127             }
128         }
129     }
130 
131 }

 

用到的一个简单实体类

 1 package com.kafka.base;
 2 
 3 /**
 4  * @description: 指令 用一个简单的实体类举例
 5  * @author: YangWanYi
 6  * @create: 2022-07-04 11:19
 7  **/
 8 public class Command {
 9     private String name;
10     private String detail;
11 
12     public Command() {
13     }
14 
15     public Command(String name, String detail) {
16         this.name = name;
17         this.detail = detail;
18     }
19 
20     public String getName() {
21         return name;
22     }
23 
24     public void setName(String name) {
25         this.name = name;
26     }
27 
28     public String getDetail() {
29         return detail;
30     }
31 
32     public void setDetail(String detail) {
33         this.detail = detail;
34     }
35 }

 

SpringBoot整合kafka

引入springboot的kafka依赖 

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>

 

配置application.yml 

server:
  port: 10003

spring:
  kafka:
    bootstrap-servers: 192.168.65.60:9999,192.168.65.60:9998
    producer:
      retries: 3 # 设置大于0的值,客户端会把发送失败的记录重新发送
      batch-size: 16384
      buffer-memory: 33554432
      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
      auto-offset-reset: earliest
      # 指定消息key和消息体的编解码方式
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    listener:
      # 当一条记录被消费者监听器ListenerConsumer处理之后提交
      # RECORD
      # 当每一批poll()的数据被消费者监听器ListenerConsumer处理之后提交
      # BATCH
      # 当每一批poll()的数据被消费者监听器ListenerConsumer处理之后,距离上次提交时间大于TIME时提交
      # TIME
      # 当每一批poll()的数据被消费者监听器ListenerConsumer处理之后,被处理记录数量大于等于COUNT时提交
      # COUNT
      # TIME或COUNT有一个条件满足时提交
      # COUNT_TIME
      # 当每一批poll()的数据被消费者监听器ListenerConsumer处理之后,手动调用Acknowledgment.acknowledge()后提交
      # MANUAL
      # 手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种
      # MANUAL_IMMEDIATE
      ack-mode: manual_immediate

 

 

 发送消息

 1 package com.example.springbootkafka;
 2 
 3 import org.springframework.beans.factory.annotation.Autowired;
 4 import org.springframework.kafka.core.KafkaTemplate;
 5 import org.springframework.web.bind.annotation.GetMapping;
 6 import org.springframework.web.bind.annotation.PathVariable;
 7 import org.springframework.web.bind.annotation.RestController;
 8 
 9 /**
10  * @description: kafka测试接口
11  * @author: YangWanYi
12  * @create: 2022-07-11 14:57
13  **/
14 @RestController
15 public class KafkaController {
16 
17     private final static String TOPIC_NAME = "TestTopicYWY";
18 
19     @Autowired
20     private KafkaTemplate<String, String> kafkaTemplate;
21 
22     @GetMapping("/sendMsg/{msg}")
23     public void sendMsg(@PathVariable("msg") String msg) {
24         // 更多API进入KafkaTemplate对象查看
25         kafkaTemplate.send(TOPIC_NAME, msg).addCallback(success -> System.out.println("消息发送成功:" + success.getRecordMetadata().topic() + "-" + success.getRecordMetadata().partition() + "-" + success.getRecordMetadata().offset() + "-" + msg),
26                 failure -> System.out.println("消息发送失败:" + failure.getMessage()));
27     }
28 
29 }

 

 消费消息 

 1 package com.example.springbootkafka;
 2 
 3 import org.apache.kafka.clients.consumer.ConsumerRecord;
 4 import org.springframework.kafka.annotation.KafkaListener;
 5 import org.springframework.kafka.support.Acknowledgment;
 6 import org.springframework.stereotype.Component;
 7 
 8 /**
 9  * @description: kafka消费端
10  * @author: YangWanYi
11  * @create: 2022-07-12 15:53
12  **/
13 @Component
14 public class KafkaConsumer {
15 
16 
17     /**
18      * 消费消息的常用注解[如果在yml中已配置相关参数,这里就不用配置了,如果yml和这里都配置了同样的参数,会优先使用这里的值,如果两处都没有配置,就使用默认值。]
19      *
20      * @param record
21      * @param ack
22      * @KafkaListener( groupId="testGroup", // 消费组名
23      * topicPartitions={
24      * @TopicPartition(topic="topic01",partitions={"0","1"}),
25      * @TopicPartition( topic="topic02",
26      * partitions="0",
27      * partitionOffsets=@PartitionOffset(partition="1",initialOffset="99")
28      * )
29      * },
30      * concurrency="6" // 指同消费组下的消费者个数,也就是并发消费数量,必须小于等于分区总数
31      * )
32      */
33     @KafkaListener(topics = "test-topic", groupId = "group1")
34     public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
35         System.out.println(record.value());
36         System.out.println(record);
37         ack.acknowledge(); // 手动提交offset
38     }
39 
40 
41     /**
42      * 配置多个消费组
43      * 一个消费组相当于队列
44      * 多个消费组相当于发布订阅
45      */
46     @KafkaListener(topics = "test-topic", groupId = "group2")
47     public void listen2Group(ConsumerRecord<String, String> record, Acknowledgment ack) {
48         System.out.println(record.value());
49         System.out.println(record);
50         ack.acknowledge(); // 手动提交offset
51     }
52 
53 }

 

 Kafka核心总控制器Controller

在kafka集群中会有一个或多个broker,其中有一个broker会被选举为控制器(Kafka Controller),它负责管理整个集群中所有分区和副本的状态。

当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本;

当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息;

当使用Kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责让新分区被其他节点感知到。

 

Controller选举机制

在kafka集群启动的时候,会自动选举一台broker作为controller来管理整个集群,选举的过程是集群中每个broker都会尝试在zookeeper上创建一个/controller临时节点,zookeeper会保证有且仅有一个broker能创建成功,这个broker就会成为集群的总控器controller。

当这个controller角色的broker宕机了,zookeeper临时节点/controller就会消失,集群里其他broker会一直监听这个临时节点,发现临时节点消失了就会竞争再次创建临时节点,zookeeper又会保证有一个broker成为新的controller,这就是controller选举机制。

(OS一下:真是处处充满竞争啊!就连kafka集群的一个总控器角色各个节点也要不断地竞争,没竞争上还会一直监听现有的总控器是不是挂了,挂了又开始竞争。-_-|| 太不容易了!一个节点都尚且如此拼搏!我们更不要当咸鱼,更不要躺平!兄弟姐妹们,加油!!ヾ(◍°∇°◍)ノ゙ )

具备控制器身份的broker比其他的broker要承担更多的职责,具体如下:

①、监听broker相关的变化,为zookeeper中的/brokers/ids/节点添加BrokerChangeListener,用来处理broker增减的变化;

②、监听topic相关的变化,为zookeeper中的/brokers/topics/节点添加TopicChangeListener,用来处理topic增减的变化,为zookeeper中的/admin/delete_topics/节点添加TopicDeletionListener,用来处理删除topic的动作;

③、从zookeeper中读取获取当前所有与topic、partition以及broker有关的信息并进行相应的管理,对于所有topic对应的zookeeper中的/brokers/topics/[topic]节点添加PartitionModificationsListener,用来监听topic中的分区分配变化;

④、更新集群的元数据信息,同步到其他普通的broker节点中。

 

Partition副本选举Leader机制

Controller感知到分区Leader所在的broker挂了,controller会从ISR列表(参数unclean.leader.election.enable=false的前提下)里挑第一个broker作为leader(第一个broker最先放进ISR列表中,可能是同步数据最多的副本),如果参数unclean.leader.election.enable=true,代表在ISR列表里所有副本都挂了的时候可以在ISR列表以外的副本中选leader,这种设置可以提高可用性,但是选出的新leader数据可能少很多。

副本进入ISR列表有两个条件:

①、副本节点不能产生分区,必须能与zookeeper保持会话,以及跟leader副本网络联通;

②、副本能复制leader上的所有写操作,兵器而不能落后太多。(与leader副本同步滞后的副本,是由replica.lag.time.max.ms配置决定的,超过这个时间还没有跟leader同步过一次的副本会被移除ISR列表。)

 

消费者消费消息的offset记录机制

每个consumer会定期把自己消费分区的offset提交给kafka内部topic:_consumer_offsets,提交过去的时候,key是consumerGroupId+topic+分区号,value就是当前offset的值。kafka会定期清理topic里的消息,最后就保留最新的那条数据。因为_consumer_offsets可能会接收高并发的请求,kafka默认给其分配50个分区(可以通过offsets.topic.num.partitions设置),这样可以通过加机器的方式扛大并发。

通过公式可以选出consumer消费的offset要提交到_consume_offsets的哪个分区:hash(consumerGroupId)%_consumer_offsets主题的分区数

 

消费者Rebalance机制

Rebalance就是说如果消费组里的消费者数量有变化或消费的分区数有变化,kafka会重新分配消费者消费分区的关系。比如consumer group中某个消费者挂了,此时会自动把分配给他的分区交给其他的消费者,如果这个挂掉的消费者又重启了,那么又会把一些分区重新交还给他。

需要注意的是:Rebalance只针对subscribe这种不指定分区消费的情况,如果通过assign这种消费方式指定了分区,kafka不会进行Rebalance。

以下这几种情况可能会触发消费者Rebalance:

①、消费组里的consumer增加或减少了;

②、动态给topic增加了分区;

③、消费组订阅了更多的topic。

Rebalance过程中,消费者无法从kafka消费消息,这对kafka的TPS有影响。如果kafka集群内节点较多,比如几百个,那Rebalance可能会耗时很多,所以应该尽量避免在系统高峰期发生Rebalance。

 

消费者Rebalance分区分配策略

主要有三种Rebalance的策略:range(按序号顺序分段分区)、round-robin(轮询)、sticky。

Kafka提供了消费者客户端参数partition.assignment.strategy来设置消费者与订阅主题之间的分区分配策略,默认为range分配策略。

补充中……

posted @ 2022-07-01 16:00  敲代码的小浪漫  阅读(168)  评论(0编辑  收藏  举报