kafka系列(六)—— Consumer API
Consumer 消费数据时的可靠性是很容易保证的,因为数据在 Kafka 中是持久化的,故不用担心数据丢失问题。由于 consumer 在消费过程中可能会出现断电宕机等故障, consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。所以 offset 的维护是 Consumer 消费数据是必须考虑的问题。
Consumer 消费数据时的可靠性是很容易保证的,因为数据在 Kafka 中是持久化的,故不用担心数据丢失问题。
由于 consumer 在消费过程中可能会出现断电宕机等故障, consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。
所以 offset 的维护是 Consumer 消费数据是必须考虑的问题。
- 0.9.0.0版本之前是Scala语言写的,之后是java语写的
- 新版本使用的是org.apache.kafka.client.consumer.KafkaConsumer
- 旧版本使用的是kafka.consumer.ZookeeperConsumerConnector/SimpleConsumer
6.1 消费者流程
consumer 采用 pull(拉) 模式从 broker 中读取数据。
push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成 consumer 来不及处理消息, 典型的表现就是拒绝服务以及网络拥塞。而 pull 模式则可以根据 consumer 的消费能力以适当的速率消费消息。
pull 模式不足之处是,如果 kafka 没有数据,消费者可能会陷入循环中, 一直返回空数据。 针对这一点, Kafka 的消费者在消费数据时会传入一个时长参数 timeout,如果当前没有数据可供消费, consumer 会等待一段时间之后再返回,这段时长即为 timeout
对于Kafka而言,pull模式更合适,它可简化broker的设计,consumer可自主控制消费消息的速率,同时consumer可以自己控制消费方式——即可批量消费也可逐条消费,同时还能选择不同的提交方式从而实现不同的传输语义。
6.2 消费者创建
- 回忆控制台的消费者创建:
# 启动zk和kafka集群,在kafka集群中打开一个消费者
[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh \
--zookeeper hadoop102:2181 --topic first
# --from-beginning是指从头消费,与后面javaAPI中的auto.offset.reset=earilest一样
- java消费创建
pom.xml
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.12</artifactId>
<version>0.11.0.0</version>
</dependency>
</dependencies>
Kafka消费者Java API
1、构造一个java.util.Properties对象,至少指定bootstrap.servers、key.deserializer、value.deserializer和group.id的值
2、使用Properties实例构造KafkaConsumer对象
3、调用KafkaConsumer.subscribe()订阅一个topic列表
4、循环调用KafkaConsumer.poll()获取封装在ConsumerRecord的topic信息
5、处理获取到的ConsumerRecord对象
6、关闭KafkaConsumer
详细参考:https://blog.csdn.net/hancoder/article/details/107446151
poll
java consumer是一个多线程或者说是一个双线程的java进程
- 用户主线程:创建ConsumerKafkaConsumer的线程。(poll在这里运行)
- 后台心跳线程:consumer后台创建一个心跳线程
消费者组执行rebalance、消息获取、coordinator管理、异步任务结果的处理甚至位移提交等操作都是运行在用户主线程中的。
常见用法
while (true) {
// 读取数据,读取超时时间为100ms ,即每个100ms拉取一次
ConsumerRecords<String, String> records = consumer.poll(100);//但是拉取到的数据可能处理失败,所以这里容易出问题,重新启动时因为有offset我们就不能重新处理数据了,所以我们后面改用手动提交
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
poll()方法根据当前consumer消费的唯一返回消息集合。当poll首先被调用用,新的消费者组会被创建并根据对应的唯一重设策略(auto.offset.reset)来设定消费者组的位移。一旦consumer开始提交位移,每个后续的rebalance完成后都会将位置设置为上次已提交的位移。
传递给poll()方法的超时设定参数用于控制consumer等待消息的最大阻塞时间。由于某些原因,broker端有时候无法立即满足consumer端的获取请求(比如consumer要求至少一次获取1MB的数据,但broker端无法立即全部给出),那么此时consumer端就会阻塞以等待数据不断累积被满足consumer需求。如果用户不想让consumer一直处于阻塞状态,则需要给定一个超时时间。因此poll方法返回满足一下任一条件时即可返回
- 获取了足够多的可用数据
- 等待时间超过指定的超时设置
consumer是单线程的设计理念,因此consumer运行在它专属的线程中。新版本的java consumer不是线程安全的,如果没有显示地同步锁保护机制,kafka会排除KafkaConsumer is not safe for multi-threaded access异常,这代表同一个kafkaconsumer实例用在了多个线程中,这是不允许的。
上面的是while(isRunning)来判断是否退出消费循环结束consumer应用。具体的做法是让isRunning边控几位volatile型
6.3 消费者组
消费者使用一个消费者组名group.id来表示自己,topic的每条消息都只会发送到每个订阅他的消费者组的一个消费者实例上。
- 一个消费者组包含多个消费者
- 每个partition消息只能被发送到消费者组中一个消费者实例上(即一个partition只能对应一个consumer,不能对应2个consumer,所以消费者组里的consumer不要比partition数多,多了没有意义)
- partition消息可以发送到多个消费者组中
- consumer从partition中消费消息是顺序消费,默认从头开始消费
因此,基于消费者组可以实现:
- 基于队列的模型:所有消费者都在同一消费者组里,每条消息只会被一个消费者处理
- 基于发布订阅的模型:消费者属于不同的消费者组。极端情况下每个消费者都有自己的消费者组,这样kafka消息就能广播到所有消费者实例上。
在下图中,有一个由三个消费者组成的group,有一个消费者读取主题中的两个分区,另外两个分别读取一个分区。某个消费者读取某个分区,也可以叫做某个消费者是某个分区的拥有者。
在这种情况下,消费者可以通过水平扩展的方式同时读取大量的消息。另外,如果一个消费者失败了,那么其他的group成员会自动负载均衡读取之前失败的消费者读取的分区。
消费者组的意义
消费者组里某个消费者挂了组内其他消费能接管partition,这叫重平衡。
- 高伸缩性
- 高容错性
消费者组再平衡
再均衡是指,对于一个消费者组,分区的所属从一个消费者转移到另一个消费者的行为。他为消费者组剧本里高可用性和伸缩性提供了保障,使得我们既可以方便又安全地删除组内的消费者或者往消费者组里添加消费者。不过再均衡发生期间,消费者是无法拉取信息的。
(1)指定了patition情况下,则直接使用;
(2)未指定patition但指定key情况下,将key的hash值与topic的partition数进行取余得到partition值;
(3)patition和key都未指定情况下,第一次调用时随便生成一个整数(后面每次调用在这个整数上自增),将这个值与topic可用的partition总数取余得到partition值。即使用round-robin算法轮询选出一个patition。
消费者组再平衡:比如有20个消费者组,订阅了100个partition,正常情况下消费者组会为每个消费者分配5个partition,每个消费者负责读取5个分区的数据。
一个 consumer group 中有多个 consumer,一个 topic 有多个 partition,所以必然会涉及到 partition 的分配问题,即确定那个 partition 由哪个 consumer 来消费。
再平衡触发条件:
1、当一个 consumer 加入组时,读取的是原本由其他 consumer 读取的分区。
2、当一个 consumer 离开组时(被关闭或发生崩溃),原本由它读取的分区将由组里的其他 consumer 来读取。
3、当 Topic 发生变化时,比如添加了新的分区,会发生分区重分配。
producter是线程安全的,consumer不是线程安全的。有两种典型的处理模式:一是每个线程里创建consumer。二是只用一个consumer然后在里面分线程
// 出现再均衡时,马上再提交一回
public class CommitSynclnRebalance {
public static void main(String[] args) {
Properties properties = initNewConfig();
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
HashMap<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
consumer.subscribe(Arrays.asList("first"), new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
// 尽量避免重复消费
consumer.commitAsync(currentOffsets);
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
// do nothing
}
});
try {
while (isRunning.get()){
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record.offset()+":"+record.value());
currentOffsets.put(new TopicPartition(record.topic(),record.partition()),new OffsetAndMetadata(record.offset()+1))
}
// // 也可以通过records.partitions()分partition处理
/*
for(TopicParition partition:records.partitions()){
List<ConsumerRecords<String,String>> recordOfPartition = records.records(partition);//取得指定分区的record
long lastOffset = pRecord.get(pRecord.size()-1).offset();
Map<TopicPartition,OffsetAndMetadata> offset = new HashMap();
offset.put(partition,new OffsetAndMetadata(lastOffset+1));//从下一个位置开始查
consumer.commitAsync(offset);//针对每一次partition单独提交offset
}
*/
}
} finally {
consumer.commitAsync(currentOffsets,null);
}
}
}
再平衡分配策略
Kafka 有两种分配策略,一是 RoundRobin,一是 Range 。触发时机:消费者组里个数发生变化时。
1) RoundRobin
1) RoundRobin :把所有的 partition 和所有的 consumer 都列出来,然后按照 hashcode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者。
轮询关注的是组
假如有3个Topic :T0(三个分区P0-0,P0-1,P0-2),T1(两个分区P1-0,P1-1),T2(四个分区P2-0,P2-1,P2-2,P2-3)
有三个消费者:C0(订阅了T0,T1),C1(订阅了T1,T2),C2(订阅了T0,T2)
那么分区过程如下图所示
分区将会按照一定的顺序排列起来,消费者将会组成一个环状的结构,然后开始轮询。
C0: P0-0,P0-2,P1-1
C1:P1-0,P2-0,P2-2
C2:P0-1,P2-1,P2-3
2)Range
2)Range:范围分区策略是对每个 topic 而言的。首先对同一个 topic 里面的分区按照序号进行排序,并对消费者(不是消费者组)按照字母顺序进行排序。通过 partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。
range跟组没什么关系,只给订阅了的消费者发,而不是给订阅了的消费者组发
6.4 offset
这里的offset是consumer端的offset。
offset:每个consumer实例需要为他消费的partition维护一个记录自己消费到哪里的偏移offset。
kafka把offset保存在消费端的消费者组里。kafka引入了检查点机制定期对offset进行持久化。kafka consumer在内部使用一个map保存其订阅topic所属分区的offset。如记录topicA-0:8;topicA-1:6…
offset可以避免 consumer 在消费过程中可能会出现断电宕机等故障, consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。
位移提交
位移提交offset commit:consumer客户端需要定期向kafka集群汇报自己消费数据的进度。当我们调用poll()时就会根据该信息消费。
- 旧版本:旧版本的记录在zookeeper的/consumers/[group.id]/offsets/[topic]/[partitionID]下。缺点是zk是一个协调服务组件,不适合用作位移信息的存储组件,频繁高并发读写不是zk擅长的事情。
- 新版本:0.9.0.0版本之后,位移提交放到kafka-Broker内部一个名为
__concumer_offsets
的topic里。 - 提交间隔时长:当我们将enable.auto.commit设置为true,那么消费者会在poll方法调用后每隔5秒(由auto.commit.interval.ms指定)提交一次位移。和很多其他操作一样,自动提交也是由poll()方法来驱动的。在调用poll()时,消费者判断是否到达提交时间,如果是则提交上一次poll返回的最大位移
[zk: localhost:2181(CONNECTED) 1] ls /
[cluster, controller_epoch, brokers, zookeeper, admin, isr_change_notification, consumers, latest_producer_id_block, config]
[zk: localhost:2181(CONNECTED) 4] ls /brokers/topics
[first]
[zk: localhost:2181(CONNECTED) 6] ls /consumers
[console-consumer-9, console-consumer-67579] # 不同机器里的结果是一样的。数字是消费者组
# 67579号消费者组里offset信息里的 主题first 0号partition 的offset
[zk: localhost:2181(CONNECTED) 10] get /consumers/console-consumer-67579/offsets/first/0
4
cZxid = 0x100000060
ctime = Thu Jul 16 15:59:43 CST 2020
mZxid = 0x100000060
mtime = Thu Jul 16 15:59:43 CST 2020
pZxid = 0x100000060
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 1
numChildren = 0
consumer会在kafka集群的所有broker中选择一个broker作为consumer group的coordinator,用于实现组成员管理、消费分配方案制定以及提交位移等。和普通的kafka topic相同,该topic有多个分区,每个分区有多个副本,他存在的唯一目的就是保存consumer提交的位移。
当消费者组首次启动时,由于没有初始的位移信息,coordinator不需要为其确定初始位移值,这就是consumer参数auto.offset.reset的作用。通常情况下,consumer要么从最早的位移开始读取,要么从最新的位移开始读取。
当consumer运行了一段时间之后,它必须要提交自己的位移值。如果consumer崩溃或者被关闭,它负责的分区就会被分配给其他consumer,因此要在其他consumer读取这些分区前就做好位移提交工作,否则会出现消息的重复消费。
cosnsumer提交位移的主要机制是通过向所属的coordinator发送位移提交请求来实现的。每个位移提交请求都会往__consumer_offsets对应分区上追加写入一条消息。消息的key是group.id+topic+partition的元组,而value就是位移值。如果consumer为同一个group的同一个topic分区提交了多次位移那么__consumer_offsets对应的分区上就会有若干条key相同但value不同的消息,但显然我们只关心最新一次提交的那条消息。从某种程序来说,只有最新提交的位移值是有效的,其他消息包含的位移值其实都已经过期了。kafka通过compact策略来处理这种消息使用模式。
考虑到一个kafka生产环境可能有很多consumer或consumer group,如果这些consumer同时提交位移,则必将加重__concumer_offsets的写入组咋,因此默认为该topic创建了50个分区,并且对每个group.id哈希后取模运算,分散到__concumer_offsets上。也就是说,每个消费者组保存的offset都有极大的概率出现在该topic的不同分区上
两种offset
Offset从语义上来看拥有两种:Current Offset和Committed Offset。
Current Offset
Current Offset保存在Consumer客户端中,它表示Consumer希望收到的下一条消息的序号。它仅仅在poll()方法中使用。例如,Consumer第一次调用poll()方法后收到了20条消息,那么Current Offset就被设置为20。这样Consumer下一次调用poll()方法时,Kafka就知道应该从序号为21的消息开始读取。这样就能够保证每次Consumer poll消息时,都能够收到不重复的消息。
Committed Offset
Committed Offset保存在Broker上,它表示Consumer已经确认消费过的消息的序号。主要通过commitSync和commitAsync
API来操作。举个例子,Consumer通过poll() 方法收到20条消息后,此时Current Offset就是20,经过一系列的逻辑处理后,并没有调用consumer.commitAsync()或consumer.commitSync()来提交Committed Offset,那么此时Committed Offset依旧是0。
Committed Offset主要用于Consumer Rebalance。在Consumer Rebalance的过程中,一个partition被分配给了一个Consumer,那么这个Consumer该从什么位置开始消费消息呢?答案就是Committed Offset。另外,如果一个Consumer消费了5条消息(poll并且成功commitSync)之后宕机了,重新启动之后它仍然能够从第6条消息开始消费,因为Committed Offset已经被Kafka记录为5。
总结一下,Current Offset是针对Consumer的poll过程的,它可以保证每次poll都返回不重复的消息;而Committed Offset是用于Consumer Rebalance过程的,它能够保证新的Consumer能够从正确的位置开始消费一个partition,从而避免重复消费。
Offset查询
前面我们已经描述过offset的存储模型,它是按照groupid-topic-partition -> offset的方式存储的。然而Kafka只提供了根据offset读取消息的模型,并不支持根据key读取消息的方式。那么Kafka是如何支持Offset的查询呢?
答案就是Offsets Cache!!
如图所示,Consumer提交offset时,Kafka Offset Manager会首先追加一条条新的commit消息到__consumers_offsets topic中,然后更新对应的缓存。读取offset时从缓存中读取,而不是直接读取__consumers_offsets这个topic。
指定消费位置
消息的拉取是 根据poll()方法的逻辑来处理的,但是这个方法对普通开发人员来说是个黑盒子,无法精确账务其消费的起始位置。
seek()方法正好提供了这个功能,让我们得以追踪以前的消费或者回溯消费
public class SeekDemo {
static String topic="first";
public static void main(String[] args) {
Properties properties = initNewConfig();
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties);
consumer.subscribe(Arrays.asList(topic));
consumer.poll(Duration.ofMillis(2000));
Set<TopicPartition> assignment = consumer.assignment();
System.out.println(assignment);
Map<TopicPartition, Long> offsets = consumer.endOffsets(assignment);
for (TopicPartition topicPartition : assignment) {
// 指定从分区的那个offset开始消费
consumer.seek(topicPartition,3);
// 如果想要从分区末尾开始消费
// consumer.seek(topicPartition,offsets.get(topicPartition);
}
while (true){
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.println(record.offset()+":"+record.value());
}
}
}
}
1)修改配置文件 /kafka/conf/consumer.properties
# 为了看数据
exclude.internal.topics=false
2)读取 offset
重新启动消费者控制台
0.11.0.0 之前版本:
bin/kafka-console-consumer.sh --topic __consumer_offsets --zookeeper hadoop102:2181 --formatter "kafka.coordinator.GroupMetadataManager\$OffsetsMessageFormatter" --consumer.config config/consumer.properties --from-beginning
0.11.0.0 之后版本(含):
bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --from-beginning --topic first
大概1s更新一次
# 解读
# [组,主题,分区] # 以这个组合来记录消费者组消费到哪里了
# 后面16代表offset
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055672594,ExpirationTime 1595142072594]
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055677590,ExpirationTime 1595142077590]
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055682592,ExpirationTime 1595142082592]
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055687595,ExpirationTime 1595142087595]
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055692596,ExpirationTime 1595142092596]
[console-consumer-16121,first,0]::[OffsetMetadata[16,NO_METADATA],CommitTime 1595055697597,ExpirationTime 1595142097597]
...
# 然后让生成者又生成了2条,发现从16变到17/18
[console-consumer-16121,first,0]::[OffsetMetadata[17,NO_METADATA],CommitTime 1595056012701,ExpirationTime 1595142412701]
[console-consumer-16121,first,0]::[OffsetMetadata[17,NO_METADATA],CommitTime 1595056017704,ExpirationTime 1595142417704]
[console-consumer-16121,first,0]::[OffsetMetadata[17,NO_METADATA],CommitTime 1595056022708,ExpirationTime 1595142422708]
[console-consumer-16121,first,0]::[OffsetMetadata[18,NO_METADATA],CommitTime 1595056027708,ExpirationTime 1595142427708]
[console-consumer-16121,first,0]::[OffsetMetadata[18,NO_METADATA],CommitTime 1595056032713,ExpirationTime 1595142432713]
...
自动提交/手动提交offset
poll()之后从partition拉取了一些数组,然后可以调用commit()函数告诉 partition这一批数据消费成功,返回值这一批数据最高的偏移量提交给partition。
- 自动提交:隔段时间就自动提交offset,告诉partition已经消费好了。
- 手动提交:适合有较强的精确一次处理语义时,可以确保只要消息被处理完后再提交位移。
自动提交 offset 的相关参数:
- enable.auto.commit: 是否开启自动提交 offset 功能
- auto.commit.interval.ms: 自动提交 offset 的时间间隔
位移管理
consumer端需要为每个它要读取的分区保存消费进度,即分区中当前最新消费消息的位置。该位置就被称为位移(offset)。 consumer需要定期地向Kaka提交自己的位置信息,实际上,这里的位移值通常是下一条待消费的消息的位置。假设 consumer已经读取了某个分区中的第N条消息,那么它应该提交位移值为N,因为位移是从0开始的,位移为N的消息是第N+1条消息。这样下次 consumer重启时会从第N+1条消息开始消费。总而言之, offset就是consumer端维护的位置信息。
ffset对时于 consumer非常重要,因为它是实现消息交付语义保证( message delivery semantic)的基石。常见的3种消息交付语义保证如下。
- 最多一次( at most once)处理语义:消息可能丢失,但不会被重复处理。
- 最少一次( at least once)处理语义(默认):消息不会丢失,但可能被处理多次
- 精确一次( exactly once)处理语义:消息一定会被处理且只会被处理一次。
显然,若 consumer在消息消费之前就提交位移,那么便可以实现 at most once—因为若consumer在提交位移与消息消费之间崩溃,则 consumer重启后会从新的 onset位置开始消费,前面的那条消息就丢失了。
相反地,若提交位移在消息消费之后,则可实现 at least once语义。由于Kaka没有办法保证这两步操作可以在同一个事务中完成,因此Kafka默认提供的就是at least once的处理语义。好消息是 Kafka社区已于0.11.0.0版本正式支持事务以及精确一次处理语义。
既然offset本质上就是一个位置信息,那么就需要和其他一些位置信息区别开来。
- 当前提交位移last committed offset:consumer最近已经已经提交的offset
- 当前位置:consumer已读取但尚未提交时的位置
- 日志最新位移LEO(log end offset):指的是每个副本最大的 offset;与生产者的ISR有关
- 水位/高水位(HW high water):指的是消费者能见到的最大的 offset, ISR 队列中最小的 LEO。即公共部分,消费者只能看到HW的部分。与生产者的ISR有关
3.2.3 Exactly Once 语义
精准一次性:
将服务器的 ACK 级别设置为-1,可以保证 Producer 到 Server 之间不会丢失数据,即 At Least Once 语义。可以保证数据不丢失,但是不能保证数据不重复;
相对的,将服务器 ACK 级别设置为 0,可以保证生产者每条消息只会被发送一次,即 At Most Once 语义。可以保证数据不重复,但是不能保证数据不丢失。
但是,对于一些非常重要的信息,比如说交易数据,下游数据消费者要求数据既不重复也不丢失,即 Exactly Once 语义。 在 0.11 版本以前的 Kafka,对此是无能为力的,只能保证数据不丢失,再在下游消费者对数据做全局去重。对于多个下游应用的情况,每个都需要单独做全局去重,这就对性能造成了很大影响。
0.11 版本的 Kafka,引入了一项重大特性:幂等性。所谓的幂等性就是指 Producer 不论向 Server 发送多少次重复数据, Server 端都只会持久化一条。幂等性结合 At Least Once 语义,就构成了 Kafka 的 Exactly Once 语义。即:
At Least Once + 幂等性 = Exactly Once
要启用幂等性,只需要将 Producer 的参数中 enable.idompotence 设置为 true 即可。 Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时, Broker 只会持久化一条。
但是 PID 重启就会变化,同时不同的 Partition 也具有不同主键,所以幂等性无法保证跨分区跨会话的 Exactly Once
1稳定性
幂等性
所谓幂等性,就是对接口的多次调用所产生的结果和调用一次是一致的。生产者在进行重试的时候有可能会重复写入消息,而使用Kafka的幂等性功能就可以避免这种情况。
幂等性是有条件的:
1、只能保证 Producer 在单个会话内不丟不重,如果 Producer 出现意外挂掉再重启是无法保证的(幂等性情况下,是无法获取之前的状态信息,因此是无法做到跨会话级别的不丢不重);
2、幂等性不能跨多个 Topic-Partition,只能保证单个 partition 内的幂等性,当涉及多个 Topic-Partition 时,这中间的状态并没有同步。
Producer 使用幂等性的示例非常简单,与正常情况下 Producer 使用相比变化不大,只需要把Producer 的配置 enable.idempotence 设置为 true 即可,如下所示:
Properties props = new Properties();
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true");
props.put("acks", "all"); // 当 enable.idempotence 为 true,这里默认为 all
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer producer = new KafkaProducer(props);
producer.send(new ProducerRecord(topic, "test");
2 事务
幂等性并不能跨多个分区运作,而事务可以弥补这个缺憾,事务可以保证对多个分区写入操作的原子性。操作的原子性是指多个操作要么全部成功,要么全部失败,不存在部分成功部分失败的可能。
为了实现事务,应用程序必须提供唯一的transactionalId,这个参数通过客户端程序来进行设定。
前期准备
事务要求生产者开启幂等性特性,因此通过将transactional.id参数设置为非空从而开启事务特性的同时
需要将ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG设置为true(默认值为true),如果显示设置为false,则会抛出异常。
3 控制器
在Kafka集群中会有一个或者多个broker,其中有一个broker会被选举为控制器(Kafka Controller),它负责管理整个集群中所有分区和副本的状态。当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责分区的重新分配。
参考:
1、原文链接:https://blog.csdn.net/hancoder/article/details/107446151
2、尚硅谷官网、黑马等视频
更多推荐
所有评论(0)