1. 消费者位置(consumer position)

因为kafka服务端不保存消息的状态,所以消费端需要自己去做很多事情。我们每次调用poll()方法他总是返回已经保存在生产者队列中还未被消费者消费的消息。消息在每一个分区中都是顺序的,那么必然可以通过一个偏移量去确定每一条消息的位置。

偏移量在消费消息的过程中处于重要的作用。如果是自动提交消息,那么poll()方法会去在每次获取消息的时候自动提交获取最后一条消息的偏移量,告诉服务器我们已经消费到这个位置,下次从下一个位置开始消费。

我们把更新分区当前位置的操作叫做提交。消费者是如何提交偏移量的呢?kafka最新的api是这样做的:创建一个叫做_consumer_offset的特殊主题用来保存消息的偏移量。消费者每次消费都会往这个主题发送消息,消息包含每个分区的偏移量。

如果消费者一直处于运行的状态那么这个偏移量没有什么用。不过如果这个消费者崩溃或者有新的消费者加入群组触发再均衡策略,那么再均衡之后该分区的消费者如果不是之前的那一位,那么新的小伙伴怎么知道之前的伙计消费到哪里呢。所以提交他自己的offset就发挥作用了。

Consumer读取partition中的数据是通过调用发起一个fetch请求来执行的。而从KafkaConsumer来看,它有一个poll方法。但是这个poll方法只是可能会发起fetch请求。原因是:Consumer每次发起fetch请求时,读取到的数据是有限制的,通过配置项max.partition.fetch.bytes来限制的。而在执行poll方法时,会根据配置项个max.poll.records来限制一次最多pool多少个record。

那么就可能出现这样的情况: 在满足max.partition.fetch.bytes限制的情况下,假如fetch到了100个record,放到本地缓存后,由于max.poll.records限制每次只能poll出15个record。那么KafkaConsumer就需要执行7次才能将这一次通过网络发起的fetch请求所fetch到的这100个record消费完毕。其中前6次是每次pool中15个record,最后一次是poll出10个record。

在consumer中,还有另外一个配置项:max.poll.interval.ms ,它表示最大的poll数据间隔,如果超过这个间隔没有发起pool请求,但heartbeat仍旧在发,就认为该consumer处于 livelock状态。就会将该consumer退出consumer group。所以为了不使Consumer 自己被退出,Consumer 应该不停的发起poll(timeout)操作。而这个动作 KafkaConsumer Client是不会帮我们做的,这就需要自己在程序中不停的调用poll方法了。

当一个consumer因某种原因退出Group时,进行重新分配partition后,同一group中的另一个consumer在读取该partition时,怎么能够知道上一个consumer该从哪个offset的message读取呢?也是是如何保证同一个group内的consumer不重复消费消息呢?上面说了一次走网络的fetch请求会拉取到一定量的数据,但是这些数据还没有被消息完毕,Consumer就挂掉了,下一次进行数据fetch时,是否会从上次读到的数据开始读取,而导致Consumer消费的数据丢失吗?

为了做到这一点,当使用完poll从本地缓存拉取到数据之后,需要client调用commitSync方法(或者commitAsync方法)去commit 下一次该去读取 哪一个offset的message。

而这个commit方法会通过走网络的commit请求将offset在coordinator中保留,这样就能够保证下一次读取(不论是进行rebalance)时,既不会重复消费消息,也不会遗漏消息。

对于offset的commit,Kafka Consumer Java Client支持两种模式:由KafkaConsumer自动提交,或者是用户通过调用commitSync、commitAsync方法的方式完成offset的提交。

2. 位移管理(offset management)

2.1 自动提交

Kafka默认是定期帮你自动提交位移的(enable.auto.commit = true),使用这种简单的方式之前你需要知道可能会带来什么后果。

假设我们仍然使用默认的5s提交时间间隔,在最近一次提交之后的3s发生了再均衡,再均衡之后,消费者从最后一次提交的偏移量位置开始读取消息。这个时候偏移量已经落后

了3s,所以在这3s内到达的消息会被重复处理。可以通过修改提交时间间隔来更频繁地提交偏移量,减小可能出现重复悄息的时间窗,不过这种情况是无也完全避免的。

在使用自动提交时,每次调用轮询方告都会把上一次调用返回的偏移量提交上去,它并不知道具体哪些消息已经被处理了,所以在再次调用之前最好确保所有当前调用返回的消息都已经处理完毕(在调用close()方法之前也会进行自动提交)。一般情况下不会有什么问题,不过在处理异常或提前退出轮询时要格外小心。

2.2 手动提交

在多partition多consumer的场景下自动提交总会发生一些不可控的情况。所以消费者API也为我们提供了另外一种提交偏移量的方式。开发者可以在程序中自己决定何时提交,而不是基于时间间隔。

在使用手动提交之前我们需要先将:

  1. properties.put("enable.auto.commit", "false");

然后使用:

  1. consumer.commitSync();

来提交。

commitSync()方法会提交由poll()方法返回的最新偏移量,提交成功后马上返回,否则跑出异常。

我们处理消息的逻辑可以变成这样:

  1. while (true) {
  2. ConsumerRecords<String, String> records = consumer.poll(100);
  3. for (ConsumerRecord<String, String> record : records) {
  4. System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
  5. try {
  6. consumer.commitSync();
  7. } catch (Exception e) {
  8. System.out.println("commit failed");
  9. }
  10. }
  11. }

每处理一次消息我们提交一次offset。

异步手动提交

上面我们使用commitSync()的方式提交数据,每次提交都需要等待broker返回确认结果。这样没提交一次等待一次会限制我们的吞吐量。

如果采用降低提交频率来保证吞吐量,那么则有增加消息重复消费的风险。所以kafka消费者提供了异步提交的API。我们只管发送提交请求无需等待broker返回。

  1. while (true) {
  2. ConsumerRecords<String, String> records = consumer.poll(100);
  3. for (ConsumerRecord<String, String> record : records) {
  4. System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
  5. }
  6. consumer.commitAsync();
  7. }

commitAsync()方法提交最后一个偏移量。在成功提交或碰到无怯恢复的错误之前,commitAsync()会一直重试,但是commitAsync()不会,这也是commitAsync()不好的一个地方。它之所以不进行重试,是因为在它收到服务器响应的时候, 可能有一个更大的偏移量已经提交成功。假设我们发出一个请求用于提交偏移量2000,这个时候发生了短暂的通信问题,服务器收不到请求,自然也不会作出任何响应。与此同时,我们处理了另外一批消息,并成功提交了偏移量3000。如果commitAsync()重新尝试提交偏移量2000 ,它有可能在偏移量3000之后提交成功。这个时候如果发生再均衡,就会出现重复消息。

当然使用手动提交最大的好处就是如果发生了错误我们可以记录下来。commitAsync()也支持回调方法,提交offset发生错误我们可以记下当前的偏移量。

  1. while (true) {
  2. ConsumerRecords<String, String> records = consumer.poll(100);
  3. for (ConsumerRecord<String, String> record : records) {
  4. System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
  5. }
  6. consumer.commitAsync(new OffsetCommitCallback() {
  7. @Override
  8. public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
  9. if(e != null){
  10. System.out.println("commit failed"+map);
  11. }
  12. }
  13. });
  14. }

同步和异步组合提交

一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大问题,因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。但如果这是发生在关闭消费者或再均衡前的最后一次提交,就要确保能够提交成功。因此,在消费者关闭前一般会组合使用commitAsync()和commitSync()。

  1. try {
  2. while (true) {
  3. ConsumerRecords<String, String> records = consumer.poll(100);
  4. for (ConsumerRecord<String, String> record : records) {
  5. System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
  6. }
  7. consumer.commitAsync();
  8. }
  9. } catch (Exception e) {
  10. System.out.println("commit failed");
  11. } finally {
  12. try {
  13. consumer.commitSync();
  14. } finally {
  15. consumer.close();
  16. }
  17. }

如果一切正常我们使用commitAsync()来提交。如果直接关闭消费者,就没有所谓的下一次提交了。使用commitSync()会一直重试,直到提交成功。

2.3 提交特定偏移量

上面我们手动提交使用的commitAsync()和commitSync()都是提交每一次消费最后一条消息的偏移量,那么如果我们一次拉取了很多消息但是没有消费完,想提交我们消费完成的位置该怎么处理呢?kafka也有相应的对策。

  1. Map<TopicPartition,OffsetAndMetadata> currentOffset = new HashMap<>();
  2. while (true) {
  3. ConsumerRecords<String, String> records = consumer.poll(100);
  4. for (ConsumerRecord<String, String> record : records) {
  5. System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
  6. currentOffset.put(new TopicPartition(record.topic(),record.partition()),new OffsetAndMetadata(record.offset(),"metadata"));
  7. try {
  8. System.out.println("模拟消息处理失败的情况");
  9. } catch (Exception e) {
  10. consumer.commitAsync(currentOffset,null);
  11. }
  12. }
  13. }

这里调用的是commitAsync(),调用commitSync()也是可以的。代码中模拟我们在处理消息的过程中可能会出错的情况,每次读消息都把当前的offset存入map中,如果出错就提交当前已经消费到的偏移量。

2.4 再均衡监听器

前面我们说过当发生consumer退出或者新增,partition新增的时候会触发再均衡。那么发生再均衡的时候如果某个consumer正在消费的任务没有消费完该如何提交当前消费到的offset呢?kafka提供了再均衡监听器,在发生再均衡之前监听到,当前consumer可以在失去分区所有权之前处理offset关闭句柄等。

消费者API中有一个()方法:

  1. subscribe(Collection<TopicPartition> var1, ConsumerRebalanceListener var2);

ConsumerRebalanceListener对象就是监听器的接口对象,我们需要实现自己的监听器继承该接口。接口里面有两个方法需要实现:

  1. void onPartitionsRevoked(Collection<TopicPartition> var1);
  2. void onPartitionsAssigned(Collection<TopicPartition> var1);

第一个方法会在再均衡开始之前和消费者停止读取消息之后被调用。如果在这里提交偏移量,下一个接管分区的消费者就知道该从哪里开始读取了。

第二个会在重新分配分区之后和消费者开始读取消息之前被调用。、

我们来模拟一下再均衡的场景:

  1. final Consumer<String, String> consumer = new KafkaConsumer<>(props);
  2. consumer.subscribe(Arrays.asList("page_visits"));
  3. final Map<TopicPartition,OffsetAndMetadata> currentOffset = new HashMap<>();
  4. class HandleRebance implements ConsumerRebalanceListener{
  5. @Override
  6. public void onPartitionsRevoked(Collection<TopicPartition> collection) {
  7. System.out.println("partition is rebanlance");
  8. consumer.commitAsync(currentOffset,null);
  9. }
  10. @Override
  11. public void onPartitionsAssigned(Collection<TopicPartition> collection) {
  12. }
  13. }
  14. consumer.subscribe(topic,new HandleRebance());
  15. while (true) {
  16. ConsumerRecords<String, String> records = consumer.poll(100);
  17. for (ConsumerRecord<String, String> record : records) {
  18. System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
  19. currentOffset.put(new TopicPartition(record.topic(),record.partition()),new OffsetAndMetadata(record.offset(),"metadata"));
  20. try {
  21. System.out.println("模拟消息处理失败的情况");
  22. } catch (Exception e) {
  23. consumer.commitAsync(currentOffset,null);
  24. }
  25. }
  26. }

首先实现了ConsumerRebalanceListener接口,实现方法里面如果监听到发生再均衡我们提交当前处理过的偏移量。

2.5 从特定偏移量处开始处理

前面都是consumer.poll()之后读取该批次的消息,kafka还提供了从分区的开始或者末尾读消息的功能:

  1. seekToEnd(Collection<TopicPartition> partitions)
  2. seekToBeginning(Collection<TopicPartition> partitions)

另外kafka还提供了从指定偏移量处读取消息,可以通过seek()方法来处理:

  1. seek(TopicPartition partition, long offset)

提交当前分区和当前消费位置信息。

2.6 独立消费者–不属于群组的消费者

到目前为止我们讨论的都是消费者群组,分区被自动分配给群组的消费者,群组的消费者有变动会触发再均衡。那么是不是可以回归到别的消息队列的方式:不需要群组消费者也可以自己订阅主题?

kafka也提供了这样的案例,因为kafka的主题有分区的概念,那么如果没有群组就意味着你的自己订阅到特定的一个分区才能消费内容。如果是这样的话,就不需要订阅主题,而是为自己分配分区。一个消费者可以订阅主题(井加入消费者群组),或者为自己分配分区,但不能同时做这两件事情。

下面的例子演示如何为自己分配分区并读取消息的:

  1. final Consumer<String, String> consumer = new KafkaConsumer<>(props);
  2. List<PartitionInfo> partitionInfoList = consumer.partitionsFor("page_visits");
  3. List<TopicPartition> topicPartitionList = new ArrayList<>();
  4. if(partitionInfoList != null){
  5. for(PartitionInfo partitionInfo : partitionInfoList){
  6. topicPartitionList.add(new TopicPartition(partitionInfo.topic(),partitionInfo.partition()));
  7. consumer.assign(topicPartitionList);
  8. }
  9. }
  10. final Map<TopicPartition,OffsetAndMetadata> currentOffset = new HashMap<>();
  11. while (true) {
  12. ConsumerRecords<String, String> records = consumer.poll(100);
  13. for (ConsumerRecord<String, String> record : records) {
  14. System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
  15. currentOffset.put(new TopicPartition(record.topic(),record.partition()),new OffsetAndMetadata(record.offset(),"metadata"));
  16. try {
  17. System.out.println("模拟消息处理失败的情况");
  18. } catch (Exception e) {
  19. consumer.commitAsync(currentOffset,null);
  20. }
  21. }
  22. }
  1. consumer.partitionsFor(“主题”)方法允许我们获取某个主题的分区信息。
  2. 知道想消费的分区后使用assign()手动为该消费者分配分区。

除了不会发生再均衡,也不需要手动查找分区,其他的看起来一切正常。不过要记住,如果主题增加了新的分区,消费者并不会收到通知。所以,要么周期性地调用consumer.partitionsFor()方法来检查是否有新分区加入,要么在添加新分区后重启应用程序。

kafka同步异步消费和消息的偏移量(四)的更多相关文章

  1. IO中同步异步,阻塞与非阻塞 -- 通俗篇

    一.同步与异步 同步/异步, 它们是消息的通知机制 1. 概念解释 A. 同步 所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回. 按照这个定义,其实绝大多数函数都是同步调用(例 ...

  2. kafka 保证消息被消费和消息只消费一次

    1. 保证消息被消费 即使消息发送到了消息队列,消息也不会万无一失,还是会面临丢失的风险. 我们以 Kafka 为例,消息在Kafka 中是存储在本地磁盘上的, 为了减少消息存储对磁盘的随机 I/O, ...

  3. kafka同步生产者和异步生产者深入剖析

    什么是kafka同步生产者,什么是kafka异步生产者? 比如这里某个topic有3个分区. kafka同步生产者:这个生产者写一条消息的时候,它就立马发送到某个分区去.  kafka异步生产者:这个 ...

  4. 循序渐进做项目系列(2):最简单的C/S程序——消息异步调用与消息同步调用

    上篇博客 循序渐进做项目系列(1):最简单的C/S程序——让服务器来做加法 实现了一个最简单的C/S程序,即让服务器来做加法.当时为了通俗易懂采用了消息异步调用的方式.今天我们要采用消息同步调用的方式 ...

  5. 【kafka学习之六】kakfa消息生产、消费示例

    环境 虚拟机:VMware 10 Linux版本:CentOS-6.5-x86_64 客户端:Xshell4 FTP:Xftp4 jdk1.8 kafka_2.11-0.11.0.0 zookeepe ...

  6. 消息/事件, 同步/异步/协程, 并发/并行 协程与状态机 ——从python asyncio引发的集中学习

    我比较笨,只看用await asyncio.sleep(x)实现的例子,看再多,也还是不会. 已经在unity3d里用过coroutine了,也知道是“你执行一下,主动让出权限:我执行一下,主动让出权 ...

  7. 深入浅出理解基于 Kafka 和 ZooKeeper 的分布式消息队列

    消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题.实现高性能,高可用,可伸缩和最终一致性架构,是大型分布式系统不可缺少的中间件. 本场 Chat 主要内容: Kafk ...

  8. Kafka 和 ZooKeeper 的分布式消息队列分析

    1. Kafka 总体架构 基于 Kafka-ZooKeeper 的分布式消息队列系统总体架构如下: 如上图所示,一个典型的 Kafka 体系架构包括若干 Producer(消息生产者),若干 bro ...

  9. Kafka是分布式发布-订阅消息系统

    Kafka是分布式发布-订阅消息系统 https://www.biaodianfu.com/kafka.html Kafka是分布式发布-订阅消息系统.它最初由LinkedIn公司开发,之后成为Apa ...

随机推荐

  1. Python的函数, 返回值, 参数

    1. 函数 函数是对功能的封装 语法: def 函数名(形参): 函数体(代码块,return) 调用: 函数名(实参) 2. 返回值 return:在函数执行的时候, 遇到return 就直接返回, ...

  2. 初识Grep

    前言:grep这个命令都不陌生,最常用的就是和管道符结合,例如:ps -ef | grep docker,但是我还是想认识一下这个非常giao的命令... Grep称为全局正则表达式检索工具,在企业中 ...

  3. Docker笔记(二):Docker管理的对象

    原文地址:http://blog.jboost.cn/2019/07/14/docker-2.html 在Docker笔记(一):什么是Docker中,我们提到了Docker管理的对象包含镜像.容器. ...

  4. Spring中@value以及属性注入的学习

    1.简单的Java配置 配置文件(jdbc.properties) jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://1 ...

  5. Docker笔记(五):整一个自己的镜像

    原文地址:http://blog.jboost.cn/2019/07/17/docerk-5.html 获取镜像的途径有两个,一是从镜像仓库获取,如官方的Docker Hub,二是自定义.上文已经介绍 ...

  6. 基于Actor模型的CQRS、ES解决方案分享

    开场白 大家晚上好,我是郑承良,跟大家分享的话题是<基于Actor模型的CQRS/ES解决方案分享>,最近一段时间我一直是这个话题的学习者.追随者,这个话题目前生产环境落地的资料少一些,分 ...

  7. SSRS报表-级联筛选参数刷新后不能默认全选 -问题解决方案

    好久没有写博客了,最近更新完善修复了SSRS报表的一些问题,和大家分享. 问题描述: 报表中,区域->专区->省份->地级市 此四个筛选参数是联动的,在DataSet中前一父级参数作 ...

  8. 数组指针&&指针数组

    数组指针: 定义 int (*p)[n];      占用内存中一个指针的存储空间 ()优先级高,首先说明p是一个指针,指向一个int型的一维数组,故名数组(的)指针.指向的一维数组的长度是n,也可以 ...

  9. java高并发系列 - 第14天:JUC中的LockSupport工具类,必备技能

    这是java高并发系列第14篇文章. 本文主要内容: 讲解3种让线程等待和唤醒的方法,每种方法配合具体的示例 介绍LockSupport主要用法 对比3种方式,了解他们之间的区别 LockSuppor ...

  10. NOIp 2018 普及&提高组试题答案

    你们考的咋样呢?在评论区说出自己的分数吧!