图解 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 关键字段
public class NetworkClient implements KafkaClient {
// 状态枚举值
private enum State {
ACTIVE,
CLOSING,
CLOSED
}
/* the selector used to perform network i/o */
// 用于执行网络 I/O 的选择器
private final Selectable selector;
// Metadata元信息的更新器, 它可以尝试更新元信息
private final MetadataUpdater metadataUpdater;
/* the state of each node's connection */
// 管理集群所有节点连接的状态
private final ClusterConnectionStates connectionStates;
/* the set of requests currently being sent or awaiting a response */
// 当前正在发送或等待响应的请求集合
private final InFlightRequests inFlightRequests;
/* the socket send buffer size in bytes */
// 套接字发送数据的缓冲区的大小(以字节为单位)
private final int socketSendBuffer;
/* the socket receive size buffer in bytes */
// 套接字接收数据的缓冲区的大小(以字节为单位)
private final int socketReceiveBuffer;
/* the client id used to identify this client in requests to the server */
// 表示客户端id,标识客户端身份
private final String clientId;
/* the current correlation id to use when sending requests to servers */
// 向服务器发送请求时使用的当前关联 ID
private int correlation;
/* default timeout for individual requests to await acknowledgement from servers */
// 单个请求等待服务器确认的默认超时
private final int defaultRequestTimeoutMs;
/* time in ms to wait before retrying to create connection to a server */
// 重连的退避时间
private final long reconnectBackoffMs;
/**
* True if we should send an ApiVersionRequest when first connecting to a broker.
* 是否需要与 Broker 端的版本协调,默认为 true
* 如果为 true 当第一次连接到一个 broker 时,应当发送一个 version 的请求,用来得知 broker 的版本, 如果为 false 则不需要发送 version 的请求。
*/
private final boolean discoverBrokerVersions;
// broker 端版本
private final ApiVersions apiVersions;
// 存储着要发送的版本请求,key 为 nodeId,value 为构建请求的 Builder
private final Map<String, ApiVersionsRequest.Builder> nodesNeedingApiVersionsFetch = new HashMap<>();
// 取消的请求集合
private final List<ClientResponse> abortedSends = new LinkedList<>();
从该类属性字段来看比较多,这里说几个关键字段:
- 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()
/**
* Begin connecting to the given node, return true if we are already connected and ready to send to that node.
*
* @param node The node to check
* @param now The current timestamp
* @return True if we are ready to send to the given node
*/
@Override
public boolean ready(Node node, long now){
// 空节点
if (node.isEmpty())
throw new IllegalArgumentException("Cannot connect to empty node " + node);
// 1、判断节点是否准备好发送请求
if (isReady(node, now))
return true;
// 2、判断节点连接状态
if (connectionStates.canConnect(node.idString(), now))
// if we are interested in sending to a node and we don't have a connection to it, initiate one
// 3、初始化连接,但此时不一定连接成功了
initiateConnect(node, now);
return false;
}
/**
* Check if the node with the given id is ready to send more requests.
* @param node The node
* @param now The current time in ms
* @return true if the node is ready
*/
@Override
public boolean isReady(Node node, long now){
// if we need to update our metadata now declare all requests unready to make metadata requests first priority
// 当发现正在更新元数据时,会禁止发送请求 && 当连接没有创建完毕或者当前发送的请求过多时,也会禁止发送请求
return !metadataUpdater.isUpdateDue(now) && canSendRequest(node.idString(), now);
}
/**
* Are we connected and ready and able to send more requests to the given connection?
* 检测连接状态、发送请求是否过多
* @param node The node
* @param now the current timestamp
*/
private boolean canSendRequest(String node, long now){
// 三个条件必须都满足
return connectionStates.isReady(node, now) && selector.isChannelReady(node) &&
inFlightRequests.canSendMore(node);
}
该方法表示某个节点是否准备好并可以发送请求,主要做了三件事:
- 先判断节点是否已经准备好连接并接收请求了,需要满足以下四个条件:
● !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()
/**
* 创建连接
* Initiate a connection to the given node
* @param node the node to connect to
* @param now current time in epoch milliseconds
*/
private void initiateConnect(Node node, long now){
String nodeConnectionId = node.idString();
try {
// 1、更新连接状态为正在连接
connectionStates.connecting(nodeConnectionId, now, node.host(), clientDnsLookup);
// 获取连接地址
InetAddress address = connectionStates.currentAddress(nodeConnectionId);
log.debug("Initiating connection to node {} using address {}", node, address);
// 2、调用 selector 尝试异步进行连接,后续通过selector.poll进行监听事件就绪
selector.connect(nodeConnectionId,
new InetSocketAddress(address, node.port()),
this.socketSendBuffer,
this.socketReceiveBuffer);
} catch (IOException e) {
log.warn("Error connecting to node {}", node, e);
// Attempt failed, we'll try again after the backoff
connectionStates.disconnected(nodeConnectionId, now);
// Notify metadata updater of the connection failure
metadataUpdater.handleServerDisconnect(now, nodeConnectionId, Optional.empty());
}
}
该方法主要是进行初始化连接,做了两件事:
- 调用 connectionStates.connecting() 更新连接状态为正在连接。
- 调用 selector.connect() 异步发起连接,此时不一定连接上了,后续 Selector.poll() 会监听连接是否准备好并完成连接,如果连接成功,则会将 ConnectionState 设置为 CONNECTED。
当连接准备好后,接下来我们来看下发送相关的方法。
02.2.3 send()、doSend()
/**
* ClientRequest 是客户端的请求,封装了 requestBuilder
*/
public final class ClientRequest {
// 节点地址
private final String destination;
// ClientRequest 中通过 requestBuilder 给不同类型的请求设置不同的请求内容
private final AbstractRequest.Builder<?> requestBuilder;
// 请求头的 correlationId
private final int correlationId;
// 请求头的 clientid
private final String clientId;
// 创建时间
private final long createdTimeMs;
// 是否需要进行响应
private final boolean expectResponse;
// 请求的超时时间
private final int requestTimeoutMs;
// 回调函数 用来处理响应
private final RequestCompletionHandler callback;
......
}
/**
* Queue up the given request for sending. Requests can only be sent out to ready nodes.
* @param request The request
* @param now The current timestamp
* 发送请求,这个方法 生产者和消费者都会调用,其中 ClientRequest 表示客户端的请求。
*/
@Override
public void send(ClientRequest request, long now){
doSend(request, false, now);
}
// 检测请求版本是否支持,如果支持则发送请求
private void doSend(ClientRequest clientRequest, boolean isInternalRequest, long now){
// 确认是否活跃
ensureActive();
// 目标节点id
String nodeId = clientRequest.destination();
// 是否是 NetworkClient 内部请求 这里为 false
if (!isInternalRequest) {
// 检测是否可以向指定 Node 发送请求,如果还不能发送请求则抛异常
if (!canSendRequest(nodeId, now))
throw new IllegalStateException("Attempt to send a request to node " + nodeId + " which is not ready.");
}
AbstractRequest.Builder<?> builder = clientRequest.requestBuilder();
try {
// 检测版本
NodeApiVersions versionInfo = apiVersions.get(nodeId);
// ... 忽略
// builder.build()是 ProduceRequest.Builder,结果是ProduceRequest
// 调用 doSend 方法
doSend(clientRequest, isInternalRequest, now, builder.build(version));
} catch (UnsupportedVersionException unsupportedVersionException) { log.debug("Version mismatch when attempting to send {} with correlation id {} to {}", builder, clientRequest.correlationId(), clientRequest.destination(), unsupportedVersionException);
// 请求的版本不协调,那么生成 clientResponse
ClientResponse clientResponse = new ClientResponse(clientRequest.makeHeader(builder.latestAllowedVersion()),
clientRequest.callback(), clientRequest.destination(), now, now,
false, unsupportedVersionException, null, null);
// 添加到 abortedSends 集合里
abortedSends.add(clientResponse);
}
}
/**
* isInternalRequest 表示发送前是否需要验证连接状态,如果为 true 则表示客户端已经确定连接是好的
* request表示请求体
*/
private void doSend(ClientRequest clientRequest, boolean isInternalRequest, long now, AbstractRequest request){
// 目标节点地址
String destination = clientRequest.destination();
// 生成请求头
RequestHeader header = clientRequest.makeHeader(request.version());
if (log.isDebugEnabled()) {
log.debug("Sending {} request with header {} and timeout {} to node {}: {}",
clientRequest.apiKey(), header, clientRequest.requestTimeoutMs(), destination, request);
}
// 1、构建 NetworkSend 对象 结合请求头和请求体,序列化数据,保存到 NetworkSend
Send send = request.toSend(destination, header);
// 2、构建 inFlightRequest 对象 保存了发送前的所有信息
InFlightRequest inFlightRequest = new InFlightRequest(
clientRequest,
header,
isInternalRequest,
request,
send,
now);
// 3、把 inFlightRequest 加入 inFlightRequests 集合里
this.inFlightRequests.add(inFlightRequest);
// 4、调用 Selector 异步发送数据,并将 send 和对应 kafkaChannel 绑定起来,并开启该 kafkaChannel 底层 socket 的写事件,等待下一步真正的网络发送
selector.send(send);
}
@Override
public boolean active(){
// 判断状态是否是活跃的
return state.get() == State.ACTIVE;
}
// 确认是否活跃
private void ensureActive(){
if (!active())
throw new DisconnectException("NetworkClient is no longer active, state is " + state);
}
从上面源码可以看出此处发送并不是真正的网络发送,而是先将数据发送到缓存中。
- 首先最外层是 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() 方法进行处理的,代码如下:
/**
* Do actual reads and writes to sockets.
* @param timeout The maximum amount of time to wait (in ms) for responses if there are none immediately,
* must be non-negative. The actual timeout will be the minimum of timeout, request timeout and
* metadata timeout
* @param now The current time in milliseconds
* @return The list of responses received
*/
@Override
public List<ClientResponse> poll(long timeout, long now){
// 确认是否活跃
ensureActive();
// 取消发送是否为空
if (!abortedSends.isEmpty()) {
// If there are aborted sends because of unsupported version exceptions or disconnects,
// handle them immediately without waiting for Selector#poll.
List<ClientResponse> responses = new ArrayList<>();
handleAbortedSends(responses);
completeResponses(responses);
return responses;
}
// 1、尝试更新元数据
long metadataTimeout = metadataUpdater.maybeUpdate(now);
try {
// 2、执行网络 I/O 操作,真正读写发送的地方,如果客户端的请求被完整的处理过了,会加入到completeSends 或 complteReceives 集合中
this.selector.poll(Utils.min(timeout, metadataTimeout, defaultRequestTimeoutMs));
} catch (IOException e) {
log.error("Unexpected error during I/O", e);
}
// process completed actions
long updatedNow = this.time.milliseconds();
// 响应结果集合:真正的读写操作, 会生成responses
List<ClientResponse> responses = new ArrayList<>();
// 3、完成发送的handler,处理 completedSends 集合
handleCompletedSends(responses, updatedNow);
// 4、完成接收的handler,处理 completedReceives 队列
handleCompletedReceives(responses, updatedNow);
// 5、断开连接的handler,处理 disconnected 列表
handleDisconnections(responses, updatedNow);
// 6、处理连接的handler,处理 connected 列表
handleConnections();
// 7、处理版本协调请求(获取api版本号) handler
handleInitiateApiVersionRequests(updatedNow);
// 8、超时连接的handler,处理超时连接集合
handleTimedOutConnections(responses, updatedNow);
// 9、超时请求的handler,处理超时请求集合
handleTimedOutRequests(responses, updatedNow);
// 10、完成响应回调
completeResponses(responses);
return responses;
}
这里的步骤比较多,我们按照先后顺序讲解下。
- 尝试更新元数据。
- 调用 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 端了。
/**
* Handle any completed request send. In particular if no response is expected consider the request complete.
* @param responses The list of responses to update
* @param now The current time
*/
private void handleCompletedSends(List<ClientResponse> responses, long now){
// if no response is expected then when the send is completed, return it
// 1、遍历 completedSends 发送完成的请求集合,通过调用 Selector 获取从上一次 poll 开始的请求
for (Send send : this.selector.completedSends()) {
// 2、从 inFlightRequests 集合获取该 Send 关联对应 Node 的队列取出最新的请求,但并没有从队列中删除,取出后判断这个请求是否期望得到响应
InFlightRequest request = this.inFlightRequests.lastSent(send.destination());
// 3、是否需要响应, 如果不需要响应,当Send请求完成时,就直接返回.还是有request.completed生成的ClientResponse对象
if (!request.expectResponse) {
// 4、如果不需要响应就取出 inFlightRequests 中该 Sender 关联对应 Node 的 inFlightRequest,即提取最新的请求
this.inFlightRequests.completeLastSent(send.destination());
// 5、调用 completed() 生成 ClientResponse,第一个参数为null,表示没有响应内容,把请求添加到 Responses 集合
responses.add(request.completed(null, now));
}
}
}
该方法主要用来在客户端发送请求后,对响应结果进行处理,做了五件事:
- 遍历 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 方法。
/**
* Handle any completed receives and update the response list with the responses received.
* @param responses The list of responses to update
* @param now The current time
* 处理 CompletedReceives 队列,根据返回的响应信息实例化 ClientResponse ,并加到响应集合里
*/
private void handleCompletedReceives(List<ClientResponse> responses, long now){
// 1、遍历 CompletedReceives 响应集合,通过 Selector 返回未处理的响应
for (NetworkReceive receive : this.selector.completedReceives()) {
// 2、获取发送请求的 Node id
String source = receive.source();
// 3、从 inFlightRequests 集合队列获取已发送请求「最老的请求」并删除(从 inFlightRequests 删除,因为inFlightRequests 存储的是未收到请求响应的 ClientRequest,现在请求已经有响应了,就不需要保存了)
InFlightRequest req = inFlightRequests.completeNext(source);
// 4、解析响应,并且验证响应头,生成 responseStruct 实例
Struct responseStruct = parseStructMaybeUpdateThrottleTimeMetrics(receive.payload(), req.header,throttleTimeSensor, now);
// 生成响应体
AbstractResponse response = AbstractResponse.parseResponse(req.header.apiKey(), responseStruct, req.header.apiVersion());
....
// If the received response includes a throttle delay, throttle the connection.
// 流控处理
maybeThrottle(response, req.header.apiVersion(), req.destination, now);
// 5、判断返回类型
if (req.isInternalRequest && response instanceof MetadataResponse)
// 处理元数据请求响应
metadataUpdater.handleSuccessfulResponse(req.header, now, (MetadataResponse) response);
else if (req.isInternalRequest && response instanceof ApiVersionsResponse)
// 处理版本协调响应
handleApiVersionsResponse(responses, req, now, (ApiVersionsResponse) response);
else
// 普通发送消息的响应,通过 InFlightRequest.completed(),生成 ClientResponse,将响应添加到 responses 集合中
responses.add(req.completed(response, now));
}
}
// 解析响应,并且验证响应头,生成 responseStruct 实例
private static Struct parseStructMaybeUpdateThrottleTimeMetrics(ByteBuffer responseBuffer, RequestHeader requestHeader, Sensor throttleTimeSensor, long now){
// 解析响应头
ResponseHeader responseHeader = ResponseHeader.parse(responseBuffer,
requestHeader.apiKey().responseHeaderVersion(requestHeader.apiVersion()));
// 解析响应体
Struct responseBody = requestHeader.apiKey().parseResponse(requestHeader.apiVersion(), responseBuffer);
// 验证请求头与响应头的 correlation id 必须相等
correlate(requestHeader, responseHeader);
if (throttleTimeSensor != null && responseBody.hasField(CommonFields.THROTTLE_TIME_MS))
throttleTimeSensor.record(responseBody.get(CommonFields.THROTTLE_TIME_MS), now);
return responseBody;
}
该方法主要用来处理接收完毕的网络请求集合,做了五件事:
- 遍历 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」集合里对应队列的第一个元素,如下图所示:
文章转载自公众号:华仔聊技术