
图解 Kafka 源码之 NetworkClient 网络通信组件架构设计(上篇)
阅读本文大约需要 40 分钟。
大家好,我是 华仔, 又跟大家见面了。
上篇主要带大家深度剖析了「发送网络 I/O 的 Sender 线程的架构设计」,消息先被暂存然后调用网络I/O组件进行发送,今天主要聊聊「真正进行网络 I/O 的 NetworkClient 的架构设计」深度剖析下消息是如何被发送出去的。
认真读完这篇文章,我相信你会对 Kafka NetworkClient 的源码有更加深刻的理解。
这篇文章干货很多,希望你可以耐心读完。
01 总的概述
继续通过「场景驱动」的方式,来看看消息是如何在客户端被累加和待发送的。
在上篇中,我们知道了消息被 Sender 子线程先暂存到 KafkaChannel 的 Send 字段中,然后调用 NetworkClient#client.poll() 进行真正发送出去,如下图所示「6-11步」。
NetworkClient 为「生产者」、「消费者」、「服务端」等上层业务提供了网络I/O的能力。在 NetworkClient 内部使用了前面介绍的 Kafka 对 NIO 的封装组件,同时做了一定的封装,最终实现了网络I/O能力。NetworkClient 不仅仅用于客户端与服务端的通信,也用于服务端之间的通信。
接下来我们就来看看,「NetworkClient 网络I/O组件的架构实现以及发送处理流程」,为了方便大家理解,所有的源码只保留骨干。
02 NetworkClient 架构设计
NetworkClient 类是 KafkaClient 接口的实现类,它内部的重要字段有「Selectable」、「InflightRequest」以及内部类 「MetadataUpdate」。
github 源码地址如下:
02.1 关键字段
从该类属性字段来看比较多,这里说几个关键字段:
- selector:Kafka 自己封装的 Selector,该选择器负责监听「网络I/O事件」、「网络连接」、「读写操作」。
- metadataUpdater:NetworkClient 的内部类,主要用来实现Metadata元信息的更新器, 它可以尝试更新元信息。
- connectionStates:管理集群所有节点连接的状态,底层使用 Map<nodeid, NodeConnectionState>实现,NodeConnectionState 枚举值表示连接状态,并且记录了最后一次连接的时间戳。
- inFlightRequests:用来保存当前正在发送或等待响应的请求集合。
- socketSenderBuffer:表示套接字发送数据的缓冲区的大小。
- socketReceiveBuffer:表示套接字接收数据的缓冲区的大小。
- clientId:表示客户端id,标识客户端身份。
- reconnectBackoffMs:表示重连的退避事件,为了防止短时间内大量重连造成的网络压力,设计了这么一个时间段,在此时间段内不得重连。
02.2 关键方法
NetworkClient 类的方法也不少,这里针对关键方法逐一讲解下,原文完整版在星球里,感兴趣的可以扫文末二维码加入。
02.2.1 ready()
该方法表示某个节点是否准备好并可以发送请求,主要做了三件事:
- 先判断节点是否已经准备好连接并接收请求了,需要满足以下四个条件:
● !metadataUpdater.isUpdateDue(now):不能是正在更新元数据的状态,且元数据不能过期。
● canSendRequest(node.idString(), now):此处有3个条件。(1)、客户端和 node 连接是否处于 ready 状态;(2)、客户端和 node 的 channel 是否建立好;(3)、inFlightRequests 中对应的节点是否可以接收更多的请求。
- 如果连接好返回 true 表示准备好,如果没有准备好接收请求,则会尝试与对应的 Node 连接,此处也需要满足两个条件:
● 首先连接必须是 isDisconnected,不能是 connecteding 状态,即客户端与服务端的连接状态是没有连接上。
● 两次重试之间时间差要大于重试退避时间,目的就是为了避免网络拥塞,防止重连过于频繁造成网络压力过大。
3. 最后初始化连接。
02.2.2 initiateConnect()
该方法主要是进行初始化连接,做了两件事:
- 调用 connectionStates.connecting() 更新连接状态为正在连接。
- 调用 selector.connect() 异步发起连接,此时不一定连接上了,后续 Selector.poll() 会监听连接是否准备好并完成连接,如果连接成功,则会将 ConnectionState 设置为 CONNECTED。
当连接准备好后,接下来我们来看下发送相关的方法。
02.2.3 send()、doSend()
从上面源码可以看出此处发送并不是真正的网络发送,而是先将数据发送到缓存中。
- 首先最外层是 send() ,里面调用 doSend() 。
- 这里的 doSend() 主要的作用是判断 inFlightRequests 集合上对应的节点是不是能发送请求,需要满足三个条件:
● 客户端和 node 连接是否处于 ready 状态。
● 客户端和 node 的 channel 是否建立好。
● inFlightRequests 集合中对应的节点是否可以接收更多的请求。
- 最后再次调用另一个 doSend(),用来最终的请求发送到缓存中。步骤如下:
● 构建 NetworkSend 对象 结合请求头和请求体,序列化数据,保存到 NetworkSend。
● 构建 inFlightRequest 对象。
● 把 inFlightRequest 加入 inFlightRequests 集合里等待响应。
● 调用Selector异步发送数据,并将 send 和对应 kafkaChannel 绑定起来,并开启该 kafkaChannel 底层 socket 的写事件,等待下一步真正的网络发送。
综上可以得出这里的发送过程其实是把要发送的请求先封装成 inFlightRequest,然后放到 inFlightRequests 集合里,然后放到对应 channel 的字段 NetworkSend 里缓存起来。总之,这里的发送过程就是为了下一步真正的网络I/O发送而服务的。
接下来看下真正网络发送的方法。
02.2.4 poll()
该方法执行网络发送并把响应结果「pollSelectionKeys 的各种读写」做各种状态处理,此处是通过调用 handleXXX() 方法进行处理的,代码如下:
这里的步骤比较多,我们按照先后顺序讲解下。
- 尝试更新元数据。
- 调用 Selector.poll() 执行真正网络 I/O 操作,可以点击查看 图解 Kafka 源码网络层实现机制之 Selector 多路复用器 主要操作以下3个集合。
● connected集合:已经完成连接的 Node 节点集合。
● completedReceives集合:接收完成的集合,即 KafkaChannel 上的 NetworkReceive 写满后会放入这个集合里。
● completedSends集合:发送完成的集合,即 channel 上的 NetworkSend 读完后会放入这个集合里。
- 调用 handleCompletedSends() 处理 completedSends 集合。
- 调用 handleCompletedReceives() 处理 completedReceives 队列。
- 调用 handleDisconnections() 处理与 Node 断开连接的请求。
- 调用 handleConnections() 处理 connected 列表。
- 调用 handleInitiateApiVersionRequests() 处理版本号请求。
- 调用 handleTimedOutConnections() 处理连接超时的 Node 集合。
- 调用 handleTimedOutRequests() 处理 inFlightRequests 集合中的超时请求,并修改其状态。
- 调用 completeResponses() 完成每个消息自定义的响应回调。
接下来看下第 3~9 步骤的方法实现。
02.2.5 handleCompletedSends()
当 NetworkClient 发送完请求后,就会调用 handleCompletedSends 方法,表示请求已经发送到 Broker 端了。
该方法主要用来在客户端发送请求后,对响应结果进行处理,做了五件事:
- 遍历 seletor 中的 completedSends 集合,逐个处理完成的 Send 对象。
- 从 inFlightRequests 集合获取该 Send 关联对应 Node 的队列中第一个元素,但并没有从队列中删除,取出后判断这个请求是否期望得到响应。
- 判断是否需要响应。
- 如果不需要响应就删除 inFlightRequests 中该 Sender 关联对应 Node 的 inFlightRequest,对于 Kafka 来说,有些请求是不需要响应的,对于发送完不用考虑是否发送成功的话,就构建 callback 为 null 的 Response 对象。
- 通过 InFlightRequest.completed(),生成 ClientResponse,第一个参数为 null 表示没有响应内容,最后把 ClientResponse 添加到 Responses 集合。
从上面源码可以看出,「completedSends」集合与「InflightRequests」集合协作的关系。
但是这里有个问题:如何保证从 Selector 返回的请求,就是对应到 InflightRequests 集合队列的最新的请求呢?
completedSends 集合保存的是最近一次调用 poll() 方法中发送成功的请求「发送成功但还没有收到响应的请求集合」。而 InflightRequests 集合存储的是已经发送但还没收到响应的请求。每个请求发送都需要等待前面的请求发送完成,这样就能保证同一时间只有一个请求正在发送,因为 Selector 返回的请求是从上一次 poll 开始的,这样就对上了。
「completedSends」的元素对应着「InflightRequests」集合里对应队列的最后一个元素, 如下图所示:
02.2.6 handleCompletedReceives()
当 NetworkClient 收到响应时,就会调用 handleCompletedReceives 方法。
该方法主要用来处理接收完毕的网络请求集合,做了五件事:
- 遍历 selector 中的 completedReceives 集合,逐个处理完成的 Receive 对象。
- 获取发送请求的 Node id。
- 从 inFlightRequests 集合队列获取已发送请求「最老的请求」并删除(从 inFlightRequests 删除,因为inFlightRequests 存储的是未收到请求响应的 ClientRequest,现在请求已经有响应了,就不需要保存了)。
- 解析响应,并且验证响应头,生成 responseStruct 实例,生成响应体。
- 处理响应结果,此处分为三种情况:
● 处理元数据请求响应,则调用 metadataUpdater.handleSuccessfulResponse()。
● 处理版本协调响应,则调用 handleApiVersionsResponse()。
● 普通发送消息的响应,通过 InFlightRequest.completed(),生成 ClientResponse,将响应添加到 responses 集合中。
从上面源码可以看出,「completedReceives」集合与「InflightRequests」集合也有协作的关系, completedReceives 集合指的是接收到的响应集合,如果请求已经收到响应了,就可以从 InflightRequests 删除了,这样 InflightRequests 就起到了可以防止请求堆积的作用。
与 「completedSends」正好相反,「completedReceives」集合对应 「InflightRequests」集合里对应队列的第一个元素,如下图所示:
文章转载自公众号:华仔聊技术
