针对 Java NIO 的 SocketChannel,kafka 是如何封装统一的传输层来实现最基础的网络连接以及读写操作的?
剖析 KafkaChannel 是如何对传输层、读写 buffer 操作进行封装的?
剖析工业级 NIO 实战:如何基于位运算来控制事件的监听以及拆包、粘包是如何实现的?
剖析 Kafka 是如何封装 Selector 多路复用器的?
剖析 Kafka 封装的 Selector 是如何初始化并与 Broker 进行连接以及网络读写的?
剖析 Kafka 网络发送消息和接收响应的整个过程是怎样的?
认真读完这篇文章,我相信你会对 Kafka 网络层源码有更加深刻的理解。
这篇文章干货很多,希望你可以耐心读完。
01 总体概述
通过场景驱动的方式,在网络请求封装和监听好后,我们来看看消息是如何进行网络收发的,都需要做哪些工作。
发送消息流程剖析
消息预发送
消息真正发送
接收响应流程剖析
读取响应结果
解析响应信息
处理回调
为了方便大家理解,所有的源码只保留骨干。
02 发送消息流程剖析
02.1 消息预发送
这部分涉及的东西比较多,此处就简单的说明下,后续会有专门篇章进行剖析。
客户端先准备要发送的消息,流程如下:
Sender 子线程会从 RecordAccumulator 缓冲区拉取要发送的消息集合,抽取到的数据会存放到下面几个地方:
发送时会放入 inFlightRequests 集合和 KafkaChannel 的 send 对象,其中 inFlightRequests 后续篇章再进行剖析,这里简单说明下,该集合用来存储和操作待发送消息的缓存区,当请求准备网络发送时,会把请求从队头放入队列;当接收到响应后,会把请求从队尾删除。
待发送完成后会放入 completedRequests 集合。
对已经过期的数据进行处理。
封装客户端请求 ClientRequest,把 ClientRequest 类对象发送给 NetworkClient,它主要有以下2个工作要做:
根据 ClientRequest 类对象构造 InFlightRequest 类对象。
根据 ClientRequest 类对象构造 NetworkSend 类对象,并放入到 KafkaChannel 的缓存里。
此时消息预发送结束。
接下来我们依次看下 Selector 和 KafkaChannel 类的具体源码实现。
02.1.1 请求数据暂存内存中
github 源码地址如下:
https://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/common/network/Selector.java
https://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/common/network/KafkaChannel.java
/**
* 消息预发送
*/
public void send(Send send) {
// 1. 从服务端获取 connectionId
String connectionId = send.destination();
// 2. 从数据包中获取对应连接
KafkaChannel channel = openOrClosingChannelOrFail(connectionId);
// 3. 如果关闭连接集合中存在该连接
if (closingChannels.containsKey(connectionId)) {
// 把 connectionId 放入 failedSends 集合里
this.failedSends.add(connectionId);
} else {
try {
// 4. 暂存数据预发送,并没有真正的发送,一次只能发送一个
channel.setSend(send);
} catch (Exception e) {
// 5. 更新 KafkaChannel 的状态为发送失败
channel.state(ChannelState.FAILED_SEND);
// 6. 把 connectionId 放入 failedSends 集合里
this.failedSends.add(connectionId);
// 7. 关闭连接
close(channel, CloseMode.DISCARD_NO_NOTIFY);
...
}
}
}
从源码中可以看到调用了 KafkaChannel 类的 setSend() 方法。
public void setSend(Send send) {
if (this.send != null)
throw new IllegalState