文章目录

  • 前言
  • ConsumerCoordinator
  • poll
  • sendJoinGroupRequest



前言

本文主要解析ConsumerCoordinator这个模型对于消费者加入组、位移提交的底层原理。

Note:关于心跳线程这一部分,在下一篇文章中会解析。


ConsumerCoordinator

首先看下类的关系图。

咖啡店人员架构 咖啡人员架构图_客户端

来到它的父类 - AbstractCoordinator,看下它的有关注释说明。

AbstractCoordinator对一个消费者组中的消费者与Kafka服务端的协调者之间的交互进行管理。
同时又指定了Kafka的组管理协议的四个操作:

咖啡店人员架构 咖啡人员架构图_客户端_02

组成员注册时,需要协调者提供它们的元数据。
协调者从组成员中选择一个作为leader。
leader收集组成员的元数据,分配状态。
每个成员收到leader分配的状态,开始处理。

而ConsumerCoordinator,继承了AbstractCoordinator相关逻辑,是消费者的协调者。


poll

主要处理消费者加入消费者组的相关逻辑、位移的自动提交。

public boolean poll(Timer timer) {
	// 可能会更新元数据快照。
    maybeUpdateSubscriptionMetadata();
    // 触发OffsetCommitCallback#onComplete逻辑。
    invokeCompletedOffsetCommitCallbacks();
    // 如果订阅类型是根据主题自动分配分区,根据主题pattern自动分配分区两者之一。
    if (subscriptions.partitionsAutoAssigned()) {
    	// 唤醒心跳线程。
        pollHeartbeat(timer.currentTimeMs());
        // 如果协调者未知 && 客户端无法与协调者节点连接就绪。
        if (coordinatorUnknown() && !ensureCoordinatorReady(timer)) {
            return false;
        }
        // 如果需要加入组 或者 加入组的请求还未完成。
        if (rejoinNeededOrPending()) {        
        	// 如果订阅类型是根据主题pattern自动分配分区
            if (subscriptions.hasPatternSubscription()) {
                if (this.metadata.timeToAllowUpdate(timer.currentTimeMs()) == 0) {
                	// 请求元数据更新
                    this.metadata.requestUpdate();
                }
                // 如果元数据更新失败
                if (!client.ensureFreshMetadata(timer)) {
                    return false;
                }
                // 可能会更新元数据快照
                maybeUpdateSubscriptionMetadata();
            }
            // 尝试加入组
            if (!ensureActiveGroup(timer)) {     
                return false;
            }
        }
    } else {
    	// 元数据需要更新 && 客户端与节点的连接状态没有处于就绪状态
        if (metadata.updateRequested() && !client.hasReadyNodes(timer.currentTimeMs())) {
        	// 客户端进行元数据更新
            client.awaitMetadataUpdate(timer);
        }
    }
    // 提交位移
    maybeAutoCommitOffsetsAsync(timer.currentTimeMs());
    return true;
}

咖啡店人员架构 咖啡人员架构图_协调者_03

如果消费者元数据的更新版本号 > 元数据快照保存的版本号,那么就更新元数据快照。

咖啡店人员架构 咖啡人员架构图_元数据_04

触发队列中所有的OffsetCommitCompletion#invoke方法。

咖啡店人员架构 咖啡人员架构图_元数据_05

触发OffsetCommitCallback#onComplete逻辑。

咖啡店人员架构 咖啡人员架构图_咖啡店人员架构_06

唤醒心跳线程。

咖啡店人员架构 咖啡人员架构图_元数据_07

更新heartbeatTimer、sessionTimer、pollTimer保存的时间为当前时间。
heartbeatTimer的截止时间与当前时间的比较。判断是否可以发送心跳。

咖啡店人员架构 咖啡人员架构图_客户端_08

更新heartbeatTimer、sessionTimer、pollTimer保存的时间为当前时间。
重置pollTimer的截止时间。

咖啡店人员架构 咖啡人员架构图_元数据_09

检查协调者是否为null。

咖啡店人员架构 咖啡人员架构图_客户端_10

如果客户端连接协调者节点失败,标记协调者未知(coordinator为null)。

咖啡店人员架构 咖啡人员架构图_咖啡店人员架构_11

确保客户端与协调者节点连接就绪。

对于协调者节点,倾向于寻找服务端inflightRequests数量为0或者最少的节点。


咖啡店人员架构 咖啡人员架构图_协调者_12

需要加入组 或者 之前加入组的请求还未完成。

咖啡店人员架构 咖啡人员架构图_咖啡店人员架构_13

尝试让消费者加入组。
启动一个心跳线程。

咖啡店人员架构 咖啡人员架构图_协调者_14

启动心跳线程。
boolean joinGroupIfNeeded(final Timer timer) {
	// 需要加入组 || 加入组的请求处理还未完成
    while (rejoinNeededOrPending()) {
    	// 客户端不能成功连接协调者节点
        if (!ensureCoordinatorReady(timer)) {
            return false;
        }
        // 如果需要为加入组做些准备
        if (needsJoinPrepare) {
        	// 进行加入组的准备工作
            onJoinPrepare(generation.generationId, generation.memberId);
            needsJoinPrepare = false;
        }
        // 发送一个加入组的请求
        final RequestFuture<ByteBuffer> future = initiateJoinGroup();
        // 委派NetworkClient进行底层的交互
        client.poll(future, timer);
        if (!future.isDone()) {
            return false;
        }

        if (future.succeeded()) {
            ByteBuffer memberAssignment = future.value().duplicate();
            // 对加入组完成后的后续处理。     
            onJoinComplete(generation.generationId, generation.memberId, generation.protocol, memberAssignment);
            // 重置joinFuture为null
            resetJoinGroupFuture();
            needsJoinPrepare = true;
        } else {
            resetJoinGroupFuture();
            final RuntimeException exception = future.exception();
            if (exception instanceof UnknownMemberIdException ||
                    exception instanceof RebalanceInProgressException ||
                    exception instanceof IllegalGenerationException ||
                    exception instanceof MemberIdRequiredException)
                continue;
            else if (!future.isRetriable())
                throw exception;

            timer.sleep(retryBackoffMs);
        }
    }
    return true;
}
加入组的完整过程。

咖啡店人员架构 咖啡人员架构图_元数据_15

可能会自动提交位移。
触发ConsumerRebalanceListener#onPartitionsRevoked逻辑。
重置组订阅关系。

咖啡店人员架构 咖啡人员架构图_协调者_16

关闭心跳线程。
发送一个加入组的请求。(后续会详细分析)
添加监听器。

咖啡店人员架构 咖啡人员架构图_客户端_17

加入组成功或者失败的回调处理。

@Override
protected void onJoinComplete(int generation,
                              String memberId,
                              String assignmentStrategy,
                              ByteBuffer assignmentBuffer) {
    // only the leader is responsible for monitoring for metadata changes (i.e. partition changes)
    if (!isLeader)
        assignmentSnapshot = null;

	// 寻找指定名字的PartitionAssignor
    PartitionAssignor assignor = lookupAssignor(assignmentStrategy);
    if (assignor == null)
        throw new IllegalStateException("Coordinator selected invalid assignment protocol: " + assignmentStrategy);

    Assignment assignment = ConsumerProtocol.deserializeAssignment(assignmentBuffer);
    // 如果实际分配的分区与保存的分配分区信息不匹配
    if (!subscriptions.assignFromSubscribed(assignment.partitions())) {
    	// 处理实际分配的分区与保存的分配分区信息不匹配的情况
        handleAssignmentMismatch(assignment);
        return;
    }

    Set<TopicPartition> assignedPartitions = subscriptions.assignedPartitions();

    // 更新订阅关系
    maybeUpdateJoinedSubscription(assignedPartitions);

    // 触发PartitionAssignor#onAssigment逻辑
    assignor.onAssignment(assignment, generation);

    // 如果开启了自动提交
    if (autoCommitEnabled)
    	// 更新当前时间,重置截止时间
        this.nextAutoCommitTimer.updateAndReset(autoCommitIntervalMs);

    ConsumerRebalanceListener listener = subscriptions.rebalanceListener();
    log.info("Setting newly assigned partitions: {}", Utils.join(assignedPartitions, ", "));
    try {
    	// 触发监听器的onPartitionsAssigned逻辑
        listener.onPartitionsAssigned(assignedPartitions);
    } catch (WakeupException | InterruptException e) {
        throw e;
    } catch (Exception e) {
        log.error("User provided listener {} failed on partition assignment", listener.getClass().getName(), e);
    }
}
加入组完成之后的处理。

咖啡店人员架构 咖啡人员架构图_咖啡店人员架构_18

自动提交位移。

咖啡店人员架构 咖啡人员架构图_协调者_19

对所有消费过的位移进行提交,并且附加回调。

咖啡店人员架构 咖啡人员架构图_咖啡店人员架构_20

触发队列中存储的OffsetCommitCallback#onComplete逻辑。
提交位移。

咖啡店人员架构 咖啡人员架构图_咖啡店人员架构_21

发送位移提交请求。
如果成功提交,则触发拦截器的onCommit逻辑。

咖啡店人员架构 咖啡人员架构图_咖啡店人员架构_22

构造OffsetCommitRequest.Builder实例。
调用ConsumerNetworkClient进行发送。
使用OffsetCommitResponseHandler对响应进行处理。

咖啡店人员架构 咖啡人员架构图_元数据_23

如果服务端成功处理的话,客户端这里会触发RequestFutureListener#onSuccess逻辑。

sendJoinGroupRequest

前面的代码分析中,有提到发送一个加入组的请求。

接下来简单看下这段代码逻辑。

咖啡店人员架构 咖啡人员架构图_元数据_24

构造JoinGroupRequest。
委派NetworkClient发送这个请求。

对于后续的响应处理。这里只看下请求成功时的响应处理。

咖啡店人员架构 咖啡人员架构图_咖啡店人员架构_25

可见,根据角色的不同,分为了onJoinLeader和onJoinFollower两个处理逻辑。


首先看下onJoinLeader。

咖啡店人员架构 咖啡人员架构图_协调者_26

使用指定的PartitionAssignor分配新的分区分配策略。
构造SyncGroupRequest。
委派NetworkClient发送请求。

咖啡店人员架构 咖啡人员架构图_客户端_27


然后看下onJoinFollower。

咖啡店人员架构 咖啡人员架构图_协调者_28

构造SyncGroupRequest。
委派NetworkClient发送请求。