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

1,324 阅读6分钟

写在前面

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

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

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

关系概要

saramaClient-导出.png

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

partitionConsumer 的创建

当为一对 Topic/Partition 订阅去创建 ConsumerGroupClaim 时,会调用 ConsumePartition 方法,去创建 partitionConsumer 实例。 企业微信截图_13173fa6-c9e4-4279-81df-fd7c2bc6ead5.png 在 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 的消息传递。这边放在后面分析。

企业微信截图_97074302-60fa-4124-94af-b639911c61e7.png

brokerConsumer 的创建和消费的开启。

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

企业微信截图_4772c777-4c92-447d-8ae8-72ffa185d8d0.png

接下来,我们通过 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 函数。

企业微信截图_e66adb29-4fe1-4515-a9ae-a50aa0f2233e.png

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 方法了。

企业微信截图_312826dd-1d2e-4a45-84e0-c22ecd7ebe53.png

responseFeeder

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

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

企业微信截图_76a706a7-afde-4275-9bd4-d80446aa4e9b.png

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

由此,从 ConsumerGroupClaim 的创建到消息拉取,再到执行回调函数 ConsumerClaims 进行消息处理,整个链路就闭环了。 企业微信截图_98303b02-a9a9-496f-8254-79c63622c66d.png

dispatcher

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

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

企业微信截图_2a35c8fc-48b4-4b5c-9725-f3392f856455.png

整体关系

未命名文件-导出.png 在经过上述分析后,我们大致可以绘制出上图,来概括 partitionConsumer 和 brokerConsumer 的关系

partitionConsumer和brokerConsumer退出消费

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

企业微信截图_bf675a85-8c26-4cb7-9b2c-96d1d8344fe6.png 企业微信截图_bb583c16-f7a8-4b56-afc0-3e62ecee3846.png

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

企业微信截图_55a13525-207d-4566-9cc8-068926608516.png

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

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

企业微信截图_f5f1e83d-620b-443d-b4d1-ba6b629a0902.png

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

总结

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