写在前面
上一章中介绍了 partitionConsumer 和 brokerConsumer 二者的协作。
其中提到,partitionConsumer 根据自己订阅的 Topic/Partition,从 Client 中找到相应的 Broker 实例,并查找或创建 brokerConsumer。
进一步的我们知道,brokerConsumer 下的 Broker 实例才是和 Kafka集群Broker 交互的组件,负责调用 Kafka的API,拉取消息。
本章介绍 Client 实例是怎么管理这些 Broker 实例的。
关系概要
Client 实例中有两个至关重要的成员变量:
- brokers:map类型变量,通过 BrokerID 索引每个Broker实例。
- metadata:map类型变量,通过 Topic/Partition 索引到存储每个 Partition 元数据的结构体 PartitionMetadata。在 PartitionMetadata 结构体中,有一个 Leader 变量,它的含义就是,当前 Partitions 中,Leader 分区所在的那个 Kafka集群Broker 的 BrokerID。而进一步的,这个 Leader 所代表的 BrokerID 就是 brokers 成员变量中的索引。
这些有关Kafka集群中Broker和主题分区的元数据,它们都是通过协程的方式,定时触发调用 Kafka 的 GetMetadata API来获取的。这个协程的调用,就在 Client 实例的创建阶段完成。
Client的创建
在专题的第二章中提到,在创建 ConsumerGroup 实例之前,会调用 NewClient 方法创建出 Client 实例。
NewClient 方法在创建出 Client 实例后,会:
- 根据传入的Kafka集群中Broker的地址列表,调用 randomizeSeedBrokers 方法,为每个地址生成一个 Broker 实例。这些 Broker 实例并不参消息拉取,而是做为"种子Brokers"(seedBrokers),用来在初次拉取Kafka集群元数据时,提供调用Kafka API的接口函数。
- 通过
go withRecover(client.backgroundMetadataUpdater)
开启一个协程。
元数据定时更新
协程中运行的 backgroundMetadataUpdater 方法,通过 ticker 定时器配合循环 select 监听的方式,持续间歇性调用 refreshMetadata 方法。
这个 refreshMetadata 方法,最终经过下层的调用链,会去执行 tryRefreshMetadata 方法。在这个方法中, 主要做以下几件事:
- LeastLoadedBroker:根据一定规则,选取一个可用的 Broker 实例(可能从前面提到的"种子Brokers"中获取),作为Kafka API的调用者。
- GetMetadata:使用上一步选取出的 Broker 实例,调用 GetMetadata API,获取 Kafka 集群中Broker和所有主题/分区的元数据信息。其中包括BrokerID,主题名称,分区ID和分区Leader的信息。
- updateMetadata:该方法中,根据上一步中获取到的元数据信息,对 Client 实例的 brokers 和 metadata 这两个成员变量进行更新。代码较为简单,这里不过多赘述。
分析至此,我们知道 Client 实例通过定期更新的方式,去拉取Kafka集群的元数据。不断的刷新 Broker 实例和 Topic/Partition 元数据信息。为 sarama 的消费者的每个 Topic/Partition 订阅,提供可用的 Broker 实例。
Broker拉取消息
在上一章中,我们提到 brokerConsumer 调用 fetchNewMessages 去拉取消息。在完成 request 的构建后,最后就是通过 brokerConsumer 所属的成员变量 broker 调用KafkaAPI Fetch去完成消息的拉取。
接下来我们以 Fetch 为例,介绍 Broker 的request和response过程。
sendAndReceive
在 Broker 调用 Kafka API 发送请求时,都会调用 sendAndReceive 方法,在这个方法中,完成 request 和response 的处理。
- send :send 方法会将请求进行发送,并返回一个叫做 promise 变量。它是
*responsePromise
类型的变量,指向 responsePromise 实例。responsePromise 实例中两个 channel,分别是 packages 和 errors。它们分别用来传送 Kafka API 返回的正常结果或者错误结果。 - handleResponsePromise :handleResponsePromise 方法中会通过 select 监听 promise,得到请求结果或者错误结果。
发送request
send 方法首先初始化 promise 实例,并调用 sendWithPromise 方法
sendWithPromise 方法中会去调用 sendInternal 去发送请求。
sendInternal 方法首先会将 request 序列化,然后调用b.write(buf)
将请求数据写入操作系统 socket。
而接下来一个很重要的是,通过自增 correlationID 生成 Kafka API请求的唯一ID,然后将 correlationID 赋值给 promise 实例。
然后将 promise 实例写入 Broker 实例的 responses channel。这样请求就发送完毕了。
bash
CorrelationId,int32类型,由客户端指定的一个数字唯一标示这次请求的id,
服务器端在处理完请求后也会把同样的CorrelationId写到Response中,
这样客户端就能把某个请求和响应对应起来了。
转载自 --- https://www.zhihu.com/question/436617723
接收response
Broker 实例中有一个叫做 responseReceiver 方法,它会通过 responses channel 接收 promise 实例。 在接收到新的 promise 实例后,会通过b.readFull(header)
方法,阻塞读取 response 数据。 在读取到 Kafka 服务端返回的结果后,会将 response 中的 correlationID 和 promise 的 correlationID 对比,如果两者相同,则表示当前结果为这个 promise 等待的请求 response 数据。
在确认 correlationID 后,会执行response.handle
(其实就是执行 promise 实例的 handle 方法)处理 response 数据。
在 handle 方法中,会根据请求结果,将数据放入 promise 实例的 packets channel 或者 errors channel 中。前文中 sendAndRevice 方法中调用的 handleResponsePromise 方法,就是在监听 promise 的这两个通道去接收请求结果。
到此为止还有一个问题,就是 responseReceiver 方法是何时被调用的?
这个答案藏在上一篇中 pratitionConsumer 创建并匹配 brokerConsumer 实例时调用的 LeaderAndEpoch 方法中。
LeaderAndEpoch 会调用 cachedLeader 方法,该方法在通过 Topic/Partition 订阅成功匹配到 Broker 实例后,会调用 Broker 下的 Open 方法。
在 Broker 的 Open 方法中会做三件事:
- 创建 Broker 实例和 Kafka集群 Broker 的TCP连接。
- 创建 responses 通道,用于 promise 的传送。
- 通过执行
go withRecover(b.responseReceiver)
,调用 responseReceiver 方法,去监听 responses 通道,处理 promise。
以上步骤就是Broker与Kafka服务端的交互过程。Broker 的运行机制还是较为复杂的。
总结
本章分析了 sarama ConsumerGroup 源码中涉及到的最后两个组件:Client 和 Broker。对于 ConsumerGroup 的源码分析也就结束了。