消费者
Kafka 消费者从属于消费者群组。一个群组里的消费者订阅的是同一个主题,每个消费者接收主题一部分分区的消息。同一个主题下的分区只能被同一个消费组下的一个消费者消费,不同消费组之间互不影响。
分区重分配
在主题添加分区,消费者突然下线,离开群组,发生分区重分配
再均衡
分区的所有权从一个消费者转移到另一个消费者,这样的行为称为再平衡。
再均衡发生时,消费者无法读取消息,避免不必要的在均衡。
触发时机
- 消费者向组协调器的broker发送心跳来维持群组的从属关系和分区的所有权关系,只要以正常的时间发送心跳,则认为是活跃的,如果超时,则会触发一次再均衡
- 一个消费者发生崩溃,并停止读取消息,群组协调器会等待几秒钟,确认它死亡了才触发再均衡
- 在清理消费者时,消费者会通知协调器它将要离开群组,协调器会立即触发一次再均衡
分配分区
消费者加入群组,向组协调器发送JoinGroup请求,第一个加入的消费者成为”群主“,群主从协调器那里获得群组的成员列表,并负责给每一个消费者分配分区。
消费者消费消息-Coding
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.Collections;
import java.util.Properties;
public class MessageConsumer {
public static void main(String[] args) {
Properties consumerConfig = new Properties();
// bootstrap.servers
consumerConfig.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-node01:9092,kafka-node02:9092,kafka-node03:9092,");
// 设置key的序列化器
consumerConfig.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 设置value的序列化器
consumerConfig.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// group id
consumerConfig.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "group-test");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerConfig);
consumer.subscribe(Collections.singletonList("test-vip"));
while (true){
// 控制poll() 方法的超时时间,该参数被设为0,poll() 会立即返回,否则它会在指定的毫秒数内一直等待broker 返回数据
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100L));
for (ConsumerRecord<String, String> record : records) {
String data = String.format("topic = %s, partition = %s, offset = %d, key = %s, value = %s\n",
record.topic(), record.partition(), record.offset(), record.key(), record.value());
System.out.println(data);
}
}
}
}
消费者参数配置
-
fetch.min.bytes: 指定了消费者从服务器获取记录的最小字节数,broker 在收到消费者的数据请求时,
如果可用的数据量小于fetch.min.bytes 指定的大小,那么它会等到有足够的可用数据时才把它返回给消费者 -
fetch.max.wait.ms:通过fetch.min.bytes 告诉Kafka,等到有足够的数据时才把它返回给消费者,默认是500ms,与fetch.min.bytes相互影响
-
max.partition.fetch.bytes:指定了服务器从每个分区里返回给消费者的最大字节数。它的默认值是1MB。
-
session.timeout.ms:指定了消费者在被认为死亡之前可以与服务器断开连接的时间,如果消费者没有在session.timeout.ms 指定的时间内发送心跳给群组协调器,就被认为已经死亡,协调器就会触发再均衡,把它的分区分配给群组里的其他消费者
-
heartbeat.interval.ms: 指定了poll() 方法向协调器发送心跳的频率,session.timeout.ms 则指定了消费者可以多久不发送心跳。所以,一般需要同时修改这两个属性
-
auto.offset.reset: 指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下(因消费者长
时间失效,包含偏移量的记录已经过时并被删除)如何处理。可选值:latest,earliest -
enable.auto.commit: 是否自动提交偏移量
-
auto.commit.interval.ms: 自动提交频率
-
partition.assignment.strategy: 分区分配策略
-
client.id:该属性可以是任意字符串,broker 用它来标识从客户端发送过来的消息,通常被用在日志、
度量指标和配额里 -
max.poll.records:该属性用于控制单次调用poll() 方法能够返回的记录数量
-
receive.buffer.bytes 和 send.buffer.bytes: socket 在读写数据时用到的TCP 缓冲区也可以设置大小。如果它们被设为-1,就使用操作系统的默认值
提交和偏移量
提交:消费者更新当前位置的操作
- __consumer_offset主题保存着每个分区的偏移量
- 如果提交的偏移量小于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息就会被重复处理
- 如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的消息将会丢失
提交方式
-
自动提交:
- enable.auto.commit : true
- auto.commit.interval.ms: 提交时间间隔
- 自动提交:每次调用轮询方法会把上一次调用返回的偏移量提交上去
问题:
-
重复消费
假设每5s提交一次,在最近一次提交之后的3s中发生再均衡,再均衡之后,从最后一次提交的偏移量拉取,就会被重复处理
-
同步提交
- auto.commit.offset 设为false
- 调用commitSync() 方法提交当前批次最新的偏移量
问题:再均衡的情况下也会发生重复消费
-
异步提交
- auto.commit.offset 设为false
- 调用commitAsync()
在成功提交或碰到无法恢复的错误之前,commitSync() 会一直重试,但是commitAsync()不会
- 组合提交
提交代码-Coding
public class MessageCommitConsumer {
public static void main(String[] args) {
Properties consumerConfig = new Properties();
// bootstrap.servers
consumerConfig.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-node01:9092,kafka-node02:9092,kafka-node03:9092,");
// 设置key的序列化器
consumerConfig.setProperty(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// 设置value的序列化器
consumerConfig.setProperty(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// group id
consumerConfig.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "group-test");
// 关闭自动提交
consumerConfig.setProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerConfig);
consumer.subscribe(Collections.singletonList("test-vip"));
// ================同步提交==========================
executeCommitSync(consumer);
// ================异步提交==========================
executeCommitAsync(consumer);
// ================组合提交==========================
executeCombinationCommit(consumer);
// ================提交特定偏移量==========================
executeSpecialCommit(consumer);
}
private static void executeSpecialCommit(KafkaConsumer<String, String> consumer) {
HashMap<TopicPartition, OffsetAndMetadata> tp = new HashMap<>();
int count = 0;
while (true) {
// 控制poll() 方法的超时时间,该参数被设为0,poll() 会立即返回,否则它会在指定的毫秒数内一直等待broker 返回数据
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100L));
for (ConsumerRecord<String, String> record : records) {
tp.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset() + 1, "metadata"));
String data = String.format("topic = %s, partition = %s, offset = %d, key = %s, value = %s\n",
record.topic(), record.partition(), record.offset(), record.key(), record.value());
System.out.println(data);
}
// 正常,异步提交
if (count % 1000 == 0) {
// 中间提交偏移量
consumer.commitAsync(tp,null);
}
count++;
}
}
private static void executeCombinationCommit(KafkaConsumer<String, String> consumer) {
try {
while (true) {
// 控制poll() 方法的超时时间,该参数被设为0,poll() 会立即返回,否则它会在指定的毫秒数内一直等待broker 返回数据
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100L));
for (ConsumerRecord<String, String> record : records) {
String data = String.format("topic = %s, partition = %s, offset = %d, key = %s, value = %s\n",
record.topic(), record.partition(), record.offset(), record.key(), record.value());
System.out.println(data);
}
// 正常,异步提交
consumer.commitAsync();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 重试
consumer.commitSync();
} finally {
consumer.close();
}
}
}
private static void executeCommitAsync(KafkaConsumer<String, String> consumer) {
while (true) {
// 控制poll() 方法的超时时间,该参数被设为0,poll() 会立即返回,否则它会在指定的毫秒数内一直等待broker 返回数据
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100L));
for (ConsumerRecord<String, String> record : records) {
String data = String.format("topic = %s, partition = %s, offset = %d, key = %s, value = %s\n",
record.topic(), record.partition(), record.offset(), record.key(), record.value());
System.out.println(data);
}
try {
// 带有回调
consumer.commitAsync((offsets, exception) -> {
if (exception != null) {
exception.printStackTrace();
}
});
} catch (CommitFailedException e) {
e.printStackTrace();
}
}
}
private static void executeCommitSync(KafkaConsumer<String, String> consumer) {
while (true) {
// 控制poll() 方法的超时时间,该参数被设为0,poll() 会立即返回,否则它会在指定的毫秒数内一直等待broker 返回数据
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100L));
for (ConsumerRecord<String, String> record : records) {
String data = String.format("topic = %s, partition = %s, offset = %d, key = %s, value = %s\n",
record.topic(), record.partition(), record.offset(), record.key(), record.value());
System.out.println(data);
}
try {
consumer.commitSync();
} catch (CommitFailedException e) {
e.printStackTrace();
}
}
}
}