Kafka
概述
定义
Kafka传统定义:Kafka是一个分布式的基于发布/订阅模式的消息队列(Message Queue),主要应用于大数据实时处理领域。
发布/订阅:消息的发布者不会将消息直接发送给特定的订阅者,而是将发布的消息分为不同的类别,订阅者只接收感兴趣的消息
Kafka最新定义:Kafka 是一个开源的分布式事件流平台( Event Streaming Platform),被数千家公司用于高性能数据管道、流分析、数据集成和关键任务应用
消息队列
目前企业中比较常见的消息队列产品主要有 Kafka 、ActiveMQ 、RabbitMQ 、RocketMQ 等。在大数据场景主要采用 Kafka 作为消息队列。在 JavaEE 开发中主要采用 ActiveMQ、RabbitMQ、RocketMQ。
应用场景
传统的消息队列的主要应用场景包括:缓存/消峰、解耦和异步通信。
两种模式
基础架构
快速入门
配置文件
#broker 的全局唯一编号,不能重复,只能是数字。
broker.id=0
#处理网络请求的线程数量
num.network.threads=3
#用来处理磁盘 IO 的线程数量
num.io.threads=8
#发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
#接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
#请求套接字的缓冲区大小
socket.request.max.bytes=104857600
#kafka 运行日志(数据)存放的路径,路径不需要提前创建,kafka 自动帮你创建,可以配置多个磁盘路径,路径与路径之间可以用","分隔
log.dirs=/opt/module/kafka/datas
#topic 在当前 broker 上的分区个数
num.partitions=1
#用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
# 每个 topic 创建时的副本数,默认时 1 个副本
offsets.topic.replication.factor=1
#segment 文件保留的最长时间,超时将被删除
log.retention.hours=168
#每个 segment 文件的大小,默认最大 1G
log.segment.bytes=1073741824
#检查过期数据的时间,默认 5 分钟检查一次是否数据过期
log.retention.check.interval.ms=300000
#配置连接Zookeeper 集群地址,hadoop是虚拟机的ip别名(在 zk 根目录下创建/kafka,方便管理) zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181/kafka
命令行操作
主题
查看操作主题命令参数:bin/kafka-topics.sh
参数 | 描述 |
---|---|
–bootstrap-server <String: server toconnect to> | 连接的 Kafka Broker 主机名称和端口号。 |
–topic <String: topic> | 操作的 topic 名称。 |
–create | 创建主题。 |
–delete | 删除主题。 |
–alter | 修改主题。 |
–list | 查看所有主题。 |
–describe | 查看主题详细描述。 |
–partitions <Integer: # of partitions> | 设置分区数。分区数只能增加 |
–replication-factor<Integer: replication factor> | 设置分区副本。 |
–config <String: name=value> | 更新系统默认的配置。 |
生产者
查看操作主题命令参数:bin/kafka-console-producer.sh
参数 | 描述 |
---|---|
–bootstrap-server <String: server toconnect to> | 连接的 Kafka Broker 主机名称和端口号。 |
–topic <String: topic> | 操作的 topic 名称。 |
连接上集群后就可以直接输入要发送的消息
消费者
查看操作主题命令参数:bin/kafka-console-consumer.sh
参数 | 描述 |
---|---|
–bootstrap-server <String: server toconnect to> | 连接的 Kafka Broker 主机名称和端口号。 |
–topic <String: topic> | 操作的 topic 名称。 |
–from-beginning | 从头开始消费。 |
–group <String: consumer group id> | 指定消费者组名称。 |
生产者
消息发送流程
发送原理
在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程。在 main 线程中创建了一个双端队列RecordAccumulator。main 线程将消息发送给 RecordAccumulator, Sender 线程不断从RecordAccumulator 中拉取消息发送到Kafka Broker
重要参数列表
参数名称 | 描述 |
---|---|
bootstrap.servers | 生产者连接集群所需的 broker 地址清单。 例如hadoop102:9092,hadoop103:9092,hadoop104:9092,可以设置 1 个或者多个,中间用逗号隔开。注意这里并非需要所有的 broker 地址,因为生产者从给定的 broker里找到其他broker信息 |
key.serializer 和 value.serializer | 指定发送消息的 key 和 value 的序列化类型。一定要写 |
buffer.memory | RecordAccumulator 缓冲区总大小,默认 32m。 |
batch.size | 缓冲区一批数据最大值,默认 16k。适当增加该值,可以提高吞吐量,但是如果该值设置太大,会导致数据传输延迟增加。 |
linger.ms | 如果数据迟迟未达到 batch.size,sender 等待 linger.time 之后就会发送数据。单位 ms,默认值是 0ms,表示没有延迟。生产环境建议该值大小为 5-100ms 之间。 |
acks | 0:生产者发送过来的数据,不需要等数据落盘应答。 1:生产者发送过来的数据,Leader 收到数据后应答。 -1(all):生产者发送过来的数据,Leader+和 isr 队列里面的所有节点收齐数据后应答。默认值是-1,-1 和all 是等价的。 |
max.in.flight.requests.per.connection | 允许最多没有返回 ack 的次数,默认为 5,开启幂等性 要保证该值是 1-5 的数字。 |
retries | 当消息发送出现错误的时候,系统会重发消息。retries 表示重试次数。默认是 int 最大值,2147483647。 如果设置了重试,还想保证消息的有序性,需要设置 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1 否则在重试此失败消息的时候,其他的消息可能发送成功了。 |
retry.backoff.ms | 两次重试之间的时间间隔,默认是 100ms。 |
enable.idempotence | 是否开启幂等性,默认 true,开启幂等性。 |
compression.type | 生产者发送的所有数据的压缩方式。默认是 none,也就是不压缩。 支持压缩类型:none、gzip、snappy、lz4 和 zstd。 |
异步发送API
-
导入依赖
<dependencies> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>3.0.0</version> </dependency> </dependencies>
不带回调函数
public class CustomProducer {
public static void main(String[] args) throws InterruptedException {
// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 2. 给 kafka 配置对象添加配置信息: bootstrap.servers
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092");
// key,value 序列化(必须): key.serializer, value.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
// 4. 调用 send 方法,发送消息
for (int i = 0; i < 5; i++) {
kafkaProducer.send(new ProducerRecord<>("first","atguigu " + i));
}
// 5. 关闭资源
kafkaProducer.close();
}
}
带回调函数
回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是元数据信息(RecordMetadata) 和异常信息(Exception),如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败。
消息发送失败会自动重试,不需要我们在回调函数中手动重试
public class CustomProducerCallback {
public static void main(String[] args) throws InterruptedException {
// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 2. 给 kafka 配置对象添加配置信息
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
// key,value 序列化(必须): key.serializer, value.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
// 4. 调用 send 方法,发送消息
for (int i = 0; i < 5; i++) {
// 添加回调
kafkaProducer.send(new ProducerRecord<>("first", "atguigu " + i), new Callback() {
// 该方法在 Producer 收到 ack 时调用,为异步调用
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
// 没有异常,输出信息到控制台
System.out.println(" 主 题 : " + metadata.topic() + "->" + "分区: " + metadata.partition());
} else {
// 出现异常打印
exception.printStackTrace();
}
}
});
// 延迟一会会看到数据发往不同分区
Thread.sleep(2);
}
// 5. 关闭资源
kafkaProducer.close();
}
}
同步发送API
public class CustomProducerSync {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 2. 给 kafka 配置对象添加配置信息
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");
// key,value 序列化(必须): key.serializer, value.serializer
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
// 4. 调用 send 方法,发送消息
for (int i = 0; i < 10; i++) {
// 异步发送 默认
// kafkaProducer.send(new
ProducerRecord<>("first","kafka" + i));
// 同步发送,其实就是多了一个get()
kafkaProducer.send(new ProducerRecord<>("first","kafka" + i)).get();
}
// 5. 关闭资源
kafkaProducer.close();
}
}
生产者分区
好处
- 便于合理使用存储资源。每个partition在一个broker上存储,可以把海量的数据按照分区切割成一块一块数据存储在堕胎broker上。合理控制分区的任务,可以实现负载均衡的效果
- 提高并行度。生产者可以以分区为单位发送数据,消费者可以以分区为单位进行消费数据
分区策略
自定义分区器
-
定义类实现
Partitioner
接口public class MyPartitioner implements Partitioner { @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { // 获取消息 String msgValue = value.toString(); // 创建 partition int partition; // 判断消息是否包含 xxx if (msgValue.contains("xxx")){ partition = 0; }else { partition = 1; } // 返回分区号 return partition; } // 关闭资源 @Override public void close() {} // 配置方法 @Override public void configure(Map<String, ?> configs) {}
-
在配置中添加分区器参数
public class CustomProducerCallbackPartitions { public static void main(String[] args) throws InterruptedException { Properties properties = new Properties(); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092"); properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 添加自定义分区器 properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"自定义分区器的全类名"); KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties); for (int i = 0; i < 5; i++) { kafkaProducer.send(new ProducerRecord<>("first", "xxx " + i), new Callback() { @Override public void onCompletion(RecordMetadata metadata, Exception e) { if (e == null){ System.out.println(" 主 题 : " + metadata.topic() + "->" + "分区: " + metadata.partition()); }else { e.printStackTrace(); } } }); } kafkaProducer.close(); } }
生产经验
提高吞吐量
// 在kafka的配置信息里使用如下参数
// batch.size: 批次大小, 默认 16K
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
// linger.ms: 等待时间,默认 0
properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);
// RecordAccumulator: 缓冲区大小, 默认 32M: buffer.memory
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
// compression.type: 压缩,默认 none,可配置值 gzip、 snappy、lz4 和 zstd
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");
数据可靠性
// 设置 acks
properties.put(ProducerConfig.ACKS_CONFIG, "all");
// 重试次数 retries,默认是 int 最大值, 2147483647
properties.put(ProducerConfig.RETRIES_CONFIG, 3);
数据去重
数据传递语义
幂等性
开启参数 enable.idempotence 默认为 true, false 关闭。
生产者事务
public class CustomProducerTransactions {
public static void main(String[] args) throws InterruptedException {
// 1. 创建 kafka 生产者的配置对象
Properties properties = new Properties();
// 2. 给 kafka 配置对象添加配置信息
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092");
// key,value 序列化
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
// 设置事务 id(必须),事务 id 任意起名
properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction_id_0");
// 3. 创建 kafka 生产者对象
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);
// 初始化事务
kafkaProducer.initTransactions();
// 开启事务
kafkaProducer.beginTransaction();
try {
// 4. 调用 send 方法,发送消息
for (int i = 0; i < 5; i++) {
// 发送消息
kafkaProducer.send(new ProducerRecord<>("first","xxx " + i));
}
// int i = 1 / 0;
// 提交事务
kafkaProducer.commitTransaction();
} catch (Exception e) {
// 终止事务
kafkaProducer.abortTransaction();
} finally {
// 5. 关闭资源
kafkaProducer.close();
}
}
}
数据有序
数据乱序
Broker
工作流程
zookeeper存储的Kafka信息
- 启动zookeeper
- 通过ls命令查看:
ls /kafka
broker总体工作流程
生产经验
服役新节点
-
新节点准备(克隆虚拟机)
-
关闭被克隆虚拟机,执行克隆操作
-
开启新虚拟机,修改ip地址
vim /etc/sysconfig/network-scripts/ifcfgens33
-
在新虚拟机上,修改主机名称
vim /etc/hostname
-
重新启动
-
修改新虚拟机中kafka的broker.id
-
删除新虚拟机中kafka下的datas和log文件夹
-
启动原先的kafka集群
-
单独启动新虚拟机的kafka
-
-
执行负载均衡操作
-
创建一个需要负载均衡的主题
vim topics-to-move.json { "topics": [ {"topic": "first"} ], "version": 1 }
-
生成一个负载均衡的计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2,3" --generate 现在希望使用0,1,2,3来负载均衡 topic:first
-
创建副本存储计划(所有副本存储在每个kafka中)
vim increase-replication-factor.json 将上一步的执行结果放在这里
-
执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute
-
验证副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --verify
-
退役旧节点
-
执行负载均衡操作
先按照退役一台节点,生成执行计划,然后按照服役时操作流程执行负载均衡。具体步骤同:服役新节点的第二步
-
执行停止命令
直接在退役节点执行停止命令
bin/kafka-server-stop.sh
Kafka副本
基本信息
-
作用:提高数据可靠性
-
Kafka默认副本是1个,生产环境一般配置为2个,保证数据可靠性;太多副本会增加磁盘存储空间,增加网上数据传输,降低效率。
-
Kafka 中副本分为: Leader 和 Follower。 Kafka 生产者只会把数据发往 Leader,然后 Follower 找 Leader 进行同步数据。
-
Kafka 分区中的所有副本统称为 AR(Assigned Repllicas)
AR = ISR + OSR
ISR:表示和 Leader 保持同步的 Follower 集合。 如果 Follower 长时间未向 Leader 发送通信请求或同步数据,则该 Follower 将被踢出 ISR。该时间阈值由 replica.lag.time.max.ms参数设定,默认 30s。 Leader 发生故障之后,就会从 ISR 中选举新的 Leader
OSR:表示followers与leader副本同步时,延迟过多的副本
leader选举流程
leader和follower故障处理细节
follower故障
leader故障
生产经验
手动调整分区副本存储
-
创建一个topic,4个分区,2个副本
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 4 --replication-factor 2 --topic three
-
查看分区副本存储情况
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three
-
创建副本存储计划,指定所有副本都存储在0,1节点
vim increase-replication-factor.json 输入以下内容: { "version":1, "partitions":[ {"topic":"three","partition":0,"replicas":[0,1]}, {"topic":"three","partition":1,"replicas":[0,1]}, {"topic":"three","partition":2,"replicas":[1,0]}, {"topic":"three","partition":3,"replicas":[1,0]} ] }
-
执行副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute
-
验证副本存储计划
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --verify
-
查看分区副本存储情况
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three
leader partition负载均衡
auto.leader.rebalance.enable在生产环境中一般设置为false,因为leader重选举的代价比较大,可能会带来性能影响
增加副本因子
不能直接通过命令行的方式来修改,需要创建副本存储计划
- 创建副本存储计划
vim increase-replication-factor.json
输入以下内容:
{
"version":1,
"partitions":[
{"topic":"four","partition":0,"replicas":[0,1,2]},
{"topic":"four","partition":1,"replicas":[0,1,2]},
{"topic":"four","partition":2,"replicas":[0,1,2]}
]
}
-
执行
bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute
文件存储
文件存储机制
topic数据的存储机制
使用工具查看index和log信息
kafka-run-class.sh kafka.tools.DumpLogSegments --files ./00000000000000000000.index
index文件和log文件详解
文件清理策略
Kafka 中默认的日志保存时间为 7 天,可以通过调整如下参数修改保存时间
- log.retention.hours, 最低优先级小时,默认 7 天
- log.retention.minutes, 分钟
- log.retention.ms, 最高优先级毫秒
- log.retention.check.interval.ms, 负责设置检查周期,默认 5 分钟
Kafka 中提供的日志清理策略有 delete 和 compact 两种
-
delete:删除
log.cleanup.policy = delete 所有数据启用删除策略
-
基础时间:默认打开。 以 segment 中所有记录中的最大时间戳作为该文件时间戳
如果一个segment中有一部分数据过期,一部分数据没有过期,则会以segment所有记录中最大的时间戳作为该文件的时间戳来进行判断是否需要删除
-
基于大小:默认关闭。超过设置的所有日志总大小,删除最早的 segment。
log.retention.bytes
,默认等于-1,表示无穷大
-
-
compact:压缩
对于相同的key,不同的value只会保留最后一个版本
log.cleanup/policy = compact
所有数据启用压缩策略
高效读写
-
Kafka本身是分布式集群,可以采用分区技术,并行度高
-
读数据采用稀疏索引,可以快速定位要消费的数据
-
顺序写磁盘。Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写,省去了大量磁头寻址的时间
-
页缓存+零拷贝技术
消费者
消费方式
工作流程
总体工作流程
消费者组原理
初始化流程
详细消费流程
消费者API
独立消费者案例(订阅主题)
在消费者 API 代码中必须配置消费者组 id。 命令行启动消费者不填写消费者组id 会被自动填写随机的消费者组 id
public class CustomConsumer {
public static void main(String[] args) {
// 1.创建消费者的配置对象
Properties properties = new Properties();
// 2.给消费者配置对象添加参数
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092");
// 配置序列化 必须
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
// 配置消费者组(组名任意起名) 必须
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// 3.创建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
// 4.注册要消费的主题(可以消费多个主题)
ArrayList<String> topics = new ArrayList<>();
topics.add("first");
kafkaConsumer.subscribe(topics);
// 拉取数据打印
while (true) {
// 设置 1s 中消费一批数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
// 5.打印消费到的数据
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
}
独立消费者案例(订阅分区)
public class CustomConsumerPartition {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092");
// 配置序列化 必须
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
// 配置消费者组(必须),名字可以任意起
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test");
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
// 消费某个主题的某个分区数据
ArrayList<TopicPartition> topicPartitions = new ArrayList<>();
topicPartitions.add(new TopicPartition("first", 0));
kafkaConsumer.assign(topicPartitions);
while (true){
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
}
消费者组案例
// 复制一份之前写的消费者,消费者组id配置为相同即可
public class CustomConsumer1 {
public static void main(String[] args) {
// 1.创建消费者的配置对象
Properties properties = new Properties();
// 2.给消费者配置对象添加参数
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092");
// 配置序列化 必须
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
// 配置消费者组(组名任意起名) 必须
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
// 3.创建消费者对象
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(properties);
// 4.注册要消费的主题(可以消费多个主题)
ArrayList<String> topics = new ArrayList<>();
topics.add("first");
kafkaConsumer.subscribe(topics);
// 拉取数据打印
while (true) {
// 设置 1s 中消费一批数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));
// 5.打印消费到的数据
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.println(consumerRecord);
}
}
}
}
生产经验
Kafka默认的分区策略是:Rang+CooperativeSticky
Range以及再平衡
先算出每个消费者需要消费几个分区,然后再按顺序分配
-
停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)
1 号消费者:消费到 3、 4 号分区数据
2 号消费者:消费到 5、 6 号分区数据
0 号消费者的任务会整体被分配到 1 号消费者或者 2 号消费者
0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行
-
再次重新发送消息观看结果(45s 以后)
1 号消费者:消费到 0、 1、 2、 3 号分区数据
2 号消费者:消费到 4、 5、 6 号分区数据
消费者 0 已经被踢出消费者组,所以重新按照 range 方式分配。
RoundRobin以及再平衡
// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,"org.apache.kafka.clients.consumer.RoundRobinAssignor");
按照消费者的顺序,一个一个的轮询并分配
-
停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)
1 号消费者:消费到 2、 5 号分区数据
2 号消费者:消费到 4、 1 号分区数据
0 号消费者的任务会按照 RoundRobin 的方式,把数据轮询分成 0 、 6 和 3 号分区数据,分别由 1 号消费者或者 2 号消费者消费0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行
-
再次重新发送消息观看结果(45s 以后)
1 号消费者:消费到 0、 2、 4、 6 号分区数据
2 号消费者:消费到 1、 3、 5 号分区数据消费者 0 已经被踢出消费者组,所以重新按照 RoundRobin 方式分配
Sticky以及再平衡
粘性分区定义: 可以理解为分配的结果带有“粘性的”。即在执行一次新的分配之前,考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销。粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略, 首先会尽量均衡的放置分区到消费者上面,在出现同一消费者组内消费者出现问题的时候,会尽量保持原有分配的分区不变化
// 修改分区分配策略
properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,"org.apache.kafka.clients.consumer.StickyAssignor");
-
停止掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)
1 号消费者:消费到 2、 5、 3 号分区数据。
2 号消费者:消费到 4、 6 号分区数据。
0 号消费者的任务会按照粘性规则,尽可能均衡的随机分成 0 和 1 号分区数据,分别由 1 号消费者或者 2 号消费者消费0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务分配给其他 broker 执行
-
再次重新发送消息观看结果(45s 以后)
1 号消费者:消费到 2、 3、 5 号分区数据。
2 号消费者:消费到 0、 1、 4、 6 号分区数据消费者 0 已经被踢出消费者组,所以重新按照粘性方式分配
消费者事务
如果想完成consumer端的精准一次消费,那么需要Kafka消费端将消费过程和提交offset过程做原子绑定。此时我们需要将Kafka的保存到支持事务的自定义介质(比如MySQL)
数据积压
offset位移
默认的维护位置
从0.9版本开始,consumer默认将offset保存在Kafka一个内置的topic中,该topic为__consumer_offsets
,之前都是存放在zookeeper当中,这样会增加zookeeper的访问压力
__consumer_offsets
主题里面采用 key 和 value 的方式存储数据。 key 是 group.id+topic+分区号
, value 就是当前 offset 的值。 每隔一段时间, kafka 内部会对这个 topic 进行compact,也就是每个 group.id+topic+分区号
就保留最新数据
自动提交offset
相关参数:
enable.auto.commit
:是否开启自动提交,默认是trueauto.commit.interval.ms
:自动提交的时间间隔
// 是否自动提交 offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
// 提交 offset 的时间周期 1000ms,默认 5s
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,1000);
手动提交offset
手动提交offset的方法有两种:commitSync和commitAsync。两者相同的点在于都会将本次提交的一批数据最高的偏移量提交;不同的是,同步提交阻塞当前线程,并且会自动失败重试。必须等到offset提交完毕才能去消费下一批数据
// 是否自动提交 offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
// 同步提交 offset
consumer.commitSync();
// 是否自动提交 offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");
// 异步提交 offset
consumer.commitAsync();
指定offset消费
auto.offset.reset = earliest | latest | none
, 默认是 latest。
当 Kafka 中没有初始偏移量(消费者组第一次消费)或服务器上不再存在当前偏移量
时(例如该数据已被删除),该怎么办?
-
earliest:自动将偏移量重置为最早的偏移量,
--from-beginning
-
latest(默认值):自动将偏移量重置为最新偏移量
-
none:如果未找到消费者组的先前偏移量,则向消费者抛出异常
-
任意指定 offset 位移开始消费
// 省略前面的配置信息等.... Set<TopicPartition> assignment= new HashSet<>(); while (assignment.size() == 0) { kafkaConsumer.poll(Duration.ofSeconds(1)); // 获取消费者分区分配信息(有了分区分配信息才能开始消费) assignment = kafkaConsumer.assignment(); } // 遍历所有分区,并指定 offset 从 1700 的位置开始消费 for (TopicPartition tp: assignment) { kafkaConsumer.seek(tp, 1700); } // 省略后续的消费操作...
指定时间消费
// 省略前面的配置信息等....
Set<TopicPartition> assignment = new HashSet<>();
while (assignment.size() == 0) {
kafkaConsumer.poll(Duration.ofSeconds(1));
// 获取消费者分区分配信息(有了分区分配信息才能开始消费)
assignment = kafkaConsumer.assignment();
}
HashMap<TopicPartition, Long> timestampToSearch = new HashMap<>();
// 封装集合存储,每个分区对应一天前的数据
for (TopicPartition topicPartition : assignment) {
timestampToSearch.put(topicPartition, System.currentTimeMillis() - 1 * 24 * 3600 * 1000);
}
// 获取从 1 天前开始消费的每个分区的 offset
Map<TopicPartition, OffsetAndTimestamp> offsets = kafkaConsumer.offsetsForTimes(timestampToSearch);
// 遍历每个分区,对每个分区设置消费时间。
for (TopicPartition topicPartition : assignment) {
OffsetAndTimestamp offsetAndTimestamp = offsets.get(topicPartition);
// 根据时间指定开始消费的位置
if (offsetAndTimestamp != null){
kafkaConsumer.seek(topicPartition, offsetAndTimestamp.offset());
}
}
// 省略后续的消费操作...
漏消费和重复消费分析
重复消费: 已经消费了数据,但是 offset 没提交。
漏消费: 先提交 offset 后消费,有可能会造成数据的漏消费
Kafka-Kraft模式
左图为 Kafka 现有架构, 元数据在 zookeeper 中, 运行时动态选举 controller, 由controller 进行 Kafka 集群管理。 右图为 kraft 模式架构(实验性), 不再依赖 zookeeper 集群,而是用三台 controller 节点代替 zookeeper, 元数据保存在 controller 中, 由 controller 直接进行 Kafka 集群管理。这样做的好处有以下几个:
- Kafka 不再依赖外部框架, 而是能够独立运行;
- controller 管理集群时, 不再需要从 zookeeper 中先读取数据, 集群性能上升;
- 由于不依赖 zookeeper, 集群扩展时不再受到 zookeeper 读写能力限制;
- controller 不再动态选举, 而是由配置文件规定。 这样我们可以有针对性的加强controller 节点的配置, 而不是像以前一样对随机 controller 节点的高负载束手无策
集成SpringBoot
生产者
-
配置Kafka相关信息
# 指定 kafka 的地址 spring.kafka.bootstrapservers=hadoop102:9092,hadoop103:9092,hadoop104:9092 #指定 key 和 value 的序列化器 spring.kafka.producer.keyserializer=org.apache.kafka.common.serialization.StringSerializer spring.kafka.producer.valueserializer=org.apache.kafka.common.serialization.StringSerializer
-
创建接口接受请求
@RestController public class ProducerController { // Kafka 模板用来向 kafka 发送数据 @Autowired KafkaTemplate<String, String> kafka; @RequestMapping("/test") public String data(String msg) { kafka.send("first", msg); return "ok"; } }
消费者
-
配置Kafka相关信息
# 指定 kafka 的地址 spring.kafka.bootstrapservers=hadoop102:9092,hadoop103:9092,hadoop104:9092 # 指定 key 和 value 的反序列化器 spring.kafka.consumer.keydeserializer=org.apache.kafka.common.serialization.StringDeserializer spring.kafka.consumer.valuedeserializer=org.apache.kafka.common.serialization.StringDeserializer #指定消费者组的 group_id spring.kafka.consumer.group-id=test
-
创建类消费Kafka中指定的topic的数据
@Configuration public class KafkaConsumer { // 指定要监听的 topic @KafkaListener(topics = "first") public void consumeTopic(String msg) { // 参数: 收到的 value System.out.println("收到的信息: " + msg); } }
创建接口接受请求
@RestController
public class ProducerController {
// Kafka 模板用来向 kafka 发送数据
@Autowired
KafkaTemplate<String, String> kafka;
@RequestMapping("/test")
public String data(String msg) {
kafka.send("first", msg);
return "ok";
}
}
消费者
-
配置Kafka相关信息
# 指定 kafka 的地址 spring.kafka.bootstrapservers=hadoop102:9092,hadoop103:9092,hadoop104:9092 # 指定 key 和 value 的反序列化器 spring.kafka.consumer.keydeserializer=org.apache.kafka.common.serialization.StringDeserializer spring.kafka.consumer.valuedeserializer=org.apache.kafka.common.serialization.StringDeserializer #指定消费者组的 group_id spring.kafka.consumer.group-id=test
-
创建类消费Kafka中指定的topic的数据
@Configuration public class KafkaConsumer { // 指定要监听的 topic @KafkaListener(topics = "first") public void consumeTopic(String msg) { // 参数: 收到的 value System.out.println("收到的信息: " + msg); } }