Consume

consumer概览

消费者

在0.9.0版本中,提供了新版的consumer。新版本consumer的入口类是org.apache.kafka.clients.consumer.KafkaConsumer。由此可以看出,新版本客户端的代码包都是org.apache.kafka.clients
新版的consumer不在依赖zookeeper。在旧版本的consumer中,消费位移(offset)的保存于管理都是依托于zookeeper来完成的。当数据量很大且消费很频繁时,zookeeper的读/写性能往往容易成为系统瓶颈。

新旧版本consumer对比

 

编程语言

API包名

主要使用类

新版本

Java

org.apache.kafka.clients.consumer.*

KafkaConsumer

旧版本

Scala

kafka.consumer.*

ZookeeperConsumerConector

SimpleConsumer

将consumer分为如下两类:
消费者组(consumer group),是由多个消费者实例(consumer instance)构成的一个整体进行消费的
独立消费者(standalone consumer),是单独进行消费的

在讨论consumer或开发consumer程序的时候,必须给出明确的消费者上下文信息,即所用consumer的版本以及consumer的分类。

消费者组

消费者使用一个消费者组来标记自己,topic的每条消息都只会被发送到每个订阅它的消费者组的一个消费者实例上。
上述定义给出了3个非常重要的信息:
(1)一个consumer group可能有若干个consumer实例(一个group只有一个实例也是允许的);
(2)对于同一个group而言,topic的每条消息只能被发送到group下的一个consumer实例上;
(3)topic消息可以被发送到多个group中。

kafka就是通过消费者组实现的对基于队列和基于发布/订阅的两种消息有引擎模型。
(1)如果所有的消费者都隶属于同一个消费组,那么所有的消息都会被均衡的投递到每一给消费者,即每条消息只会被一个消费者处理,这就相当于队列模式的应用。
(2)如果所有的消费者都隶属于不同的消费组,那么所有的消息都会被广播到所有的消费者,即每条消息会被所有的消费者处理,这就相当于发布/订阅模式的应用。

为什么需要consumer group?
consumer group是用于实现高伸缩性、高容错性的consumer机制。组内多个consumer实例可以同时读取kafka消息,而且一旦有某个consumer挂了,consumer group会立即将已崩溃consumer负责的分区转交给其他的consumer来负责,从而保证整个group可以继续工作,不会丢失数据,整个过程被称为Rebalance

kafka目前只提供单个分区内的消息顺序,而不会维护全局的消息顺序,因此如果用户要实现topic全局的消息读取顺序,就只能通过让每个consumer group下只包含一个consumer实例的方式来间接实现。

consumer group的特点和含义:
consumer group下可以有一个或多个consumer实例,一个consumer实例可以是一个线程,也可以是运行在其他机器上的进程。
group.id唯一表示一个consumer group。
对于某个group而言,订阅topic的每个分区只能分配该group下的一个consumer实例(当然该分区还可以被分配给其他订阅该topic的消费者组)

消费位移

这里的offset指代的是consumer端的offset,与分区日志中的offset是不同的含义。每个consumer实例都会为它消费的分区维护属于自己的位置信息来记录当前消费了多少条消息。
kafka让消费组
保存offset,那么只需要简单地保存一个长整型数据就可以了,同时kafka consumer还引入了检查点机制(chenkpointing)定期对offset进行持久化,从而简化了应答机制的实现。
kafka consumer在内部使用一个map来保存其订阅topic所属分区的offset。

位移提交

consumer客户端需要定期的向kafka集群汇报自己消费数据的进度,这一过程被称为位移提交(offset commit)。位移提交这件事情对于consumer而言非常重要,它不仅表征了consumer端的消费进度,同时也直接决定了consumer端的消费语义保证。
(1)旧版本consumer会定期将位移信息提交到Zookeeper下的固定节点上。该路径是/consumers//offset//,其中group.id、topic和partition是变化的值。
(2)新版本consumer把位移提交到kafka__consumer_offsets
注意这个topic名字前面有两个下划线。这个topic很神秘,通常不能直接操作该topic,特备是注意不要擅自删除和搬移该topic的日志文件。参见__consumer_offsets。

消费者在消费完消息之后需要执行消费位移的提交。

如下图所示,x表示某次拉取操作中此分区消息的最大偏移量,假设当前消费者已经消费了x位置的消息,那么就可以说消费者的消费位移为x,用lastConsumedOffset表示它。不过需要非常明确的是,当前消费者需要提交的消费位移并不是x,而是x+1,表示下一次拉取消息的位置。

Consume_第1张图片

org.apache.kafka.clients.consumer.KafkaConsumer#position(TopicPartition partition)用于获取x+1的值,即下次所要拉取的消息的起始偏移量(position)
org.apache.kafka.clients.consumer.KafkaConsumer#committed(TopicPartition partition)用于获取已经提交的位移值(committed offset)
一般情况下position = committed offset=lastConsumedOffset+1,但是position的值和committed offset的值不并会一直相同。

offset对于consumer非常重要,因为它是实现消息交付语义保证的基石。常见的3种消息交付语义保证如下:

  1. 最多一次(at most once)处理语义:消息可能丢失,但不会被重复处理
  2. 最少一次(at least once)处理语义:消息不会丢失,但可能被处理多次
  3. 精确一次(exactly once)处理语义:消息一定会被处理且只会被处理一次

显然,若consumer在消息消费之前就提交位移,那么便可以实现at most once,因为若consumer在提交位移与消息消费之间崩溃,则consumer重启后会从新的offset位置开始消费,前面的那条消息就丢失了。相反的,若提交位移在消息消费之后,则可实现at least once语义。由于kafka没有办法保证这两步操作可以在同一个事务中完成,因此kafka默认提供的就是at least once的处理语义
kafka社区已于0.11.0.0版本正式支持事务以及精确一次处理语义。

offset本质上就是一个位置信息,那么就需要和其他一些位置信息区别开来。

Consume_第2张图片

上次提交位移(committed offset:consumer最近一次提交的offset值。
当前位置:consumer已读取但尚未提交时的位置。

水位(HW)

HW是Hight WaterMark的缩写,俗称高水位。它不属于consumer管理的范畴,它标识一个特定的消息偏移量,消费者只能拉取到这个offset的消息。分区的每个副本都有自己的LEO,ISR中最小的LEO即为HW。
从生产者发出的一条消息首先会被写入分区的leader 副本,不过还需要等待ISR 集合中的所有follower 副本都同步完之后才能被认为已经提交,之后才会更新分区的HW,进而消费者可以消费到这条消息。

日志终端位移(LEO)

LEO是Log End Offset的缩写,也被称为日志最新位移:同样不属于consumer范畴,而是属于分区日志管辖。他标识当前日志文件中下一条待写入消息的offset,LEO的大小相当于当前日志分区中最后一条消息的offset值加1。ISR集合中的每个副本都会维护自身的LEO,而ISR集合中最小的LEO即为分区的HW,对消费者而言只能消费HW之前的消息。


消息轮询

poll内部原理

归根结底,kafka的consumer是用来读取消息的,而且要同时读取多个topic的多个分区的消息。若要实现并行的消息读取,一种方式是使用多线程的方式,为每个要读取的分区都创建一个专有的线程去消费(这其实就是旧版本consumer采用的方式);另一种是采用类似于Linux I\O模型的poll或select等,使用一个线程同时管理多个socket连接,即同时与多个broker通信实现消息的并行读取,这就是新版本consumer最重要的设计改变。

一旦consumer订阅了topic,所有的消费逻辑包括coordinator的协调、消费组的Rebalance以及数据的获取都会在主逻辑poll方法的一次调用中被执行。

新版本的java consumer是一个多线程或者说是一个双线程的java进程。创建KafkaConsumer的线程被称为用户主线程,同时consumer在后台会创建一个心跳线程,该线程被称为后台心跳线程。KafkaConsumer的poll方法在用户主线程中运行,这也同时表明:消费者组执行Rebalance、消息获取、coordinator管理、异步任务结果的处理甚至位移提交等操作都是运行在用户主线程中的。

poll使用方法

consumer使用KafkaConsumer.poll方法从订阅topic中并行的获取多个分区的消息。该方法轮询返回消息集,也就是说调用一次轮询只得到一批消息。
poll方法中可以指定一个超时时间(ms),通常情况下如果consumer拿到了足够的可用数据,那么它可以立即从该方法返回;但若当前没有足够多的数据可供返回,consumer会处于阻塞状态。这个超时参数即控制阻塞的最大时间。
poll方法会返回一个ConsumerRecord实例,该实例封装kafka消息。

Kafka consumer的角度而言,poll方法返回即认为consumer成功消费了消息

consumer订阅topic之后通常以事件循环的方式来获取订阅方案并开启消息读取。听上去有些复杂,其实用户要做的仅仅是写一个循环,然后重复性地调用poll方法。剩下的所有工作都交给poll方法帮用户完成。

poll方法根据当前consumer的消费位移返回消息集合。poll首次被调用时,新的消费者组会被创建并根据对应的位移重设策略(auto.offset.reset)来设定消费组的位移。一旦consumer开始提交位移,每个后续的Rebalance完成后都会将位置设置为上次已提交的位移。
poll方法返回满足以下任意一个条件即可返回:

  1. 要么获取了足够多的可用数据
  2. 要么等待时间超过了指定的超时时间

新版本的java consumer不是线程安全的,如果没有显示地同步锁保护机制,kafka会抛出KafkaConsumer is not safe for multi-threaded access异常。

最后千万不要忘记关闭consumer。这不仅会清除consumer创建的各种socket资源,还会通知消费者组coordinator主动离组从而更快的开启新一轮Rebalance。

位移管理

新版本consumer位移管理

consumer会在kafka集群的所有broker中选择一个broker作为消费者组的GroupCoordinator,用于实现组成员管理,消费分配方案制定以及提交位移等。为每个组选择对应GroupCoordinator的依据就是内部topic(__consumer_offsets)。详情参见Rebalance。

当消费者组首次启动时,由于没有初始的位移信息,GroupCoordinator必须为其确定初始位移值,这就是consumer参数auto.offset.reset的作用。通常情况下,consumer要么从最早的位移开始读取,要么从最新的位移开始读取。
当consumer运行了一段时间之后,他必须要提交自己的位移值。如果consumer崩溃或者被关闭,他负责的分区就会被分配给其他consumer,因此一定要在其他consumer读取这些分区前就要做好位移提交工作,否则会出现消息的重复消费。

consumer提交位移的主要机制是通过向所属的GroupCoordinator发送位移提交请求来实现的。每个位移提交请求都会往__consumer_offsets对应分区上追加写入一条消息。消息的key是group.id、topic和分区的元组,而value就是位移值。如果consumer为同一个group 的同一个topic分区提交了多次位移,那么__consumer_offsets对应的分区上就会有若干条key相同但value不同的消息,但显然我们之关心最新一次提交的那条消息。从某种程度来说,只有最新提交的位移值是有效的,其他消息包含的位移值其实都已经过期了,kafka通过compact策略来处理这种消息使用模式。

自动提交和手动提交

默认情况下,consumer是自动提交位移的,自动提交间隔是5。通过设置auto.commit.interval.ms参数可以控制自动提交间隔。
自动位移提交的优势是降低了用户的开发成本使得用户不必亲自处理位移提交,劣势是用户不能细粒度的处理位移的提交,特别是在有较强的精确一次处理语义时。这这种情况下,用户可以使用手动提交位移。
设置使用手动提交位移非常简单,仅仅需要在构建kafkaComsumer时设置enable.auto.commit=false,然后调用commitSynccommitAsync方法即可。
手动提交位移方法分为同步手动提交commitSync和异步手动提交commitAsync。调用commitSync,用户程序会等待提交结束才执行下一条语句命令。调用commitAsync,这是一个异步非阻塞调用。consumer在后续poll调用时轮询该位移提交的结果。
当用户调用commitAsync()或commitSync()时,consumer会为所有它订阅的分区提交位移。当用户调用commitSync(final Map offsets)或commitAsync(final Map offsets, OffsetCommitCallback callback)时,显示的告诉kafka为那些分区提交位移。

指定位移消费

当一个新的消费组建立的时候,它根本没有可以查找的消费位移。或者消费组内的一个新消费者订阅了一个新的主题,它也没有可以查找的消费位移。当consumer offsets主题中有关这个消费组的位移信息过期而被删除后,它也没有可以查找的消费位移。
在Kafka中每当消费者查找不到所记录的消费位移时,就会根据消费者客户端参数auto.offset.reset的配置来决定从何处开始进行消费,这个参数的默认值latest,表示从分区末尾开始消费消息。如果将auto.offset.reset参数配置为earliest,那么消费者会从起始处,也就是0开始消费。
除了查找不到消费位移,位移越界也会触发auto.offset.reset参数的执行
auto.offset.reset 参数还有一个可配置的值none,配置为此值就意味着出现查到不到消费位移的时候,既不从最新的消息位置处开始消费,也不从最早的消息位置处开始消费,此时会报出NoOffsetForPartitionException异常。

有些时候,我们需要一种更细粒度的掌控,可以让我们从特定的位移处开始拉取消息,而KafkaConsumer中的seek方法正好提供了这个功能,让我们得以追前消费或回溯消费。seek方法的具体定义如下:

public void seek(TopicPartition partition, long offset)

seek方法中的参数partition表示分区,而offset 参数用来指定从分区的哪个位置开始消费。seek方法只能重置消费者分配到的分区的消费位置,而分区的分配是在poll方法的调用过程中实现的。也就是说,在执行seek方法之前需要先执行一次poll方法,等到分配到分区之后才可以重置消费位置。

有时候我们并不知道特定的消费位置,却知道一个相关的时间点,比如我们想要消费昨天8 点之后的消息,这个需求更符合正常的思维逻辑。此时我们无法直接使用seek方法来追溯到相应的位置。KafkaConsumer同样考虑到了这种情况,它提供了一个offsetsForTimes方法,通过timestamp来查询与此对应的分区位置。

public Map offsetsForTimes(Map timestampsToSearch)

offsetsForTimes方法的参数timestampsToSearch是一个Map 类型, key 为待查询的分区,而value为待查询的时间戳,该方法会返回时间戳大于等于待查询时间的第一条消息对应的位置和时间戳,对应于OffsetAndTimestamp中的offset和timestamp字段。

你可能感兴趣的:(Kafka)