相思一夜情多少,地角天涯未是长。 
				-- 张仲素《燕子楼》

kafka producer偏移量不对 kafka的偏移量_偏移量

本文已同步掘金平台,图片依然保持最初发布的水印(如CSDN水印)。(以后属于本人原创均以新建状态在多个平台分享发布)

前言

上一篇文章大概讲述了偏移量Offset的概念,本篇文章会详细讲讲偏移量。

生产者Offset

kafka producer偏移量不对 kafka的偏移量_偏移量_02

  1. 生产者消息会分配到自己的分区里,每个分区都有一个Offset,而且是生产者最大的Offset,也是分区最大的Offset(偏移量)。
  2. 我们在写ProducerRecord生产者时,是没有指定分区的offset的,这个是有kafka自己完成的。

消费者Offset

kafka producer偏移量不对 kafka的偏移量_偏移量_03


1.上图C1和C2来来自不同的消费群组(因为同一个分区只能被同一个消费群组里一个消费者消费)

2. 生产提交的偏移量是4,C1消费者从0开始消费到3,C2消费者从0开始消费到了4,等下次来消费的时候,他们分别从各自的上一次消费的偏移量位置开始消费,当然也可以选择从头开始,或者从最近的记录消费。

auto.offset.reset:
该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
API方法:commitSync()

提交偏移量

偏移量存在哪里? 大家肯定有被问道这个问题?

Kafka的offset以前存放在ZK里,由于zookeeper不适合为服务提供负载高写(如偏移量更新),于是移除了,改成存储在主题里,原因如下:

Zookeeper is not a good way to service a high-write load such as offset updates because zookeeper routes each write though every node and hence has no ability to partition or otherwise scale writes. We have always known this, but chose this implementation as a kind of “marriage of convenience” since we already depended on zk.


译文:Zookeeper不是为高写负载(比如偏移量更新)提供服务的好方法,因为Zookeeper通过每个节点路由每个写操作,因此无法进行分区或扩展写操作。我们一直都知道这一点,但选择这个实现作为一种“方便的联姻”,因为我们已经依赖于zk。

目前消费者提交偏移量时,会发送到一个_comsumer_offsets的topic里,并保持一个内存结构:组/主题/分区,映射到最新的偏移量,方便快速检索。
磁盘里也可以看到类似如下文件:

_comsumer_offsets-1
_comsumer_offsets-2
_comsumer_offsets-3

Show me code:

提交偏移量的方式

自动提交

enable.auto.commit = true
#自动提交的时间间隔 在spring boot 2.X 版本中这里采用的是值的类型为Duration 需要符合特定的格式,如1S,1M,2H,5D
auto.commit.interval.ms = 100

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for (ConsumerRecord<String, String> record : records) {
        log.trace("Kafka消费信息ConsumerRecord={}",record.toString());
    }
}
缺点
  • 重复消费
    自动提交间隔时间会导致重复消费,比如自动提交时间100S,首次提交的偏移量是20,而消费者拉取了5条消息消费了,在50秒的时候broker突然宕机,发生了分区再均衡,这个时候之前分区的消息从之前的消费者转移到另外一个新消费者,新的消费者会读取50秒之前提交的偏移量,导致重复消费。
  • 丢失消息
    消费者批量推送消息时,消费之只消费了20条,如果broker宕机,发生分区再均衡时,会从上次提交的偏移量位置重新消费,导致批量的消息(80条)丢失
    注意:在调用close方法之前,是会自动提交一次偏移量,但异常或者提前退出轮询(poll)是不会的,需要自己定义策略来保证。比如finally方法手动提交commitSync()

手动提交-当前偏移量

enable.auto.commit = false
API方法:commintSync()

提交成功后马上返回,如果提交失败就抛出异常
Show me code:

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for (ConsumerRecord<String, String> record : records) {
        log.trace("Kafka消费信息ConsumerRecord={}",record.toString());
    }
    try {
        consumer.commitSync();
    } catch (CommitFailedException e) {
        // todo 补偿机制
        log.error("commitSync failed", e)
    }
}
缺点
  • 在broker对提交做出回应之前,应用程序一直处于阻塞状态,这样会降低吞吐量
  • poll循环获取最新偏移量后,如果全部处理完了,一定要确保commitAync()
  • 异常时,需要做出相应的补偿机制,否则会丢失消息
  • 分区再均衡时,可能会重复消费

异步提交-当前偏移量

enable.auto.commit = false
API方法:1. commintAsync()或commitAysnc(OffsetCommitCallback var1)

异步,顾名思义就知道是不阻塞了,所以这是它的优点。

Show me code:

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for (ConsumerRecord<String, String> record : records) {
        log.trace("Kafka消费信息ConsumerRecord={}",record.toString());
    }
    // 1. 无回调 二选一 
    try {
        consumer.commitAsync();
    } catch (CommitFailedException e) {
        // todo 补偿机制
        log.error("commitAsync failed", e)
    }
	// 无回调 end
	
	// 2. 有回调 二选一
	consumer.commitAsync(new OffsetCommitCallback() {	
		@Override
		public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception e)       
        {
			if (e != null) {
				System.out.println(offsets.toString());
				System.out.println(e.toString());
			}
		}
	});
	// 有回调 end
}
缺点
  • 如果失败了,不会进行重试, 但它支持异步回调。

Q1:为什么不会重试?
A1:由于异步提交,broker收到相应的时候,是无法知道哪个偏移量已经提交了(也就是说时间顺序被打乱了),所以不进行重试
Q2:如何进行解决
A2:可以用异步回调记录提交偏移量和错误信息,可参考上面的demo“有回调”

异步+同步提交

enable.auto.commit = false
API方法:commintAsync()和commitSync()

如果提交在关闭消费者或分区再均衡前的最后一次提交偏移量,那么为了保证全部提交,异步+同步提交方式非常适合。

Show me code:

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for (ConsumerRecord<String, String> record : records) {
        log.trace("Kafka消费信息ConsumerRecord={}",record.toString());
    }
    try {
        consumer.commitAsync();
    } catch (CommitFailedException e) {
        // todo 补偿机制
        log.error("commitAsync failed", e)
    } finally{
    	try {
    		consumer.commitSync();
    	 }  catch (CommitFailedException e) {
	        // todo 补偿机制
	        log.error("commitAsync failed", e)
	    } finally{
    	 	consumer.close();
    	 }
    }
}

提交特定偏移量

enable.auto.commit = false
API方法:

  1. commitSync();和commitSync(Map<TopicPartion,OffsetAndMetadata> var1)
  2. commitAsync() 和commitAsync(Map<TopicPartion,OffsetAndMetadata> var1, OffsetCommitCallback var2)

如果想在批次中间提交,或者说在poll返回结果的其中某个位置提交,那么可以使用提交特定偏移量。因为commitAsync和commitSync只会提交最后一次offset,所以用特定偏移量提交比较适合那些:分区再均衡后重新拉去大量的comsumerRecord,而又不想重复处理提交过的(这个时候需要去维护所有分区的偏移量)

Show me code:

Map<TopicPartition,OffsetAndMetaData> map = new HashMap<>();
int index = 0;
try {
	while (true) {
	    ConsumerRecords<String, String> records = consumer.poll(100);
	    for (ConsumerRecord<String, String> record : records) {
	        log.trace("Kafka消费信息ConsumerRecord={}",record.toString());
	        map.put(new TopicPartition(record.topic(),record.partition()),new (OffsetAndMetaData(records.topic(),"欢迎关注CSDN平凡君"));
	        // 每200为一批提交一次
	        if(index  % 200 == 0)
	        	 consumer.commitSync(map);
	        }
	        index++;
	    }
	}
} catch (CommitFailedException e) {
        // todo 补偿机制
        log.error("commitAsync failed", e)
} finally {
	try {
		consumer.commitSync();
	 }  catch (CommitFailedException e) {
	     // todo 补偿机制
	     log.error("commitAsync failed", e)
	 } finally{
		 	consumer.close();
	 }
}
缺点
  • 编码量大 ,如果解决不重复提交,需要记录 各个分区偏移量信息,可以建立一张DB表入库,记录每个消息偏移量的情况

如何退出poll循环

  1. 其实可以不用处理,因为Kafka会自动优雅推出选择。当poll拉去消息时,如果发现没有或消费者重启,会自动抛出异常
  2. 如果非要自己退出,那么可以用comsumer.wakeup()方法,如果循环再主线程,那么可以用钩子方法ShutdownHook里退出,show me code(这种不推荐用,第一种优雅退出最好):
// mainThread 主线程 此处省略n行代码
Runtime.getRuntime().addShutdownHook(()->{
	comsumer.wakeup();
	try{
	} catch(Exception e){
		mainThread.work();
	}
});
  1. 在退出之前,comsumer.close()是非常有必要的,因为它会提交任何还没有提交的东西, 并向群组协调器(broker)发送消息,告知自己要离开群组,接下来就会触发再均衡 ,而不需要等待会话超时。