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分配策略。
补充中……