kafka golang客户端sarama源码分析」--- 3. partitionConsumer和brokerConsumer的协作

写在前面

经过上一章中的分析,我们知道 ConsumerGroupSession 负责维护消费者的心跳和消费的起停,而 ConsumerGroupClaim 作为信使向用户层的回调函数返回拉取到的消息。

那么这一章中,我们看看 ConsumerGroupClaim 的下层组件 partitionConsumer 和 brokerConsumer是如何工作的。简单概括:各个 partitionConsumer 实例(一个 partitionConsumer 实例管理一个 Topic/Partition 订阅对),按订阅对中 leader 副本所在的 Kafka集群Broker 来进行分组,相同 Kafka集群Broker 为一组,交给 brokerConsumer 统一管理并批量的拉取消息。

一言蔽之:按 leader 划分,走批量消费

关系架构

根据上一章分析可知,Consumer 实例与 ConsumerGroup 实例一同创建。Consumer 负责维护 partitionConsumer 和 brokerConsumer 的 Consumer 实例。具体请参考上图。该实例下由 children 成员变量来管理 partitionConsumer 实例,由 brokersConsumers 成员变量来管理 brokersConsumer 实例。

partitionConsumer 的创建

当为一对 Topic/Partition 订阅去创建 ConsumerGroupClaim 时,会调用 ConsumePartition 方法,去创建 partitionConsumer 实例。 在 ConsumePartition 方法中

  • addChild:该方法主要作用就是将新建的 partitionConsumer 实例,根据其订阅的 Topic/Parttion,加入到 Consumer 实例的成员变量 children 中去。
  • LeaderAndEpoch:该方法由 Client 实例提供,它会返回一个 Broker 实例。我们知道,Client 实例下管理着 Kafka 集群下所有Broker 的抽象:Broker 实例。LeaderAndEpoch 就是通过当前 Topic/Parttion 订阅信息,找到 leader 副本所属的那个 Kafka集群Broker在 Client 中对应的 Broker 实例。Kafka 中只有 leader 副本对外提供消息拉取,因此这里只有找到 leader 副本所属的 Broker 实例,才能进行后续的消费。
  • refBrokerConsumer:根据 LeaderAndEpoch 中返回的 Broker 实例(leader),在 Consumer 下的 brokerConsumers map中查找或者创建(如果未找到)一个 brokerConsumer 实例,并返回。
贴士 复制代码
go withRecover(child.dispatcher)  
go withRecover(child.responseFeeder)

child.broker.input <- child

这三段代码非常重要,会开启 partitionConsumer 和其所属的 brokerConsumer 的消息传递。这边放在后面分析。

brokerConsumer 的创建和消费的开启。

在 refBrokerConsumer 方法中如果根据 Topic/Partition 未找到可用的 brokerConsumer 实例,那么就会调用 newBrokerConsumer 方法创建一个全新的 brokerConsumer 实例。可以看到,brokerConsumer 实例中的 broker 成员变量,就是一个 Broker 实例。refBrokerConsumer 在调用 newBrokerConsumer 传入的 *Broker 入参,就是由 LeaderAndEpoch 方法返回的。

接下来,我们通过 brokerConsumer下的 subscriptionManager 和 subscriptionConsumer 方法,来逐步分析消息拉取是如何启动的,前者做为触发器通过 channel 定时的触发后者执行消息拉取。

subscriptionManager

该函数在 newBrokerConsumer 中创建完成 brokerConsumer 实例后就由 goroutine 开启调用。 该方法的主要作用有二:

  • 监听input channel:这个 input channel 用于传递通过当前 brokerConsumer 来执行消息拉取的 partitionConsumer 实例。当 partitionConsumer 实例创建完成时,会通过child.broker.input <- child ,将 partitionConsumer 实例通过 channel 传递给 brokerConsumer。
  • 通过 timer 定时触发消费:通过定时器 timer 和 batchComplete 的控制,在一段时间内持续从 input channel 中收集新的 partitionConsumer。当 定时器 timer 超时后,通过控制 batchComplete 变量跳出循环,将新收集到的 partitionConsumer,bc.newSubscriptions <- partitionConsumers 传递给 newSubscriptions channel。 这里需要注意,当未收集到任何新的 partitionConsumer 时,也会向 bc.newSubscriptions 写入一个空值,用来强制触发监听 bc.newSubscriptions 的 subscriptionConsumer 函数。

subscriptionConsumer

newSubscriptions channel 其实就是由 subscriptionConsumer 方法进行监听的。它的主要工作如下:

  • subscriptionConsumer 方法会持续通过 newSubscriptions channel 收集并通过 updateSubscriptions 方法存储/更新 partitionConsumer 实例。updateSubscriptions 方法会将 partitionConsumer 保存到 subscriptions 变量中。
  • 通过 fetchNewMessages 方法,合并 subscriptions 变量中各个 partitionConsumer 的 Topic/Partition 信息。合并完成后,通过调用 Broker 实例的 Fetch 方法,批量的拉取消息。
  • 在 fetchNewMessages 方法 调用成功后,迭代 subscriptions 变量,找到其中的各个 partitionConsumer 实例,通过 partitionConsumer 实例下的 feeder 通道,将拉取到的消息返回给 partitionConsumer。

那么 partitionConsumer 是怎么监听 feeder 通道去把消息进一步处理的?这里就需要提到在创建 partitionConsumer 后,通过协程调用的 responseFeeder 方法了。

responseFeeder

该方法在 partitionConsumer 实例创建完成后,通过 go withRecover(child.responseFeeder) 来调用。 它的主要作用如下:

  • 通过 for 循环,持续监听 feeder 通道,接收由 brokerConsumer 的 subscriptionConsumer 函数拉取来的消息。
  • 在拉取到消息后,通过 parseResponse 函数对消息进行初步封装处理后,通过child.messages <- msg将消息投递到 partitionConsumer 实例的 messages channel中。

这个 messages channel,就是在用户回调函数 ConsumerClaims 中,通过 ConsumerGroupClaim 实例的 Messages 方法,返回给用户的。我们知道 ConsumerGroupClaim 在实例化时,会通过结构体嵌入的方式,嵌入 partitionConsumer 结构体的实例。因此,在用户回调函数中,可以直接使用 Messages 方法。

由此,从 ConsumerGroupClaim 的创建到消息拉取,再到执行回调函数 ConsumerClaims 进行消息处理,整个链路就闭环了。

dispatcher

该函数也是在 partitionConsumer 实例化后,通过go withRecover(child.dispatcher)调用的。这个函数回持续监听 trigger 通道。在 brokerConsumer 的一些处理过程中,会往 trigger 通道写入对象,去触发 dispatcher 函数的执行。

但对于 trigger 通道具体的作用,在写本篇时,笔者也并没有搞明白。因此就不做过多介绍了,此处留白,请各路掘友大佬们补充。非常感激。

整体关系

在经过上述分析后,我们大致可以绘制出上图,来概括 partitionConsumer 和 brokerConsumer 的关系

partitionConsumer和brokerConsumer退出消费

在上一篇中,我们简单分析了由用户层 context 和session层 context 通过取消信号,销毁 ConsumerGroupCliams 并退出的大致链路。 那么,ConsumerGroupCliam 下管理的 partitionConsumer 和 brokerConsumer 是如何感知到取消信号退出消费的呢?大致的执行链路如下。 首先,在 ConsumerGroupCliam 监听到取消信号后,会调用 AsyncClose 方法,告知其下工作组件停止消费,而 AsyncClose 的真正实现这是 partitionConsumer 实例。

在调用 AsyncClose 方法后,会关闭 partitionConsumer 的 dying 通道。dispatcher 方法在监听到 dying 通道关闭时,会关闭 trigger 通道,退出循环。close(child.feeder),关闭 feer 通道。 feer通道的关闭,就意味着 responseFeeder 退出 partitionConsumer 停止接收 brokerConsumer 投递的最新拉取的消息。

在 responseFeeder 退出时,会关闭 messages 通道,那么在回调函数 ConsumerClaims 这里,便可感知到消息读取关闭。

在 brokerConsumer 的 updateSubscriptions 方法(具体查看上文对 subscriptionConsumer 的介绍)中,同样会监听实例下各个 partitionConsumer 的 dying 通道是否关闭。如果关系,delete(bc.subscriptions, child),将被关闭的 partitionConsumer 从当前 brokerConsumer 下删除。这样就不会再去拉取相应 Topic/Partition 的消息了。

partitionConsumer和brokerConsumer退出消费的链路大致如上,过程还是相对比较复杂的。

总结

本篇着重分析了 partitionConsumer 和 brokerConsuemr 的关系,以及它们是如何协同工作拉取消息,并最终传递给用户进行处理的。下一篇会对 Client 和 Broker 的工作机制进行讲解。

相关推荐
刘大辉在路上2 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
lucky_syq3 小时前
Flume和Kafka的区别?
大数据·kafka·flume
观测云3 小时前
Confluent Cloud Kafka 可观测性最佳实践
kafka·confluent
攻心的子乐3 小时前
Kafka可视化工具 Offset Explorer (以前叫Kafka Tool)
分布式·kafka
追逐时光者4 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~5 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581365 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳5 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾5 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala