• Kafka请求发送分析


    随便写写T_T,本来是想分析Conenct是如何组建集群的,但是写着写着发现分析了请求是如何发送的和回收的。后面也不打算改,就当作是从Connect出发分析Kafka请求是如何发送。

    顺带一句,本文只是一篇源码分析┑( ̄Д  ̄)┍,只完成了一部分!!!

    KafkaConnect在分布式情况下的主要实现类是DistributedHerder,其生命周期基本都是在tick()里度过。

    1. // DistributedHerder.java
    2. public void tick() {
    3. try {
    4. if (!canReadConfigs) {
    5. if (readConfigToEnd(workerSyncTimeoutMs)) {
    6. canReadConfigs = true;
    7. } else {
    8. return; // Safe to return and tick immediately because readConfigToEnd will do the backoff for us
    9. }
    10. }
    11. // 这里会确定集群情况
    12. member.ensureActive();
    13. if (!handleRebalanceCompleted()) return;
    14. } catch (WakeupException e) {
    15. log.trace("Woken up while ensure group membership is still active");
    16. return;
    17. }
    18. ...
    19. // WorkerGroupMember.java
    20. public void ensureActive() {
    21. coordinator.poll(0);
    22. }

    DistributedHerder.tick内部会先确认coordinator和集群的情况,毕竟coonnect不和集群中的leader交互,不像zookeeper那样需要在配置文件里把节点都配置起来。member.ensuerActiver()具体会调用WorkerCoordinator.poll()向coordinator主动发起一次消息查询poll请求。

    1. // WorkerCoordinator.java
    2. public void poll(long timeout) {
    3. // poll for io until the timeout expires
    4. final long start = time.milliseconds();
    5. long now = start;
    6. long remaining;
    7. do {
    8. // 第一次加入或者和coordiantor的心跳响应超时,都会设置内存中的coordinator为null
    9. if (coordinatorUnknown()) {
    10. log.debug("Broker coordinator is marked unknown. Attempting discovery with a timeout of {}ms",
    11. coordinatorDiscoveryTimeoutMs);
    12. // 请求负责member.id指定集群的coordinator
    13. if (ensureCoordinatorReady(time.timer(coordinatorDiscoveryTimeoutMs))) {
    14. log.debug("Broker coordinator is ready");
    15. } else {
    16. // 连接coordinator失败,假如前面已经加入过集群了,立即清除
    17. // 前面分配的任务,避免coordiantor认为当前connect节点挂了
    18. // 导致把任务分配给其他connect节点,导致任务重复
    19. final ExtendedAssignment localAssignmentSnapshot = assignmentSnapshot;
    20. if (localAssignmentSnapshot != null && !localAssignmentSnapshot.failed()) {
    21. // 清除分配给当前connect节点的任务
    22. listener.onRevoked(localAssignmentSnapshot.leader(), localAssignmentSnapshot.connectors(), localAssignmentSnapshot.tasks());
    23. assignmentSnapshot = null;
    24. }
    25. }
    26. now = time.milliseconds();
    27. }
    28. // 前面是检测coordiantor是否准备好了
    29. // 这里是检测是否需要重新加入到group.id指定的集群中
    30. if (rejoinNeededOrPending()) {
    31. ensureActiveGroup();
    32. now = time.milliseconds();
    33. }
    34. pollHeartbeat(now);
    35. long elapsed = now - start;
    36. remaining = timeout - elapsed;
    37. // 如果从开始请求coordiantor到加入集群结束,还没超时,则可以先尝试获取下
    38. // 是否当前有网络请求过来处理,并没长时间阻塞在coordinator.poll这里
    39. long pollTimeout = Math.min(Math.max(0, remaining), timeToNextHeartbeat(now));
    40. client.poll(time.timer(pollTimeout));
    41. now = time.milliseconds();
    42. elapsed = now - start;
    43. remaining = timeout - elapsed;
    44. } while (remaining > 0);
    45. }

    connect内部通过调用AbstractCoordinator.coordinatorUnknown()来判断coordiantor是否就

    当connect第一次加入,或者和coordinator连接断开时,需要重新请求coordiantor状态。这里通过调用WorkerCoordinator.ensureCoordiantorReady(timer)完成。

    1. protected synchronized boolean ensureCoordinatorReady(final Timer timer) {
    2. // 重新判断当前coordinator是否已经就绪,可能因为是网络波动问题
    3. // 导致coordiantor只是瞬间断开后又立即恢复
    4. if (!coordinatorUnknown())
    5. return true;
    6. // 当coordiantor未知,以及当前判断未超时时,需要不断轮询
    7. do {
    8. // 上一次轮询遇到严重无法恢复的异常,就及时抛出并退出
    9. if (fatalFindCoordinatorException != null) {
    10. final RuntimeException fatalException = fatalFindCoordinatorException;
    11. fatalFindCoordinatorException = null;
    12. throw fatalException;
    13. }
    14. // 这里是构建FindCoordinatorRequest
    15. final RequestFuture future = lookupCoordinator();
    16. // 将请求交给NetworkClient去发发送
    17. client.poll(future, timer);
    18. if (!future.isDone()) {
    19. // ran out of time
    20. break;
    21. }
    22. RuntimeException fatalException = null;
    23. // 校验请求结果
    24. if (future.failed()) {
    25. if (future.isRetriable()) {
    26. log.debug("Coordinator discovery failed, refreshing metadata");
    27. client.awaitMetadataUpdate(timer);
    28. } else {
    29. fatalException = future.exception();
    30. log.info("FindCoordinator request hit fatal exception", fatalException);
    31. }
    32. } else if (coordinator != null && client.isUnavailable(coordinator)) {
    33. // 这里虽然找到了coordiantor,但是isUnavailable返回失败,说明
    34. // 请求连接失败,可以过一会再请求
    35. markCoordinatorUnknown();
    36. timer.sleep(rebalanceConfig.retryBackoffMs);
    37. }
    38. clearFindCoordinatorFuture();
    39. if (fatalException != null)
    40. throw fatalException;
    41. } while (coordinatorUnknown() && timer.notExpired());
    42. return !coordinatorUnknown();
    43. }

    lookupCoordinator()负责获取负责当前集群的coordiantor信息

    1. // AbstractCoordinator
    2. protected synchronized RequestFuture lookupCoordinator() {
    3. if (findCoordinatorFuture == null) {
    4. // 这里是从broker中选择一个节点发送请求,要求是当前具有最少请求的节点
    5. // 不涉及当前的分析所以先跳过
    6. Node node = this.client.leastLoadedNode();
    7. if (node == null) {
    8. log.debug("No broker available to send FindCoordinator request");
    9. return RequestFuture.noBrokersAvailable();
    10. } else {
    11. // 往该节点发送FindCoordinatorRequest
    12. findCoordinatorFuture = sendFindCoordinatorRequest(node);
    13. }
    14. }
    15. return findCoordinatorFuture;
    16. }
    17. // AbstraceCoordinator
    18. private RequestFuture sendFindCoordinatorRequest(Node node) {
    19. // 构建FindCoordinatorRequest请求
    20. FindCoordinatorRequest.Builder requestBuilder =
    21. new FindCoordinatorRequest.Builder(
    22. new FindCoordinatorRequestData()
    23. // 这里设置Coordinator的类型,有事务和组管理,这里选组管理
    24. .setKeyType(CoordinatorType.GROUP.id())
    25. // 对应的值是在connect中配置的group.id
    26. .setKey(this.rebalanceConfig.groupId));
    27. return client.send(node, requestBuilder)
    28. .compose(new FindCoordinatorResponseHandler());
    29. }

    由上面的逻辑可以分析出,connect需要查询对应group.id负责的coordinator,只需要往任意的broker发送FindCoordinatorRequest即可,该请求会附带上当前connect的group.id信息,随即调用ConsumerNetworkClient.send()发送请求,而且注意看这里往send()传递并不是requet,而是半成品的requestBuilder,说明当前请求除了group.id外还有其他信息需要组装。其余的client.send()发送完之后,还会调用compose组装请求发送完成后如何处理,这里因为是FindCoordinatorReuqest,所以将完成的请求交给FindCoordinatorResponseHandler

    1. public RequestFuture compose(final RequestFutureAdapter adapter) {
    2. final RequestFuture adapted = new RequestFuture<>();
    3. addListener(new RequestFutureListener() {
    4. @Override
    5. public void onSuccess(T value) {
    6. adapter.onSuccess(value, adapted);
    7. }
    8. @Override
    9. public void onFailure(RuntimeException e) {
    10. adapter.onFailure(e, adapted);
    11. }
    12. });
    13. return adapted;
    14. }
    15. private class FindCoordinatorResponseHandler extends RequestFutureAdapter {
    16. @Override
    17. public void onSuccess(ClientResponse resp, RequestFuture future) {
    18. log.debug("Received FindCoordinator response {}", resp);
    19. FindCoordinatorResponse findCoordinatorResponse = (FindCoordinatorResponse) resp.responseBody();
    20. Errors error = findCoordinatorResponse.error();
    21. // 没有发送失败
    22. if (error == Errors.NONE) {
    23. synchronized (AbstractCoordinator.this) {
    24. // use MAX_VALUE - node.id as the coordinator id to allow separate connections
    25. // for the coordinator in the underlying network client layer
    26. int coordinatorConnectionId = Integer.MAX_VALUE - findCoordinatorResponse.data().nodeId();
    27. // 获取coordinator信息
    28. AbstractCoordinator.this.coordinator = new Node(
    29. coordinatorConnectionId,
    30. findCoordinatorResponse.data().host(),
    31. findCoordinatorResponse.data().port());
    32. log.info("Discovered group coordinator {}", coordinator);
    33. // 连接coordinator
    34. client.tryConnect(coordinator);
    35. heartbeat.resetSessionTimeout();
    36. }
    37. future.complete(null);
    38. } else if (error == Errors.GROUP_AUTHORIZATION_FAILED) {
    39. future.raise(GroupAuthorizationException.forGroupId(rebalanceConfig.groupId));
    40. } else {
    41. log.debug("Group coordinator lookup failed: {}", findCoordinatorResponse.data().errorMessage());
    42. future.raise(error);
    43. }
    44. }
    45. ...
    46. }

    我们来看下ConsumerNetworkClient.send()逻辑。

    1. // ConsumerNetworkClient
    2. public RequestFuture send(Node node, AbstractRequest.Builder requestBuilder) {
    3. return send(node, requestBuilder, requestTimeoutMs);
    4. }
    5. // ConsumerNetworkClient
    6. public RequestFuture send(Node node,
    7. AbstractRequest.Builder requestBuilder,
    8. int requestTimeoutMs) {
    9. long now = time.milliseconds();
    10. // 请求完成时处理器
    11. RequestFutureCompletionHandler completionHandler = new RequestFutureCompletionHandler();
    12. // 将发送目的地Node、半成品的requestBuilder、当前时间以及
    13. // RequestFutureCompletionHandler 转化成统一的ClientRequest
    14. ClientRequest clientRequest = client.newClientRequest(node.idString(), requestBuilder, now, true,
    15. requestTimeoutMs, completionHandler);
    16. // 将请求放入到unsent中
    17. unsent.put(node, clientRequest);
    18. // 唤醒client发送线程,这里对应KafkaClient
    19. client.wakeup();
    20. // 将处理结果Future返回
    21. return completionHandler.future;
    22. }

    ConsumerNetworkClient主要做了两件事,第一就是将当前请求、发送的目的地节点以及请求完成时处理器RequestFutureCompletionHandler组装成统一的发送对象ClientRequest。

    第二件事就把生成的ClientRequesr塞入到unsent这个待发送请求的队列中关于unset的结构如下:

    1. private final static class UnsentRequests {
    2. // UnsentRequest核心时ConcurrentMap,其中key时Node节点,
    3. // value时待发送请求队列ConcurrentLinkedQueue
    4. private final ConcurrentMap> unsent;
    5. public void put(Node node, ClientRequest request) {
    6. // 这里需要加上synchronized,因为对ConcurrentMap是先取值ConcurrentLinkedQueue
    7. // 再往里面添加对象,这里有可能ConcurrentLinkedQueue本身被修改
    8. // 例如设置为null,亦或者设置为新的ConcurrentLinkedQueue
    9. // 为了避免找个情况所以加上synchronized
    10. synchronized (unsent) {
    11. ConcurrentLinkedQueue requests = unsent.get(node);
    12. if (requests == null) {
    13. requests = new ConcurrentLinkedQueue<>();
    14. unsent.put(node, requests);
    15. }
    16. requests.add(request);
    17. }
    18. }
    19. ...

    所以总结下,调用ConsumerNetworkClient.send()就是把请求塞入到UnsentRequest中,然后就返回了,实际并没有将请求真正的发送给服务端。那究竟如何才能把请求发送出去呢?回到AbstractCoordinator.ensureCoordinatorReady(),在调用完lookupCoordinator()之后,会立即调用ConsumerNetworkClient.poll()方法处理Future。

    1. // AbstractCoordinator
    2. protected synchronized boolean ensureCoordinatorReady(final Timer timer) {
    3. ...
    4. final RequestFuture future = lookupCoordinator();
    5. client.poll(future, timer);

    下面看下ConsumerNetworkClient.poll()

    1. // ConsumerNetworkClient 轮询网络IO
    2. public void poll(Timer timer, PollCondition pollCondition, boolean disableWakeup) {
    3. // pendingCompletion这里是把pendingCompletion队列中的请求完成
    4. firePendingCompletedRequests();
    5. // 自旋锁,避免多线程同时调用该方法时出现线程安全
    6. lock.lock();
    7. try {
    8. // 对请求做失败处理。客户端断开与指定节点的连接。
    9. handlePendingDisconnects();
    10. // 尝试发送当前可以发送的请求
    11. long pollDelayMs = trySend(timer.currentTimeMs());
    12. // 开始轮询IO请求
    13. if (pendingCompletion.isEmpty() && (pollCondition == null || pollCondition.shouldBlock())) {
    14. long pollTimeout = Math.min(timer.remainingMs(), pollDelayMs);
    15. if (client.inFlightRequestCount() == 0)
    16. pollTimeout = Math.min(pollTimeout, retryBackoffMs);
    17. client.poll(pollTimeout, timer.currentTimeMs());
    18. } else {
    19. client.poll(0, timer.currentTimeMs());
    20. }
    21. timer.update();
    22. checkDisconnects(timer.currentTimeMs());
    23. if (!disableWakeup) {
    24. // trigger wakeups after checking for disconnects so that the callbacks will be ready
    25. // to be fired on the next call to poll()
    26. maybeTriggerWakeup();
    27. }
    28. // throw InterruptException if this thread is interrupted
    29. maybeThrowInterruptException();
    30. // try again to send requests since buffer space may have been
    31. // cleared or a connect finished in the poll
    32. trySend(timer.currentTimeMs());
    33. // fail requests that couldn't be sent if they have expired
    34. failExpiredRequests(timer.currentTimeMs());
    35. // clean unsent requests collection to keep the map from growing indefinitely
    36. unsent.clean();
    37. } finally {
    38. lock.unlock();
    39. }
    40. // called without the lock to avoid deadlock potential if handlers need to acquire locks
    41. firePendingCompletedRequests();
    42. metadata.maybeThrowAnyException();
    43. }

    ConsumerNetworkClient.poll()做的事情比较多,先来看下trySend()的逻辑

    1. // ConsumerNetworkClient.java
    2. long trySend(long now) {
    3. long pollDelayMs = maxPollTimeoutMs;
    4. // unsent我们上面分析了,就是存储待发送给各个节点的请求
    5. // 这里开始遍历每个节点
    6. for (Node node : unsent.nodes()) {
    7. Iterator iterator = unsent.requestIterator(node);
    8. if (iterator.hasNext())
    9. pollDelayMs = Math.min(pollDelayMs, client.pollDelayMs(node, now));
    10. // 遍历每个节点,拿出ClientRequest
    11. while (iterator.hasNext()) {
    12. ClientRequest request = iterator.next();
    13. // 检查当前待发送的节点是否就绪
    14. if (client.ready(node, now)) {
    15. client.send(request, now);
    16. // 发送完立马删除当前请求
    17. iterator.remove();
    18. } else {
    19. // 如果当前节点没有待发送的请求,或者没有就绪就退出当前节点的遍历
    20. // 开始下一个节点
    21. break;
    22. }
    23. }
    24. }
    25. return pollDelayMs;
    26. }

    trySend()内部会调用KafkaClient.ready()判断待发送请求的节点是否就绪

    1. // NetworkClient.java
    2. public boolean ready(Node node, long now) {
    3. // 判断节点的host和port是否为空
    4. if (node.isEmpty())
    5. throw new IllegalArgumentException("Cannot connect to empty node " + node);
    6. // 判断当前节点是否准备好接收请求了
    7. if (isReady(node, now))
    8. return true;
    9. if (connectionStates.canConnect(node.idString(), now))
    10. // if we are interested in sending to a node and we don't have a connection to it, initiate one
    11. initiateConnect(node, now);
    12. return false;
    13. }
    14. // NetworkClient.java
    15. public boolean isReady(Node node, long now) {
    16. // 节点的元数据是否没有及时更新(元数据包括topic、partition信息)
    17. return !metadataUpdater.isUpdateDue(now) && canSendRequest(node.idString(), now);
    18. }
    19. // NetworkClient.java
    20. private boolean canSendRequest(String node, long now) {
    21. return connectionStates.isReady(node, now) && selector.isChannelReady(node) &&
    22. inFlightRequests.canSendMore(node);
    23. }
    24. // Selector
    25. public boolean isChannelReady(String id) {
    26. KafkaChannel channel = this.channels.get(id);
    27. return channel != null && channel.ready();
    28. }
    29. // InFlightRequests
    30. public boolean canSendMore(String node) {
    31. Deque queue = requests.get(node);
    32. return queue == null || queue.isEmpty() ||
    33. (queue.peekFirst().send.completed() && queue.size() < this.maxInFlightRequestsPerConnection);
    34. }

    根据上面的逻辑可以看出,Kafka内部判断对应Node是否可以发送请求会依照以下:

    1. 配置文件中的broker节点,host和port是否为空,也就是配置参数是否合法;
    2. broker的元数据(metadata)距离上一次更新时间没有超时;
    3. Kafka内部封装了Java的NIO模型,设计了自己的Selector和KafkaChannel(可以参照原生的NIO Selector和SocketChnnael),KafkaChannel表示和目标的连接通道,并且该通道需要开启;
    4. InFlightRequest在Kafka表示发送给对应节点的请求,但是还未收到响应,内部用了一个Map> requests 存储;canSendMore()判断队列中第一个请求需要发送完成,而且当前队列中处于发送的请求不能超过maxInFlightRequestsPerConnection的数量限制。

    一旦判断对应的节点可以发送请求了,就可以调用KafkaClient.send()发送,其实现是放在NetworkClient.send()

    1. // NetworkClient
    2. public void send(ClientRequest request, long now) {
    3. doSend(request, false, now);
    4. }
    5. // NetworkClient
    6. private void doSend(ClientRequest clientRequest, boolean isInternalRequest, long now) {
    7. ensureActive();
    8. // 获取目的节点
    9. String nodeId = clientRequest.destination();
    10. if (!isInternalRequest) {
    11. if (!canSendRequest(nodeId, now))
    12. throw new IllegalStateException("Attempt to send a request to node " + nodeId + " which is not ready.");
    13. }
    14. // 前面说了构建ClientRequest时候传递的是一个半成品的Builder,这里要加上请求header和版本信息
    15. AbstractRequest.Builder builder = clientRequest.requestBuilder();
    16. try {
    17. // 获取对应broker节点的版本信息
    18. NodeApiVersions versionInfo = apiVersions.get(nodeId);
    19. short version;
    20. // 如果版本信息为空,则按照当前客户端的最新的版本发送,无法获取服务端版本号的原因可能时
    21. // discoverBrokerVersions设置为false
    22. if (versionInfo == null) {
    23. version = builder.latestAllowedVersion();
    24. if (discoverBrokerVersions && log.isTraceEnabled())
    25. log.trace("No version information found when sending {} with correlation id {} to node {}. " +
    26. "Assuming version {}.", clientRequest.apiKey(), clientRequest.correlationId(), nodeId, version);
    27. } else {
    28. // 根据客户端版本和服务端版本进行比较,选择一个合适的版本
    29. version = versionInfo.latestUsableVersion(clientRequest.apiKey(), builder.oldestAllowedVersion(),
    30. builder.latestAllowedVersion());
    31. }
    32. // 实际发送调用,注意这里builder.build将version传递过去,生成实际发送的Request,也就是消息的payload部分
    33. doSend(clientRequest, isInternalRequest, now, builder.build(version));
    34. } catch (UnsupportedVersionException unsupportedVersionException) {
    35. log.debug("Version mismatch when attempting to send {} with correlation id {} to {}", builder,
    36. clientRequest.correlationId(), clientRequest.destination(), unsupportedVersionException);
    37. ClientResponse clientResponse = new ClientResponse(clientRequest.makeHeader(builder.latestAllowedVersion()),
    38. clientRequest.callback(), clientRequest.destination(), now, now,
    39. false, unsupportedVersionException, null, null);
    40. if (!isInternalRequest)
    41. abortedSends.add(clientResponse);
    42. else if (clientRequest.apiKey() == ApiKeys.METADATA)
    43. metadataUpdater.handleFailedRequest(now, Optional.of(unsupportedVersionException));
    44. }
    45. }

    在发生前,kafka需要获取对应broker可以接受的api版本信息(Kafka支持不同版本的客户端和服务端进行通信,只需要适当对消息进行处理即可);并在获取到broker的api版本后,根据当前客户端的最新的api版本latestAllowedVersion,和最老的版本oldestAllowedVersion进行比较:

    1. version = versionInfo.latestUsableVersion(clientRequest.apiKey(), builder.oldestAllowedVersion(),
    2. builder.latestAllowedVersion());
    3. // NodeApiVersions
    4. public short latestUsableVersion(ApiKeys apiKey, short oldestAllowedVersion, short latestAllowedVersion) {
    5. ApiVersion usableVersion = supportedVersions.get(apiKey);
    6. if (usableVersion == null)
    7. throw new UnsupportedVersionException("The broker does not support " + apiKey);
    8. return latestUsableVersion(apiKey, usableVersion, oldestAllowedVersion, latestAllowedVersion);
    9. }
    10. // NodeApiVersions
    11. private short latestUsableVersion(ApiKeys apiKey, ApiVersion supportedVersions,
    12. short minAllowedVersion, short maxAllowedVersion) {
    13. // 下面的比较就是取两个区间的交集的右边界
    14. short minVersion = (short) Math.max(minAllowedVersion, supportedVersions.minVersion);
    15. short maxVersion = (short) Math.min(maxAllowedVersion, supportedVersions.maxVersion);
    16. if (minVersion > maxVersion)
    17. throw new UnsupportedVersionException("The broker does not support " + apiKey +
    18. " with version in range [" + minAllowedVersion + "," + maxAllowedVersion + "]. The supported" +
    19. " range is [" + supportedVersions.minVersion + "," + supportedVersions.maxVersion + "].");
    20. return maxVersion;
    21. }

    得到两边可以接受的版本之后,调用doSend进行实际发送。

    1. // NetworkClient
    2. private void doSend(ClientRequest clientRequest, boolean isInternalRequest, long now, AbstractRequest request) {
    3. String destination = clientRequest.destination();
    4. // 请求头
    5. RequestHeader header = clientRequest.makeHeader(request.version());
    6. if (log.isDebugEnabled()) {
    7. log.debug("Sending {} request with header {} and timeout {} to node {}: {}",
    8. clientRequest.apiKey(), header, clientRequest.requestTimeoutMs(), destination, request);
    9. }
    10. // 将header、payload和发送目标组合成实际发送的消息Send
    11. // 而且这里toSend方法会将header和payload进行序列化
    12. Send send = request.toSend(destination, header);
    13. InFlightRequest inFlightRequest = new InFlightRequest(
    14. clientRequest,
    15. header,
    16. isInternalRequest,
    17. request,
    18. send,
    19. now);
    20. // 将请求塞入到InFlightRequest的队列中
    21. this.inFlightRequests.add(inFlightRequest);
    22. // 调用Selector发送请求
    23. selector.send(send);
    24. }
    25. // AbstractRequest
    26. public Send toSend(String destination, RequestHeader header) {
    27. return new NetworkSend(destination, serialize(header));
    28. }
    29. // RequestUtils
    30. public static ByteBuffer serialize(Struct headerStruct, Struct bodyStruct) {
    31. ByteBuffer buffer = ByteBuffer.allocate(headerStruct.sizeOf() + bodyStruct.sizeOf());
    32. headerStruct.writeTo(buffer);
    33. bodyStruct.writeTo(buffer);
    34. buffer.rewind();
    35. return buffer;
    36. }

    NetworkClient.doSend()会将请求封装成实际发送的消息结构Send,交给Selector.send()发送

    1. public void send(Send send) {
    2. String connectionId = send.destination();
    3. // 获取对应节点Node的KafkaChannel
    4. KafkaChannel channel = openOrClosingChannelOrFail(connectionId);
    5. // 当前节点对应的KafkaChannel已经被关闭了,则需要提示发送失败
    6. if (closingChannels.containsKey(connectionId)) {
    7. // ensure notification via `disconnected`, leave channel in the state in which closing was triggered
    8. this.failedSends.add(connectionId);
    9. } else {
    10. try {
    11. // 将Send设置在KafkaChannel中
    12. channel.setSend(send);
    13. } catch (Exception e) {
    14. channel.state(ChannelState.FAILED_SEND);
    15. this.failedSends.add(connectionId);
    16. close(channel, CloseMode.DISCARD_NO_NOTIFY);
    17. if (!(e instanceof CancelledKeyException)) {
    18. log.error("Unexpected exception during send, closing connection {} and rethrowing exception {}",
    19. connectionId, e);
    20. throw e;
    21. }
    22. }
    23. }
    24. }

    由上面的分析可以看到,实际调用ConsumerNetworkClient.send() 发送请求时候,会将请求加上对应的版本封装之后,交给底层的Selector去发送,但是Selector也并没有将请求直接发送过去,而是将请求设置为KafkaChannel的属性上。

    1. public void setSend(Send send) {
    2. // 当前KafkaChannel的Send不为空时,说明当前通道有请求发送中,需要抛出异常
    3. if (this.send != null)
    4. throw new IllegalStateException("Attempt to begin a send operation with prior send operation still in progress, connection id is " + id);
    5. this.send = send;
    6. // 设置当前SelectionKey可写,触发write事件
    7. this.transportLayer.addInterestOps(SelectionKey.OP_WRITE);
    8. }

    这里需要注意,一个KafkaChannel只能允许当前由一个Send请求,当Send请求不为空时,会直接抛出异常。

    直到现在我们都没看到请求时如何发送出去的,所以需要回到ConsumerNetworkClient.poll的方法,这里处理调用KafkaClient.send()之外,还会调用KafkaClient.poll()

    1. // ConsumerNetworkClient
    2. if (pendingCompletion.isEmpty() && (pollCondition == null || pollCondition.shouldBlock())) {
    3. // if there are no requests in flight, do not block longer than the retry backoff
    4. long pollTimeout = Math.min(timer.remainingMs(), pollDelayMs);
    5. if (client.inFlightRequestCount() == 0)
    6. pollTimeout = Math.min(pollTimeout, retryBackoffMs);
    7. client.poll(pollTimeout, timer.currentTimeMs());
    8. } else {
    9. client.poll(0, timer.currentTimeMs());
    10. }

    KafkaClient.pol()的实现是NetworkClient.poll()

    1. public List poll(long timeout, long now) {
    2. ensureActive();
    3. if (!abortedSends.isEmpty()) {
    4. // 如果部分请求因为版本不支持或者是连接断开,立即处理而不用等poll
    5. List responses = new ArrayList<>();
    6. handleAbortedSends(responses);
    7. completeResponses(responses);
    8. return responses;
    9. }
    10. // 更新集群元数据
    11. long metadataTimeout = metadataUpdater.maybeUpdate(now);
    12. try {
    13. // 调用Selector.poll
    14. this.selector.poll(Utils.min(timeout, metadataTimeout, defaultRequestTimeoutMs));
    15. } catch (IOException e) {
    16. log.error("Unexpected error during I/O", e);
    17. }
    18. // process completed actions
    19. long updatedNow = this.time.milliseconds();
    20. List responses = new ArrayList<>();
    21. handleCompletedSends(responses, updatedNow);
    22. handleCompletedReceives(responses, updatedNow);
    23. handleDisconnections(responses, updatedNow);
    24. handleConnections();
    25. handleInitiateApiVersionRequests(updatedNow);
    26. handleTimedOutRequests(responses, updatedNow);
    27. completeResponses(responses);
    28. return responses;
    29. }

    NetworkClient.poll()转而去调用Selector.poll(),Selector底层也是Java的NIO,当用到Java的原生Selector时,下面统称为NioSelector。

    1. public void poll(long timeout) throws IOException {
    2. if (timeout < 0)
    3. throw new IllegalArgumentException("timeout should be >= 0");
    4. boolean madeReadProgressLastCall = madeReadProgressLastPoll;
    5. clear();
    6. boolean dataInBuffers = !keysWithBufferedRead.isEmpty();
    7. if (!immediatelyConnectedKeys.isEmpty() || (madeReadProgressLastCall && dataInBuffers))
    8. timeout = 0;
    9. // 这部分是处理内存溢出吗,可以先跳过
    10. if (!memoryPool.isOutOfMemory() && outOfMemory) {
    11. //we have recovered from memory pressure. unmute any channel not explicitly muted for other reasons
    12. log.trace("Broker no longer low on memory - unmuting incoming sockets");
    13. for (KafkaChannel channel : channels.values()) {
    14. if (channel.isInMutableState() && !explicitlyMutedChannels.contains(channel)) {
    15. channel.maybeUnmute();
    16. }
    17. }
    18. outOfMemory = false;
    19. }
    20. /* check ready keys */
    21. long startSelect = time.nanoseconds();
    22. // 调用NIO的select,获取触发的读、写或者连接事件
    23. int numReadyKeys = select(timeout);
    24. long endSelect = time.nanoseconds();
    25. this.sensors.selectTime.record(endSelect - startSelect, time.milliseconds());
    26. if (numReadyKeys > 0 || !immediatelyConnectedKeys.isEmpty() || dataInBuffers) {
    27. // 获取触发的SelectionKey,前面我们在调用KafkaChannel.setSend()的时候
    28. // 将KafkaChannel对应的原生Channel的SelectionKey设置为可写。所以这里会被检索到
    29. Set readyKeys = this.nioSelector.selectedKeys();
    30. // Poll from channels that have buffered data (but nothing more from the underlying socket)
    31. if (dataInBuffers) {
    32. keysWithBufferedRead.removeAll(readyKeys); //so no channel gets polled twice
    33. Set toPoll = keysWithBufferedRead;
    34. keysWithBufferedRead = new HashSet<>(); //poll() calls will repopulate if needed
    35. pollSelectionKeys(toPoll, false, endSelect);
    36. }
    37. // 处理SelectionKey
    38. pollSelectionKeys(readyKeys, false, endSelect);
    39. // Clear all selected keys so that they are included in the ready count for the next select
    40. readyKeys.clear();
    41. // 注意这里第二个参数的区别,这里isImmediatelyConnected为true
    42. pollSelectionKeys(immediatelyConnectedKeys, true, endSelect);
    43. immediatelyConnectedKeys.clear();
    44. } else {
    45. madeReadProgressLastPoll = true; //no work is also "progress"
    46. }
    47. long endIo = time.nanoseconds();
    48. this.sensors.ioTime.record(endIo - endSelect, time.milliseconds());
    49. // Close channels that were delayed and are now ready to be closed
    50. completeDelayedChannelClose(endIo);
    51. // we use the time at the end of select to ensure that we don't close any connections that
    52. // have just been processed in pollSelectionKeys
    53. maybeCloseOldestConnection(endSelect);
    54. }

    Selector.poll()内部也会调用NioSelector.poll()获取到触发事件的SelectionKey,然后调用pollSelectionKeys()对各个SelectionKey进行遍历,这种做法和原生的NIO一样。

    1. // Selector
    2. void pollSelectionKeys(Set selectionKeys,
    3. boolean isImmediatelyConnected,
    4. long currentTimeNanos) {
    5. for (SelectionKey key : determineHandlingOrder(selectionKeys)) {
    6. KafkaChannel channel = channel(key);
    7. long channelStartTimeNanos = recordTimePerConnection ? time.nanoseconds() : 0;
    8. boolean sendFailed = false;
    9. String nodeId = channel.id();
    10. try {
    11. if (channel.ready() && (key.isReadable() || channel.hasBytesBuffered()) && !hasCompletedReceive(channel)
    12. && !explicitlyMutedChannels.contains(channel)) {
    13. attemptRead(channel);
    14. }
    15. try {
    16. attemptWrite(key, channel, nowNanos);
    17. } catch (Exception e) {
    18. sendFailed = true;
    19. throw e;
    20. }
    21. ...
    22. }
    23. // Selector
    24. private KafkaChannel channel(SelectionKey key) {
    25. return (KafkaChannel) key.attachment();
    26. }

    这里pollSelectionKeys我只抽取了关于读写的处理部分,当SelectionKey有事件触发时,可以通过SelectionKey.attachment()获取到跟原生Channel绑定的对象,在Kafka或者Netty中,也即是通过这种方式获取到自定义的Channel。

    当触发了可读事件的时候,Selector会调用attempedRead(),将NioChannel中的数据读取到KafkaChannel中。

    1. // Selector
    2. private void attemptRead(KafkaChannel channel) throws IOException {
    3. String nodeId = channel.id();
    4. // 从Channel中读取数据
    5. long bytesReceived = channel.read();
    6. // 如果读取到的数据不为空,则对监控数据进行更新
    7. if (bytesReceived != 0) {
    8. long currentTimeMs = time.milliseconds();
    9. sensors.recordBytesReceived(nodeId, bytesReceived, currentTimeMs);
    10. madeReadProgressLastPoll = true;
    11. NetworkReceive receive = channel.maybeCompleteReceive();
    12. // 当数据已经读取完全时,需要将channel、NetworkReceive也就是读取到的数据放入到completedReceives中
    13. // 等待处理
    14. if (receive != null) {
    15. addToCompletedReceives(channel, receive, currentTimeMs);
    16. }
    17. }
    18. if (channel.isMuted()) {
    19. outOfMemory = true; //channel has muted itself due to memory pressure.
    20. } else {
    21. madeReadProgressLastPoll = true;
    22. }
    23. }
    24. public long read() throws IOException {
    25. if (receive == null) {
    26. // 初始化NetworkReceive,负责存储接受到的数据
    27. // NetworkReceive底层是一个默认大小为4个字节的ByteBuffer
    28. receive = new NetworkReceive(maxReceiveSize, id, memoryPool);
    29. }
    30. // 从NioChannel中获取数据
    31. long bytesReceived = receive(this.receive);
    32. if (this.receive.requiredMemoryAmountKnown() && !this.receive.memoryAllocated() && isInMutableState()) {
    33. //pool must be out of memory, mute ourselves.
    34. mute();
    35. }
    36. return bytesReceived;
    37. }
    38. public long readFrom(ScatteringByteChannel channel) throws IOException {
    39. int read = 0;
    40. // Kafka请求的数据,前4个字节表示整个payload的大小,所以默认的ByteBuffer大小为4个字节
    41. if (size.hasRemaining()) {
    42. int bytesRead = channel.read(size);
    43. if (bytesRead < 0)
    44. throw new EOFException();
    45. read += bytesRead;
    46. if (!size.hasRemaining()) {
    47. size.rewind();
    48. // 获取到payload的消息大小
    49. int receiveSize = size.getInt();
    50. if (receiveSize < 0)
    51. throw new InvalidReceiveException("Invalid receive (size = " + receiveSize + ")");
    52. if (maxSize != UNLIMITED && receiveSize > maxSize)
    53. throw new InvalidReceiveException("Invalid receive (size = " + receiveSize + " larger than " + maxSize + ")");
    54. requestedBufferSize = receiveSize; //may be 0 for some payloads (SASL)
    55. if (receiveSize == 0) {
    56. buffer = EMPTY_BUFFER;
    57. }
    58. }
    59. }
    60. // buffer是另外一个ByteBuffer,负责存储payload
    61. // buffer会根据前面收到的大小来进行初始化
    62. if (buffer == null && requestedBufferSize != -1) { //we know the size we want but havent been able to allocate it yet
    63. buffer = memoryPool.tryAllocate(requestedBufferSize);
    64. if (buffer == null)
    65. log.trace("Broker low on memory - could not allocate buffer of size {} for source {}", requestedBufferSize, source);
    66. }
    67. if (buffer != null) {
    68. // 从Channel中读取数据写入到buffer中
    69. int bytesRead = channel.read(buffer);
    70. if (bytesRead < 0)
    71. throw new EOFException();
    72. read += bytesRead;
    73. }
    74. return read;
    75. }

    attempedRead()也是调用底层的channel.read()将请求读取到ByteBuffer中,值得注意的是,Kafka这里设计了每个消息的前4个字节表示payload的大小,所以在读取时需要根据前面4个字节初始化用于存储后续payload的ByteBuffer。

    读取完成后需要调用maybeCompleteReceive触发读取完毕事件

    1. public NetworkReceive maybeCompleteReceive() {
    2. if (receive != null && receive.complete()) {
    3. receive.payload().rewind();
    4. NetworkReceive result = receive;
    5. receive = null;
    6. return result;
    7. }
    8. return null;
    9. }

    再回来看下attemptWrite()的逻辑

    1. // Selector
    2. private void attemptWrite(SelectionKey key, KafkaChannel channel, long nowNanos) throws IOException {
    3. // 再次对KafkaChannel是否可以发送请求做一次判断。保证一个KafkaChannel只能有一个请求发送
    4. if (channel.hasSend()
    5. && channel.ready()
    6. && key.isWritable()
    7. && !channel.maybeBeginClientReauthentication(() -> nowNanos)) {
    8. write(channel);
    9. }
    10. }
    11. void write(KafkaChannel channel) throws IOException {
    12. String nodeId = channel.id();
    13. long bytesSent = channel.write();
    14. // 发送完成,触发发送完成事件
    15. Send send = channel.maybeCompleteSend();
    16. // We may complete the send with bytesSent < 1 if `TransportLayer.hasPendingWrites` was true and `channel.write()`
    17. // caused the pending writes to be written to the socket channel buffer
    18. if (bytesSent > 0 || send != null) {
    19. long currentTimeMs = time.milliseconds();
    20. if (bytesSent > 0)
    21. this.sensors.recordBytesSent(nodeId, bytesSent, currentTimeMs);
    22. if (send != null) {
    23. // 注意这里,对于发送完成的事件我们会放入到completedSends队列中
    24. this.completedSends.add(send);
    25. this.sensors.recordCompletedSend(nodeId, send.size(), currentTimeMs);
    26. }
    27. }
    28. }
    29. public long write() throws IOException {
    30. // send就是我们要发送的请求,如果为空则返回0表示此次发送的数据大小为0
    31. if (send == null)
    32. return 0;
    33. midWrite = true;
    34. return send.writeTo(transportLayer);
    35. }
    36. // ByteBufferSend
    37. public long writeTo(GatheringByteChannel channel) throws IOException {
    38. // 直接调用底层的NioChannel发送请求
    39. long written = channel.write(buffers);
    40. if (written < 0)
    41. throw new EOFException("Wrote negative bytes to channel. This shouldn't happen.");
    42. remaining -= written;
    43. pending = TransportLayers.hasPendingWrites(channel);
    44. return written;
    45. }

    write事件执行比较简单,因为在将ClientRequest封装为Send的时候,就已经将消息的header和payload序列话为ByteBuffer,所以仅需要调用底层的NioChannel发送请求即可。

    这里代码展示的时候提到发送成功后需要触发完成事件,也就是KafkaChannel.maybeCompleteSend(),作用时对发送完的请求进行清理,腾出空间发送下一个请求(注意读事件只要NioChannel读取完毕后,自然会清理SelectionKey的读事件)

    1. // KafkaChannel
    2. public Send maybeCompleteSend() {
    3. // Send发送完毕,也即是ByteBuffer中remaining小于0
    4. if (send != null && send.completed()) {
    5. midWrite = false;
    6. // 移除SelectionKey中的写事件
    7. transportLayer.removeInterestOps(SelectionKey.OP_WRITE);
    8. Send result = send;
    9. // 将Send设置为null,让下一个请求可以发送
    10. send = null;
    11. return result;
    12. }
    13. return null;
    14. }

    其实这里也可以看出,在KafkaChannel的Send不为空,也就是没有发送完成之前,是无法发送下一个请求的。

    现在我们知道读写事件如何监听和发送的,回过头来看下一旦这两个事件执行完毕后时如何通知到上层调用方的,来看下NetworkClient.poll()的后半部分

    1. // NetworkClient
    2. public List poll(long timeout, long now) {
    3. ...
    4. long updatedNow = this.time.milliseconds();
    5. List responses = new ArrayList<>();
    6. // 处理发送完成事件
    7. handleCompletedSends(responses, updatedNow);
    8. // 处理读取完成事件
    9. handleCompletedReceives(responses, updatedNow);
    10. handleDisconnections(responses, updatedNow);
    11. handleConnections();
    12. handleInitiateApiVersionRequests(updatedNow);
    13. handleTimedOutRequests(responses, updatedNow);
    14. completeResponses(responses);
    15. }

    来先看下如何处理读取完成事件,前面我们看到对于读取完成的事件,会放入到Selector.completedReceives队列中,在handleCompletedReceives()就是遍历这一队列

    1. private void handleCompletedReceives(List responses, long now) {
    2. for (NetworkReceive receive : this.selector.completedReceives()) {
    3. // 获取请求来源的broker
    4. String source = receive.source();
    5. // 从InflightRequest中根据broker获取请求,同时因为收到响应了
    6. // 需要从InFlightRequest将最老的请求踢出去
    7. InFlightRequest req = inFlightRequests.completeNext(source);
    8. // 获取响应
    9. Struct responseStruct = parseStructMaybeUpdateThrottleTimeMetrics(receive.payload(), req.header,
    10. throttleTimeSensor, now);
    11. // 根据broker返回的信息生成响应
    12. AbstractResponse response = AbstractResponse.
    13. parseResponse(req.header.apiKey(), responseStruct, req.header.apiVersion());
    14. if (log.isDebugEnabled()) {
    15. log.debug("Received {} response from node {} for request with header {}: {}",
    16. req.header.apiKey(), req.destination, req.header, response);
    17. }
    18. // If the received response includes a throttle delay, throttle the connection.
    19. maybeThrottle(response, req.header.apiVersion(), req.destination, now);
    20. if (req.isInternalRequest && response instanceof MetadataResponse)
    21. metadataUpdater.handleSuccessfulResponse(req.header, now, (MetadataResponse) response);
    22. else if (req.isInternalRequest && response instanceof ApiVersionsResponse)
    23. handleApiVersionsResponse(responses, req, now, (ApiVersionsResponse) response);
    24. else
    25. responses.add(req.completed(response, now));
    26. }
    27. }

    handleCompletedSends()的作用是根据Send的目标,清除在InFlightRequest中最老的元素,然后返回ClientResponse,并在NetworkClient.poll()中交给completeResponse()处理。

    1. private void completeResponses(List responses) {
    2. for (ClientResponse response : responses) {
    3. try {
    4. response.onComplete();
    5. } catch (Exception e) {
    6. log.error("Uncaught error in request completion:", e);
    7. }
    8. }
    9. }
    10. public void onComplete() {
    11. if (callback != null)
    12. callback.onComplete(this);
    13. }
    14. // RequestFutureCompletionHandler
    15. public void onComplete(ClientResponse response) {
    16. this.response = response;
    17. pendingCompletion.add(this);
    18. }

    ClientResponse中的回调时RequestCompletionHandler类型,也就是我们在创建请求时传递给ClienRequest中的参数,RequestCompletionHandler.onComplete()的逻辑是将自身塞入到pendCompletion队列中,这也对应ConsumerNetworkClient.poll()在方法的第一部分所调用的方法

    1. // ConsumerNetworkClient
    2. public void poll(Timer timer, PollCondition pollCondition, boolean disableWakeup) {
    3. firePendingCompletedRequests();
    4. ...
    5. }
    6. // ConsumerNetworkClient
    7. private void firePendingCompletedRequests() {
    8. boolean completedRequestsFired = false;
    9. for (;;) {
    10. RequestFutureCompletionHandler completionHandler = pendingCompletion.poll();
    11. if (completionHandler == null)
    12. break;
    13. // 触发完成事件
    14. completionHandler.fireCompletion();
    15. completedRequestsFired = true;
    16. }
    17. // wakeup the client in case it is blocking in poll for this future's completion
    18. if (completedRequestsFired)
    19. client.wakeup();
    20. }
    21. // ConsumerNetworkClient.RequestFutureCompletionHandler
    22. public void fireCompletion() {
    23. if (e != null) {
    24. future.raise(e);
    25. } else if (response.authenticationException() != null) {
    26. future.raise(response.authenticationException());
    27. } else if (response.wasDisconnected()) {
    28. log.debug("Cancelled request with header {} due to node {} being disconnected",
    29. response.requestHeader(), response.destination());
    30. future.raise(DisconnectException.INSTANCE);
    31. } else if (response.versionMismatch() != null) {
    32. future.raise(response.versionMismatch());
    33. } else {
    34. future.complete(response);
    35. }
    36. }
    37. // ReuqestFuture
    38. public void complete(T value) {
    39. try {
    40. if (value instanceof RuntimeException)
    41. throw new IllegalArgumentException("The argument to complete can not be an instance of RuntimeException");
    42. if (!result.compareAndSet(INCOMPLETE_SENTINEL, value))
    43. throw new IllegalStateException("Invalid attempt to complete a request future which is already complete");
    44. fireSuccess();
    45. } finally {
    46. completedLatch.countDown();
    47. }
    48. }
    49. // RequestFuture
    50. private void fireSuccess() {
    51. T value = value();
    52. while (true) {
    53. //
    54. RequestFutureListener listener = listeners.poll();
    55. if (listener == null)
    56. break;
    57. listener.onSuccess(value);
    58. }
    59. }

    这里RequestFutureListener就是调用我们前面通过compose()组装的对应请求完成。Kafka内部应该是认为当从KafkaChannel收到一个响应的时候,一定是对应InFlightRequest中最老的那个请求(mumumu,感觉怪怪的)。

    最后关于InFlightRequest:

    InFlightRequest负责存储的是已经发送过去请求,但是还没收到响应的。底层是一个Map>对象实现,用一个图表明的话就是:

  • 相关阅读:
    mysql锁
    句法引导的机器阅读理解
    数字集成电路设计(五、仿真验证与 Testbench 编写)(五)
    春秋云境靶场CVE-2021-41402漏洞复现(任意代码执行漏洞)
    【HTML】HTML基础4(超链接标签)
    远程桌面管理软件,如何使用远程桌面管理软件来远程控制服务器
    使用CNI网络插件(calico)实现docker容器跨主机互联
    用DBCC checkcatalog(数据库)检测出结构异常
    UNI-APP_开发支付宝小程序注意事项与解决方法,支付宝小程序图片显示问题
    java学习集合二 Set集合 Map集合
  • 原文地址:https://blog.csdn.net/NerverSimply/article/details/125964565