kafka技术内幕读书笔记(三):消费者

使用消费组实现消息队列的两种模式:

作为分布式的消息系统, Kafka支持多个生产者和多个消费者,生产者可以将消息发布到集群中不同节点的不同分区上;消费者也可以消费集群中多个节点的多个分区上的消息。写消息时,多个生产者可以写到同一个分区。读消息时,如果多个消费者同时读取一个分区,为了保证将日志文件的不同数据分配给不同的消费者,需要采用加锁、同步等方式,在分区级别的日志文件上做些控制。

相反,如果约定“同一个分区只可被一个消费者处理”,就不需要加锁同步了,从而可提升消费者的处理能力。而且这也并不违反消息的处理语义:原先需要多个消费者处理,现在交给一个消费者处理也是可以的。图3- 1 给出了一种最简单的消息系统部署模式,生产者的数据源多种多样,它们都统一写入Kafka集群。处理消息时有多个消费者分担任务,这些消费者的处理逻辑都相同, 每个消费者处理的分区都不会重复。

实际应用中, Kafka集群的数据需要被不同类型的消费者使用,而不同类型的消费者处理逻辑不同。Kafka使用消费组的概念,允许一组消费者进程对消费工作进行划分。每个消费者都可以配置一个所属的消费组,并且订阅多个主题。Kafka会发送每条消息给每个消费组中的一个消费者进程( 同一条消息广播给多个消费组,单播给同一组中的消费者)。被订阅主题的所有分区会平均地负载给订阅方,即消费组中的所有消费者。比如1个主题有4个分区,1个消费组有2个消费者,那么每个消费者都会分配到2个分区。

如图3 - 2所示,典型的Kafka集群部署方式会有多个消费组,并且每个消费组中也有多个消费者。这样既允许多种业务逻辑的消费组存在,也可以保证同一个消费组内的多个消费者协调工作,避免因一个消费组中只有一个消费者导致数据丢失。

总结来说就是有2种模式:

发布-订阅模式。同一条消息会被多个消费组消费,每个消费组只有一个消费者,实现广播。
队列模式。只有一个消费组、多个消费者一条消息只被消费组的一个消费者消费,实现单播。

 

消费组再平衡实现故障容错:

消费者是客户端的监务处理逻辑程序,因此要考虑消费者的故障容错。一个消费组有多个消费者,因此消费组需要维护所有的消费者。如果一个消费者宕机了,分配给这个消费者的分区需要重新分配给相同组的其他消费者;如果一个消费者加入了同一个组,之前分配给其他消费组的分区需要分配给新加入的消费者。
一旦有消费者加入或退出消费组,导致消费组成员列表发生变化,消费组中所有的消费者就要执行再平衡( rebalance ) 工作。如果订阅主题的分区有变化,所有的消费者也都要再平衡。如图3 -4 所示,在加入一个新的消费者后,需要为所有的消费者重新分配分区, 因此所有消费者都会执行再平衡。

消费者保存消费进度:

生产者的提交日志采用递增的偏移量,连同消息内容一起写入本地日志文件。生产者客户端不需要保存偏移量相关的状态,消费者客户端则要保存消费消息的偏移量即消费进度。消费进度表示消费者对一个分区已经消费到了哪里。

消费者对分区的消费进度通常保存在外部存储系统中,比如ZK 或者Kafka 的内部主题( _consumer_offsets ) 。这样分区的不同拥有者总是可以读取同一个存储系统的消费进度,即使消费者成员发生变化,也不会影响消息的消费和处理。如图3 - 5 所示,消费者消费消息时,需要定时将分区的最新消费进度保存到ZK 中。当发生再平衡时,消费者拥有的新分区消费进度都可以从ZK 中读取出来,从而恢复到最近的消费状态。

分区分配给消费者:

一个分区只能属于一个消费者线程,将分区分配给消费者有以下几种场景。

1.线程数量多于分区的数量,有部分钱程无法消费该主题下任何一条消息。
2.线程数量少于分区的数量,有一些线程会消费多个分区的数据。
3.线程数量等于分区的数量,则正好一个钱程消费一个分区的数据。

一个消费者线程消费多个分区,可以保证消费同一个分区的消息一定是有序的,但并不保证消费者接收到多个分区的消息完全有序。如图3-7所示,消费者分配了分区PO和分区p1,虽然消费者收到的消息整体上不是有序的,但是针对同一个分区的消息是有序的。比如图3-7 (左)中分区内的消息顺序(1)(2)(3)对应的消费者读取顺序也一定是(1)(2((3),图3-7 (右)中分区PO的消息顺序(1)(3)(5)对应的消费者读取顺序也一定是(1)(3)(5)。

消费者与ZK的关系:

消费者除了需要保存消费进度到ZK中,它分配的分区也是从ZK读取的。ZK不仅存储了Kafka的内部元数据,而且记录了消费组的成员列表、分区的消费进度、分区的所有者。

消费者使用代码:

消费者的配置信息要指定连接的ZK集群以及消费组编号。消费者客户端会通过消费者连接器( ConsumerConnector)连接ZK集群,获取分配的分区, 创建每个主题对应的消息流( kafkaStream),最后迭代消息流,读取每条消息,并完成具体的业务处理逻辑。

如图3-8所示,消费者客户端读取消息会创建消息流,然后从消息流中读取消息。那么为消费者分配分区,以及拉取分区消息一定发生在这两个步骤之间,而实际上这些工作都会在消费者连接器中完成。消费者客户端通过消费者连接器读取消息的具体步骤如下。

(1)消费者的配置信息指定订阅的主题和主题对应的线程数, 每个线程对应一个消息流。
(2) Consumer对象通过配置文件创建基于ZK的消费者连接器。
(3)消费者连接器根据主题和线程数创建多个消息流。
(4)在每个消息流通过循环消费者迭代器( ConsumerInterator)读出消息。

 

创建并初始化消费者连接器:

消费者( Consumer) 和消费者连接器接口( ConsumerConnector)在同一个文件中,消费者只需要消费者配置( ConsumerConfig )就可以创建消费者连接器。消费者连接器接口的主要方法有:
createMessageStreams()方法,它创建消息流并返回给客户端应用程序, 这样客户端就会使用消息流读取消息; commitOffset() 方法,它会提交分区的偏移量元数据到ZK 或者Kafka的内部主题中。

为了保证再平衡时各项准备工作都已就绪,创建消费者连接器时,需要执行以下初始化方法。

(1)确保连接上ZK , 因为消费者要和ZK通信,包括保存消费进度或者读取分区信息等。
(2)创建管理所有消费者拉取线程的消费者拉取管理器( ConsumerFetcherManager)。
(3)确保连接上偏移量管理器( OffsetManager),消费者保存消费进度到内部主题时和它通信。
(4)调度定时提交偏移量到ZK或者Kafka 内部主题的线程。

 

消费者客户端的线程模型:

消费者连接器的createMessageStreams() 方法会调用consume () 方法,但consume () 方法并不真正消费数据,而只是为消费消息做准备工作,具体步骤如下。

(1)根据客户端传入的topicCountMap构造对应的队列和消息流,消息流引用了队列。
(2)在ZK 的消费组父节点下注册消费者子节点。
(3)执行初始化工作,触发再平衡,为消费者分配分区,拉取线程会拉取消息放到队列中。
(4)返回消息流列表,队列中有数据时,客户端就可以从消息流中迭代读取消息。

消费者连接拇为了存储拉取线程拉取的消息,本质上还是使用“队列”这种具有缓冲功能的数据结构。将队列封装到消息流中,那么队列和消息流是一一对应的。而根据线程模型,为了保证相同进程内不同线程互相隔离,客户端设置的topi.cCountMap有多少个线程,就对应了多少个队列和消息流。
消费者客户端线程模型的主要概念有消费者线程、队列、消息流, 这三者的关系都是一一对应的。如果将线程模型和服务端的分区再结合起来, 一个线程允许分配多个分区,那么多个分区会共用同一个线程对应的一个队列和一个消息流。

重新初始化消费者:

消费者连接器的consume() 方法在注册消费者到ZK后,调用reinitializeConsumer() 方法执行重新初始化。消费者启动时希望被加入消费组,必须执行一次初始化方法,并触发消费组内所有消费者成员(当然也包括自己)的再平衡。

ZKSessionExpireListener。当新的会话建立或者会话超时需要重新注册消费者,并调用syncedRebalance ()触发再平衡。
ZKTopicPartitionChangeListener。当主题的分区数量变化时,通过rebalanceEventTriggered触发再平衡。
ZKRebalancerListener。当消费组成员变化时, 通过rebalanceEventTriggered发再平衡。

创建分区信息对象:

从ZK中读取出的分区的偏移量, 会被用来构造分区信息对象( PartitionTopicInfo)。分区信息对象的主要内容有:分区,表示拉取线程的“目标” ;队列,作为消息的“存储”介质; 偏移量, 作为拉取“状态” 。消费者的拉取线程会以最新的“状态”拉取ZK的offsetCounter是这个分区最近一次的消费偏移量,也是最新的拉取偏移量。消费者向服务端发起拉取数据请求时,拉取偏移量( fetchOffset )表示要从哪里开始拉取。消费者从服务端拉取消息写到本地后,消费偏移量( consumedOffset )表示消费到了哪里。

分区信息的队列从消费者连接器的topicThreadIdAndQueues 中获得。在消费者连接器的consul'le()方法中, 队列也被用来构造消息流对象,那么拉取线程只要面向分区信息,就能获取到底层的队列,也就可以为消息流的队列填充数据。图3 - 15 总结了队列从创建到填充数据,再到数据被消费的过程,具体步骤如下。

(1)连接器根据订阅信息生成队列和消息流的映射,并且队列也会传给消息流。
(2) 为消费者分配分区时,会从ZK中读取分区消费到的最新位置。
(3) 根据偏移量创建分区信息, 队列也会传给分区信息对象。
(4)分区信息被用于消费者的拉取线程。
(5)拉取线程从服务端的分区拉取消息。
(6)消费者拉取到消息后, 会将最新的偏移量更新到ZK 。

(7)拉取线程将拉取到的消息填充到队列里。
(8)消息流可以从队列里获取消息。
(9)应用程序从消息流里迭代获取消息。

分区信息和队列有关,那么它跟消费者客户端的线程模型也有关:一个消费者线程可以消费多个分区,而一个消费者线程对应一个队列,所以一个队列可以保存多个分区的数据。即对于不同的分区,可能会使用同一个队列来保存消费者拉取到的消息。比如,消费者设置了一个线程就只有一个队列,而分区分了两个给它,这样一个队列就要处理两个分区。图3- 16 (上)是分区信息中队列的数据来源路线,图13-16 (下)展示了分区信息和客户端线程模型的关系。

关闭和更新拉取线程管理器:

再平衡操作中我们已经分析了分区的所有权、分区的分配,剩下和l拉取线程( ConsumerFetcherThread)相关的是: 关闭和更新消费者的拉取线程管理器( ConsumerFetcherManager ,下文简称“拉取管理器”) 。再平衡操作前, closeFetchersForQueues () 方法关闭拉取管理器时,也要关闭它管理的所有线程。除了拉取线程应该关闭, 和拉取线程相关的数据结构也需要清理,比如分区信息对象的队列需要清空。另外, 消费者在拉取数据时会周期性地提交偏移量到ZK 中,在关闭拉取管理器时也要提交一次所有分区的偏移量。

拉取线程管理器:

消费者的拉取管理器( ConsumerFetcherManager)管理了当前消费者的所有拉取线程,这些拉取线程会从服务端的分区拉取消息。前面我们知道每个消费者都会分配到分区信息集合, 这些分区会被拉取管理器的startConnections()方法使用。pa rtitionMap 变量表示被消费者拉取管理辑所管理的分区集合。

Kafka 的生产者和消费者都只能和分区的主副本通信,所以消费者再平衡后分配到分区信息,需要找到分区的主副本。拉取管理器会启动一个后台的LeaderFinderThread 线程, 不断找出已经存在主副本的分区,被选中的分区会被加入对应的拉取线程。

拉取线程的拉取状态:

消费者拉取管理器创建消费者拉取线程时,会把它持有的‘‘代表分配给当前消费者的所哟与分区信息“,即partitionMap全集数据传给每一个拉取线程。因为分区信息对象中的队列会用来存放分区的拉取结果,如果没有把分区信息传给每个拉取线程,拉取钱程就无法获取其中的队列,就没有地方来存放拉取到的消息。如图3-19所示, ZKRebalancerListener将分区信息集合传给拉取管理器(否则管理器也不知道它到底要拉取哪些分区),拉取管理器再把分区信息、集合传给每个拉取线程。

拉取线程流程:

1. 构建拉取请求

拉取管理器后台线程调用拉取线程的addPartitions() 方法, partitionMap变量保存了每个分区的拉取状态。拉取线程的运行方法会根据拉取状态构建井处理拉取请求。

2. 处理拉取请求

消费者和备份副本的拉取工作都一样,拉取线程向服务端拉取消息的步骤如下。
(1) buildFetchRequest(partitionMap) 根据partitionMap构建拉取请求。
(2) fetch(fetchRequest ) 根据拉取请求向目标节点拉取消息,并返回响应结果。
(3) processPartionData(partitionData ) 处理拉取到的分区结果数据。

消费者拉取线程的fetch()方法,通过SimpleConsumer向服务端发起请求并返回所有分区及其数据( PartitionData ),然后处理每个分区的拉取结果。processPartitionData()方法的参数是分区数据的底层消息集,即从服务端拉取到的分区消息对象。它会根据分区得到分区信息对象,调用其enqueue()方法,将消息集包装成数据块( FetchedDataChunk )放入分区信息对象的队列中。

如图3-20所示,分区信息对象作为消费者应用程序和拉取线程的中间桥梁, 保存了“拉取偏移量”和“ 队列”两个重要的信息。拉取偏移量用在拉取钱程中,表示要从分区的什么位置拉取消息,拉取线程拉取到数据后将拉取结果填充到队列中。回顾一下消费者连接器在一开始创建了队列和消息流时,队列是空的。现在,分区信息对象的队列有数据后, 消费者应用程序可以通过消息流从队列中取得数据。

如图3-2 1 所示,总结从分配分区给消费者,到拉取线程拉取消息返回给消费者的具体步骤如下。

(1) 再平衡操作将分区分配给消费者,读取ZK 的偏移量作为分区信息的拉取偏移量。
(2) 分区信息的队列用来存储结果数据,拉取偏移量作为拉取线程初始的拉取位置。
(3) 拉取线程拉取分区的数据,初始时从拉取偏移量开始拉取消息。
(4)    partitionMap 表示分区的最新拉取状态,每次拉取数据后都要更新拉取状态。
(5) 拉取线程创建拉取请求,并通过SimpleConsumer发送请求和接收响应结果。
(6) 拉取钱程技取到分区消息后, 将分区数据的消息集填充到分区信息对象的队列。
(7) 创建消费者连接对象时,会创建队列和消息流, 一个队列关联了一个消息流。
(8) 消费者客户端从消息流中迭代读取结果数据,实际上就是从队列中拉取消息。

消费者消费消息:

消费者拉取钱程拉取每个分区的数据,会将分区的消息集包装成一个数据块( FetchedDataChunk )放入分区信息的队列中。而每个队列都对应一个消息流( KafkaStream ) ,消费者客户端选代消息流,实际上是迭代每个数据块中消息集的每条消息。
如图3-22 所示,一个队列包含多个数据块,每个数据块对应一个分区的消息集, 一个消息集包含多条消息。消费者迭代器( ConsumerIterator)封装了迭代获取消息的逻辑,客户端不需要面向数据块、消息集这些内部对象,只需要对消费者迭代器循环获取消息即可。

(1) 消费者的拉取线程从服务端拉取分区的消息。
(2) 拉取到分区消息后,就更新分区信息对象的拉取偏移量。
(3) 将分区数据的消息集封装成数据块。
(4) 客户端循环迭代数据块的消息集。
(5) 消费完一条消息后,就更新分区信息对象的消费偏移量。

(6)消息流中的每一条消息返回给消费者客户端应用程序。

 

 

展开阅读全文

没有更多推荐了,返回首页