生产者
- ACK + 重试机制
-
生产者生产数据写入kafka,等待kafka返回ack确认,收到ack,生产者发送下一条
-
选项
-
0:不等待ack,直接发送下一条
- 优点:快
- 缺点:数据易丢失
-
1:生产者将数据写入Kafka,Kafka等待这个分区Leader副本,返回ack,发送下一条
- 优点:性能和安全做了中和的选项
- 缺点:依旧存在一定概率的数据丢失的情况
-
all:生产者将数据写入Kafka,Kafka等待这个分区所有副本同步成功,返回ack,发送下一条
- 优点:安全
- 缺点:性能比较差
- 方案:搭配min.insync.replicas来使用
- min.insync.replicas:表示最少同步几个副本就可以返回ack
-
重试机制
retries = 0 发送失败的重试次数
-
-
消费者
- 基础规则:每个消费者消费Topic分区的数据按照Offset进行消费
- 第一次消费:根据属性
- auto.offset.reset:lastest/earliest
- 第二次消费开始:消费者在内存中记录上一次消费的offset + 1 = 这一次要消费的位置
- Kafka将每个消费者消费的位置主动定期记录在一个Topic中:__consumer_offsets
- 如果消费者遇到故障,Kafka怎么保证不重复不丢失?
- Kafka将每个消费者消费的offset存储在一个独立的Topic中:__consumer_offsets
- 如果消费者故障,重启,从这个Topic查询上一次的offset + 1
- 问题2:这个Topic中记录的offset是怎么来的?
- 默认机制:根据时间周期由消费者自动提交
- 导致问题:数据重复或者数据丢失问题
- 解决问题:根据处理的结果来实现基于每个分区的手动提交
- 消费一个分区、处理一个分区、处理成功,提交这个分区的offset
- 工作中:不需要管使用手动提交还是自动提交?
- 因为工作中不利用Kafka的存储来实现offset的恢复
- 一般我们自己存储offset:将每个分区的offset存储在MySQL、Zookeeper、Redis
- 如果程序故障,从MySQL、Zookeeper、Redis中读取上一次的Offset
自动提交
Properties props = new Properties();
props.setProperty("bootstrap.servers", "node1:9092");
props.setProperty("group.id", "test");
props.setProperty("enable.auto.commit", "true");
props.setProperty("auto.commit.interval.ms", "1000");
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);
consumer.subscribe(Arrays.asList("bigdata01"));
while (true){
ConsumerRecords<String, String> poll = consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : poll) {
String topic = record.topic();
int partition = record.partition();
long offset = record.offset();
String key = record.key();
String value = record.value();
System.out.println(topic+"\t"+partition+"\t"+offset+"\t"+key+"\t"+value);
}
}
原因
- 数据丢失的情况
- 如果刚消费,还没处理,就达到提交周期,记录了当前 的offset
- 最后处理失败,需要重启,重新消费处理
- Kafka中已经记录消费过了,从上次消费的后面进行消费
- 数据重复的情况
- 如果消费并处理成功,但是没有提交offset,程序故障
- 重启以后,kafka中记录的还是之前的offset,重新又消费一遍
- 数据重复问题
解决
- 实现手动提交Topic的Offset
小结
- 消费是否成功,是根据处理的结果来决定的
- 提交offset是根据时间来决定了
- 需要:根据处理的结果来决定是否提交offset
- 如果消费并处理成功:提交offset
- 如果消费处理失败:不提交offset
手动提交Topic Offset
Properties props = new Properties();
props.setProperty("bootstrap.servers", "node1:9092");
props.setProperty("group.id", "test");
props.setProperty("enable.auto.commit", "false");
// props.setProperty("auto.commit.interval.ms", "1000");
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("bigdata01"));
while (true){
ConsumerRecords<String, String> poll = consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> record : poll) {
System.out.println(record.topic()+"\t"+record.partition()+"\t"+record.key()+"\t"+record.offset()+"\t"+record.value());
}
consumer.commitSync();
}
原因
- Offset是分区级别的
- 提交offset是按照整个Topic级别来提交的
解决
- 提交offset的时候,按照分区来提交
- 消费成功一个分区,就提交一个分区的offset
小结
- 导致问题:数据重复
- 导致原因:offset是分区级别,提交时topic级别,只要有一个分区失败,整个提交失败,实际上部分分区已经处理成功了
手动提交partition Offset
Properties props = new Properties();
props.setProperty("bootstrap.servers", "node1:9092");
props.setProperty("group.id", "test");
props.setProperty("enable.auto.commit", "false");
// props.setProperty("auto.commit.interval.ms", "1000");
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
TopicPartition part0 = new TopicPartition("bigdata01", 0);
TopicPartition part1 = new TopicPartition("bigdata01", 1);
ArrayList<TopicPartition> list = new ArrayList<>();
list.add(part0);
list.add(part1);
// consumer.assign(Arrays.asList(part0,part1));
consumer.assign(list);
while (true){
ConsumerRecords<String, String> poll = consumer.poll(Duration.ofSeconds(1));
Set<TopicPartition> partitions = poll.partitions();
for (TopicPartition partition : partitions) {
List<ConsumerRecord<String, String>> records = poll.records(partition);
long offset = 0;
for (ConsumerRecord<String, String> record : records) {
String topic = record.topic();
int part= record.partition();
offset = record.offset();
String key = record.key();
String value = record.value();
System.out.println(topic+"\t"+part+"\t"+offset+"\t"+key+"\t"+value);
}
Map<TopicPartition, OffsetAndMetadata> offsets = Collections.singletonMap(partition,new OffsetAndMetadata(offset+1));
consumer.commitSync();
}
}