
图解 Kafka 网络层源码实现机制之收发消息全过程
作者 | 王江华
来源 | 华仔聊技术(ID:gh_97b8de4b5b34)
大家好,我是 华仔, 又跟大家见面了。
在上一篇中,主要带大家深度剖析了「Kafka 对多路复用器 Selector」的封装全过程,今天我们主要对 Kafka 网络层收发流程进行总结下,本系列总共分为3篇,这是下篇,主要剖析最后一个问题:
- 针对 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.javahttps://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/common/network/KafkaChannel.java
从源码中可以看到调用了 KafkaChannel 类的 setSend() 方法。
该方法主要用来预发送,即在发送网络请求前,将需要发送的ByteBuffer 数据保存到 KafkaChannel 的 send 中,然后调用传输层方法增加对这个 channel 上「OP_WRITE」事件的关注,同时还保留了「OP_READ」事件,此时该 Channel 是同时可以进行读写的。当真正执行发送的时候,会先从 send 中读取数据。
02.2 消息真正发送
Sender 子线程会调用 Selector 的 「poll」方法把请求真正发送出去。
02.2.1 poll()
该方法就干了一件事,即收集准备就绪事件,并针对事件进行网络操作,通过上述简化代码可以看出是调用了 「pollSelectionKeys」 方法,真正读写操作在该方法中,我们来看看:
02.2.2 pollSelectionKeys()
该方法主要用来处理监听到的事件,包括连接事件、读写事件、以及立即完成的连接的。接下来我们看看尝试进行网络写操作,如何才能进行真正写。
02.2.3 attemptWrite()
该方法主要用来尝试进行网络写操作,方法很简单,必须「同时满足4个条件」:
「channel 还有数据可以发送」即数据还未发送完成。
「channel 连接就绪」。
「写事件是可写状态」只要写缓冲区未写满会一直产生「OP_WRITE」 事件,如果不写数据或者写满时则需要取消 「OP_WRITE」 事件,防止产生不必要的资源消耗。
「客户端验证没有开启」。
当满足以上4个条件后就可以进行写操作了,接下来我们看看写操作的过程。
02.2.4 write()
该方法主要用来真正执行网络写操作的,大家知道在网络编程过程中,不一定一次性可以发送完成,此时就需要判断是否发送完成,如果未完成返回null,「等待下次轮询 poll() 会继续发送,并继续关注这个 channel 的写事件」,如果发送完成,「则返回 send,并取消 Selector 在这个 socketchannel 上 OP_WRITE 事件的关注」。这里调用了 KafkaChannel 类的 write() 进行写操作发送,并调用 maybeCompleteSend() 判断是否发送完成,我们先来看下 write() 写操作:
02.2.6 KafkaChannel.write()
该方法主要用来把保存在 send 上的数据真正发送出去,调用 ByteBufferSend.writeTo 把数据真正发送出去,我们来看看 wirteTo() 方法:
该方法主要用来把 buffers 数组写入到 SocketChannel 里,因为在网络编程中,写一次不一定可以完全把数据都写成功,所以调用java nio 底层 channel.write(buffers) 方法会返回「已经写入成功多少字节」的返回值,这样调用一次后就知道已经写入多少字节了。
当调用 write() 以及一系列底层方法进行写操作后,会返回已经发出的字节数,如果这次没有发送完毕则返回 null,「等待下次轮询 poll 继续发送网络写操作,并继续关注这个 channel 的写事件」,所以需要判断下本次是否发送完毕了,我们来看看:
02.2.7 maybeCompleteSend()
该方法主要用来判断是否写数据完毕了,而判断的写数据完毕的条件是 buffer 中 remaining 没有剩余且 pending 为 false。如果发送完成,把发送完成的请求添加到发送完成的集合 completedSends 里。
待消息请求发送完成后,又做了哪些工作呢?这里涉及到 NetworkClient 类的相关知识,这里简单说明下,后续再剖析:
github 源码地址如下:
从源码可以看出会对「completedSends」集合和「inFlightRequests」集合是一个「互相协作」的关系。
其中「completedSends」集合是指发送完成但还没有返回的请求集合,而「inFlightRequests」集合则是保存了已经发送出去但还没有收到响应结果的 Request 集合。其中「completedSends」的元素对应着「inFlightRequests」集合对应队列的最后一个元素。
到此发送消息流程剖析完毕,至于发送完成后续工作,我们待讲解 Sender 和 NetWorkClient 的时候再详细进行剖析,接下来我们来看看接收响应流程。
03 接收响应流程剖析
在上面剖析 Selector.pollSelectionKeys() 时候,当网络读事件就绪后会调用 attemptRead() 进行尝试网络读操作,我们来看看:
03.1 读取响应结果
03.1.1 attemptRead()
该方法主要用来尝试读取数据并添加已经接收完毕的集合中。我们看到会先调用 KafkaChannel.read() 方法进行读取,然后判断是否读完了,如果没有读完,下次轮询时候接着读取,如果读完了就假如到请求读完的集合 completedReceives 中。
我们来看下是如何判断 NetworkReceive 对象是否已经读完了的:
03.1.2 maybeCompleteReceive()
该方法主要用来判断数据已经读取完毕了,而判断是否读完的条件是 NetworkReceive 里的 buffer 是否用完,包括上面说过的表示响应消息头 size ByteBuffer 和响应消息体本身的 buffer ByteBuffer,这两个都读完才算真正读完了。
如果此时并没有读完一个完整的 NetworkReceive 对象,则下次触发读事件会继续填充整个 NetworkReceive 对象,如果此时读完一个完整的NetworkReceive 对象则将其置空,下次触发读事件时会创建一个全新的NetworkReceive 对象。
03.2 解析响应消息
等读取完一个完整响应消息后,接下来要做哪些工作呢?那就是要解析这个响应消息,我们来看看是如何实现的:
github 源码地址如下:
该方法主要用来循环遍历 completedReceives 集合做一些响应处理工作,在文章开始的时候就简单说过,收到响应后会将其从「inFlightRequests」中删除掉,然后去解析这个响应:
该方法主要用来解析响应的,并判断响应头跟响应体的 correlationId 值是否一致,否则抛异常。
此时只对响应做了解析但并没有对响应进行处理,而响应处理是通过调用回调方法进行处理的,我们来看下。
03.3 处理回调
到此接收响应消息流程剖析完毕。
04 总结
这里,我们一起来总结一下这篇文章的重点。
1、带你先整体的梳理了 Kafka 网络层收发流程,主要分为「发送消息流程」和「接收响应流程」。
2、又带你分别剖析了发送消息流程和接收响应流程的源码实现细节。
