kafka消费者--加入consumergroup流程

一个消费者 group 由一个或者多个消费者组成,原则上每个消费者都需要有一个 groupId。这个可以在KafkaConsumer创建的时候指定。当消费者组只有一个消费者时,此时可以认为就是点对点模式;当有多个消费者时,就可以认为是发布订阅模式。

对于Broker 端的TopicPartition 而言,一个Partition 只能被一个消费者消费。也就是说,假设一个Topic 有 3 个分区(TopicA),此时groupId 为test 的消费者组内有4 个消费者,此时组内的每个成员都订阅了TopicA。这个时候最多有3个消费者可以消费到数据,因为主题的分区只有3个。

Coordinator 组件

Coordinator 表示一类组件,其中包含了消费者端的 ConsumerCoordinator 和 Broker 端的 GroupCoordinator。在Broker 端,GroupCoordinator 负责的是:消费者 group 成员管理以及 offset 提交。消费者 offset 提交在老版本的 Kafka 中是存储在 Zookeeper 中的。新版本的 Kafka 中,将Topic 的消费 offset 存储在一个叫 __consumer_offsets 的主题中。这是一个 Kafka 内部主题,默认情况下会有 50 个分区,每个分区会有3个副本。

后面我们将会详细的讲解 ConsumerCoordinator 在客户端和 Broker 端的交互流程。其中主要包括,一个消费者如何加入 group 和 offset 提交。

ConsumerCoordinator 主体流程

 

首先我们在调用 KafkaConsumer.poll() 时,首先会去调用 ConsumerCoordinator.poll() ,然后也会去调用位移提交的相关操作。对于 ConsumerCoordinator.poll(),也就是上图中的入口,下面看下入口 ConsumerCoordinator.poll() 的代码实现。

/**
 * 消费者加入 Group,它确保了这个 group 的 coordinator 是已知的,并且这个consumer是已经加入group,也用于offset周期性的提交。
 * 如果超时将会立即返回。
 */
public boolean poll(Timer timer) {
	// 可能会更新订阅的元数据信息
	maybeUpdateSubscriptionMetadata();
	// 用于测试
	invokeCompletedOffsetCommitCallbacks();

	if (subscriptions.partitionsAutoAssigned()) {
		/** 检查心跳线程运行是否正常, 如果心跳线程运行失败, 则抛出异常; 反之更新poll 调用的时间 */
		pollHeartbeat(timer.currentTimeMs());
		// coordinator 未知,初始化 Consumer Coordinator
		if (coordinatorUnknown() && !ensureCoordinatorReady(timer)) {
			return false;
		}
		// 判断是否需要重新加入group,如果订阅的 partition 变化或则分配的 partition 变化时,需要rejoin
		if (rejoinNeededOrPending()) {
			// 因为初始化的metadata刷新和初始化Rebalance存在竞态条件,在这里要确保metadata刷新在前面。这样可以保证在入组之前,订阅主题和Broker主题至少有一次匹配的过程。
			if (subscriptions.hasPatternSubscription()) {
				// 对于模式匹配订阅的Consumer,当一个Topic创建后,任何Consumer通过刷新metadata后发现新Topic后,都会触发一次Rebalance。因此此时可能会有大量的Rebalance操作,通过下面的backoff time判断会显著降低Rebalance的频率。
				if (this.metadata.timeToAllowUpdate(timer.currentTimeMs()) == 0) {
					this.metadata.requestUpdate();
				}

				if (!client.ensureFreshMetadata(timer)) {
					return false;
				}

				maybeUpdateSubscriptionMetadata();
			}
			// 确保group是active;加入group;分配到订阅的partition
			if (!ensureActiveGroup(timer)) {
				return false;
			}
		}
	} else {
		// For manually assigned partitions, if there are no ready nodes, await metadata.
		// If connections to all nodes fail, wakeups triggered while attempting to send fetch
		// requests result in polls returning immediately, causing a tight loop of polls. Without
		// the wakeup, poll() with no channels would block for the timeout, delaying re-connection.
		// awaitMetadataUpdate() initiates new connections with configured backoff and avoids the busy loop.
		// When group management is used, metadata wait is already performed for this scenario as
		// coordinator is unknown, hence this check is not required.
		if (metadata.updateRequested() && !client.hasReadyNodes(timer.currentTimeMs())) {
			client.awaitMetadataUpdate(timer);
		}
	}
	// 设置自动commit时,当定时达到时,执行自动commit
	maybeAutoCommitOffsetsAsync(timer.currentTimeMs());
	return true;
}

简单的描述一下上面的流程:

1、ensureCoordinatorReady() 的主要作用是发送GroupCoordinator请求,并建立连接。

2、判断是否需要加入group,如果订阅主题分区发生变化,或者新消费者入组等,需要重新入组。此时是通过ensureActiveGroup() 发送JoinGroup、SyncGroup,并获取到分配给自身的TopicPartition。

3、检测心跳线程是否正常,心跳线程需要定时向GroupCoordinator发送心跳,超过约定阈值就会认为Consumer离组,触发Rebalance。

4、如果设置的是自动提交位移,达到时间阈值就会提交offset。

后面将对于 ensureCoordinatorReady() 和 ensureActiveGroup() 详细说明一下。

消费者group状态

对于消费者group的状态包含了下面几种

 

消费者group各个状态之间的流转如下所示

 

下面简单的描述一下状态流转过程:最开始消费者group是Empty 状态,当Rebalance 开启后,会被置于 RreparingRebalance 状态等待成员加入group。之后当有成员入组时,会变更到CompletingRebalance 状态等待分配方案。分配完成后会流转到Stable 状态完成充平衡。

当有新成员入组或者成员退出时,消费者group 状态从 Stable 直接变为 PreparingRebalance 状态,此时所有成员都需要重新加入group。当所有的成员都退出组时,状态会变为 Empty。Kafka 定期自动删除过期位移的条件就是,group要处于 Empty 状态。当消费者 group 停用了很长时间(超过7天),此时Kafka 就可能将其删除。

与Broker建立TCP连接

与Broker 建立TCP连接是通过 ensureCoordinatorReady() 方法实现的,下面我们看一下方法的具体实现。

/**
 * 确保 coordinator ready 去接收请求 (已经连接,并可以发送请求)
 */
protected synchronized boolean ensureCoordinatorReady(final Timer timer) {
    if (!coordinatorUnknown())
        return true;

    do {
        // 获取 GroupCoordinator,并建立连接
        final RequestFuture<Void> future = lookupCoordinator();
        client.poll(future, timer);

        if (!future.isDone()) {
            // ran out of time
            break;
        }
        // 如果获取的过程中失败了
        if (future.failed()) {
            if (future.isRetriable()) {
                log.debug("Coordinator discovery failed, refreshing metadata");
                client.awaitMetadataUpdate(timer);
            } else
                throw future.exception();
        } else if (coordinator != null && client.isUnavailable(coordinator)) {
            // 当找到了 coordinator,但是连接失败了,此时标记为 dead,然后重试
            markCoordinatorUnknown();
            timer.sleep(rebalanceConfig.retryBackoffMs);
        }
    } while (coordinatorUnknown() && timer.notExpired());

    return !coordinatorUnknown();
}

上面方法入口中,与Broker 端建立TCP 连接的主要逻辑委派给了 lookupCoordinator() 去实现。

/**
 * 选择最空闲的节点, 发送 groupCoordinator请求
 */
protected synchronized RequestFuture<Void> lookupCoordinator() {
	if (findCoordinatorFuture == null) {
		// 选择一个连接数最少的节点(假装最空闲)
		Node node = this.client.leastLoadedNode();
		if (node == null) {
			log.debug("No broker available to send FindCoordinator request");
			return RequestFuture.noBrokersAvailable();
		} else
			// 发送请求并处理响应
			findCoordinatorFuture = sendFindCoordinatorRequest(node);
	}
	return findCoordinatorFuture;
}

然后发送GroupCoordinator 请求的详情如下:

/**
 * 发送 CoordinatorRequest 请求
 * 为 group 寻找 coordinator,发送 GroupMetadata 请求到Broker。
 */
private RequestFuture<Void> sendFindCoordinatorRequest(Node node) {
	// 初始化 GroupMetadata 请求
	log.debug("Sending FindCoordinator request to broker {}", node);
	FindCoordinatorRequest.Builder requestBuilder = new FindCoordinatorRequest.Builder(new FindCoordinatorRequestData().setKeyType(CoordinatorType.GROUP.id()).setKey(this.rebalanceConfig.groupId));
	// 请求发送之后的响应结果,委派给了 Handler 执行
	return client.send(node, requestBuilder).compose(new FindCoordinatorResponseHandler());
}

// 对 GroupCoordinator 的 response 进行处理,回调
private class FindCoordinatorResponseHandler extends RequestFutureAdapter<ClientResponse, Void> {
	@Override
	public void onSuccess(ClientResponse resp, RequestFuture<Void> future) {
		log.debug("Received FindCoordinator response {}", resp);
		clearFindCoordinatorFuture();

		FindCoordinatorResponse findCoordinatorResponse = (FindCoordinatorResponse) resp.responseBody();
		Errors error = findCoordinatorResponse.error();
		if (error == Errors.NONE) {
			synchronized (AbstractCoordinator.this) {
				int coordinatorConnectionId = Integer.MAX_VALUE - findCoordinatorResponse.data().nodeId();
				/** 如果正确获取 GroupCoordinator 时, 建立连接,并更新心跳时间 */
				AbstractCoordinator.this.coordinator = new Node(coordinatorConnectionId, findCoordinatorResponse.data().host(), findCoordinatorResponse.data().port());
				log.info("Discovered group coordinator {}", coordinator);
				// 初始化 tcp 连接
				client.tryConnect(coordinator);
				// 更新心跳时间
				heartbeat.resetSessionTimeout();
			}
			future.complete(null);
		} else if (error == Errors.GROUP_AUTHORIZATION_FAILED) {
			future.raise(new GroupAuthorizationException(rebalanceConfig.groupId));
		} else {
			log.debug("Group coordinator lookup failed: {}", findCoordinatorResponse.data().errorMessage());
			future.raise(error);
		}
	}

	@Override
	public void onFailure(RuntimeException e, RequestFuture<Void> future) {
		clearFindCoordinatorFuture();
		super.onFailure(e, future);
	}
}

 

发送加入组请求(Rebalance流程)

消费者首次加入group也可以认为是Rebalance的一种,其中包含了两类请求:JoinGroup 和 SyncGroup 请求。我们先看一下两次请求的流程:

 

当组内成员加入group时,它会向协调者发送一个JoinGroup请求。请求中会将自己要订阅的Topic 上报,这样协调者就可以收集到所有成员的订阅信息。收集完订阅信息之后,通常情况下,第一个发送JoinGroup 请求的成员将会自动称为Leader。这里面的Leader 和 分区的Leader 副本不是一个概念,这里面的Leader 是消费者group 的 Leader,它将会负责具体的分区分配方案制定。下面我们看一下源代码的实现:

/**
 * 确保Group是active,并且加入该group
 * 向GroupCoordinator发送JoinGroup、SyncGroup请求,并获取分配的主题分区
 */
boolean ensureActiveGroup(final Timer timer) {
	// 确保 GroupCoordinator 已经连接,防止之前建立的连接断开
	if (!ensureCoordinatorReady(timer)) {
		return false;
	}
	// 启动心跳发送线程(并不一定发送心跳,满足条件后才会发送心跳)
	startHeartbeatThreadIfNeeded();
	// 发送 JoinGroup 请求,并对返回的信息进行处理
	return joinGroupIfNeeded(timer);
}

JoinGroup 的请求发送是在 joinGroupIfNeeded() 中实现的:

/**
 * 发送 JoinGroup + SyncGroup 请求
 */
boolean joinGroupIfNeeded(final Timer timer) {
	while (rejoinNeededOrPending()) {
		if (!ensureCoordinatorReady(timer)) {
			return false;
		}

		/** 触发 onJoinPrepare, 包括 offset commit 和 rebalance listener */
		if (needsJoinPrepare) {
			onJoinPrepare(generation.generationId, generation.memberId);
			needsJoinPrepare = false;
		}
		/** 初始化 JoinGroup 请求,并发送该请求 */
		final RequestFuture<ByteBuffer> future = initiateJoinGroup();
		client.poll(future, timer);
		if (!future.isDone()) {
			// we ran out of time
			return false;
		}
		/** 到这一步,时间上SyncGroup 已经成功了 */
		if (future.succeeded()) {
			ByteBuffer memberAssignment = future.value().duplicate();
			onJoinComplete(generation.generationId, generation.memberId, generation.protocol, memberAssignment);

			// 重置 joinFuture 为空
			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(rebalanceConfig.retryBackoffMs);
		}
	}
	return true;
}

下面我们看下 initiateJoinGroup() 的实现:

/**
 * 发送JoinGroup请求,并添加listener
 */
private synchronized RequestFuture<ByteBuffer> initiateJoinGroup() {
	if (joinFuture == null) {
		/** rebalance 期间,心跳线程停止 */
		disableHeartbeatThread();
		/** 标记为 rebalance */
		state = MemberState.REBALANCING;
		joinFuture = sendJoinGroupRequest(); /** 发送 JoinGroup 请求 */
		joinFuture.addListener(new RequestFutureListener<ByteBuffer>() {
			@Override
			public void onSuccess(ByteBuffer value) {
				synchronized (AbstractCoordinator.this) {
					log.info("Successfully joined group with generation {}", generation.generationId);
					/** 标记 Consumer 为 stable */
					state = MemberState.STABLE;
					rejoinNeeded = false;

					if (heartbeatThread != null)
						heartbeatThread.enable();
				}
			}

			@Override
			public void onFailure(RuntimeException e) {
				synchronized (AbstractCoordinator.this) {
					/** 标记 Consumer 为 Unjoined */
					state = MemberState.UNJOINED;
				}
			}
		});
	}
	return joinFuture;
}

下面我们看一下发送请求的方法 sendJoinGroupRequest():

/**
 * 发送 JoinGroup 请求并返回分配结果(在 JoinGroupResponseHandler中实现)
 */
RequestFuture<ByteBuffer> sendJoinGroupRequest() {
	if (coordinatorUnknown())
		return RequestFuture.coordinatorNotAvailable();

	// 发送JoinGroup请求
	log.info("(Re-)joining group");
	JoinGroupRequest.Builder requestBuilder = new JoinGroupRequest.Builder(
			new JoinGroupRequestData()
					.setGroupId(rebalanceConfig.groupId)
					.setSessionTimeoutMs(this.rebalanceConfig.sessionTimeoutMs)
					.setMemberId(this.generation.memberId)
					.setGroupInstanceId(this.rebalanceConfig.groupInstanceId.orElse(null))
					.setProtocolType(protocolType())
					.setProtocols(metadata())
					.setRebalanceTimeoutMs(this.rebalanceConfig.rebalanceTimeoutMs)
	);
	log.debug("Sending JoinGroup ({}) to coordinator {}", requestBuilder, this.coordinator);

	int joinGroupTimeoutMs = Math.max(rebalanceConfig.rebalanceTimeoutMs, rebalanceConfig.rebalanceTimeoutMs + 5000);
	return client.send(coordinator, requestBuilder, joinGroupTimeoutMs).compose(new JoinGroupResponseHandler());
}

/**
 * 处理 JoinGroup response 的 handler(同步 group 信息)
 */
private class JoinGroupResponseHandler extends CoordinatorResponseHandler<JoinGroupResponse, ByteBuffer> {
	@Override
	public void handle(JoinGroupResponse joinResponse, RequestFuture<ByteBuffer> future) {
		Errors error = joinResponse.error();
		if (error == Errors.NONE) {
			log.debug("Received successful JoinGroup response: {}", joinResponse);
			sensors.joinLatency.record(response.requestLatencyMs());

			synchronized (AbstractCoordinator.this) {
				/** 如果此时 Consumer 的状态不是 rebalacing,就引起异常 */
				if (state != MemberState.REBALANCING) {
					// if the consumer was woken up before a rebalance completes, we may have already left
					// the group. In this case, we do not want to continue with the sync group.
					future.raise(new UnjoinedGroupException());
				} else {
					AbstractCoordinator.this.generation = new Generation(joinResponse.data().generationId(), joinResponse.data().memberId(), joinResponse.data().protocolName());
					/** JoinGroup成功,下面需要进行SyncGroup,获取分配的主题分区 */
					if (joinResponse.isLeader()) {
						// Leader 将会执行分配方案,并发送SyncGroup请求
						onJoinLeader(joinResponse).chain(future);
					} else {
						onJoinFollower().chain(future);
					}
				}
			}
		} else if (error == Errors.COORDINATOR_LOAD_IN_PROGRESS) {
			log.debug("Attempt to join group rejected since coordinator {} is loading the group.", coordinator());
			// backoff and retry
			future.raise(error);
		} else if (error == Errors.UNKNOWN_MEMBER_ID) {
			// reset the member id and retry immediately
			resetGeneration();
			log.debug("Attempt to join group failed due to unknown member id.");
			future.raise(error);
		} else if (error == Errors.COORDINATOR_NOT_AVAILABLE
				|| error == Errors.NOT_COORDINATOR) {
			// re-discover the coordinator and retry with backoff
			markCoordinatorUnknown();
			log.debug("Attempt to join group failed due to obsolete coordinator information: {}", error.message());
			future.raise(error);
		} else if (error == Errors.FENCED_INSTANCE_ID) {
			log.error("Received fatal exception: group.instance.id gets fenced");
			future.raise(error);
		} else if (error == Errors.INCONSISTENT_GROUP_PROTOCOL
				|| error == Errors.INVALID_SESSION_TIMEOUT
				|| error == Errors.INVALID_GROUP_ID
				|| error == Errors.GROUP_AUTHORIZATION_FAILED
				|| error == Errors.GROUP_MAX_SIZE_REACHED) {
			// log the error and re-throw the exception
			log.error("Attempt to join group failed due to fatal error: {}", error.message());
			if (error == Errors.GROUP_MAX_SIZE_REACHED) {
				future.raise(new GroupMaxSizeReachedException(rebalanceConfig.groupId));
			} else if (error == Errors.GROUP_AUTHORIZATION_FAILED) {
				future.raise(new GroupAuthorizationException(rebalanceConfig.groupId));
			} else {
				future.raise(error);
			}
		} else if (error == Errors.UNSUPPORTED_VERSION) {
			log.error("Attempt to join group failed due to unsupported version error. Please unset field group.instance.id and retry" + "to see if the problem resolves");
			future.raise(error);
		} else if (error == Errors.MEMBER_ID_REQUIRED) {
			// Broker requires a concrete member id to be allowed to join the group. Update member id
			// and send another join group request in next cycle.
			synchronized (AbstractCoordinator.this) {
				AbstractCoordinator.this.generation = new Generation(OffsetCommitRequest.DEFAULT_GENERATION_ID, joinResponse.data().memberId(), null);
				AbstractCoordinator.this.rejoinNeeded = true;
				AbstractCoordinator.this.state = MemberState.UNJOINED;
			}
			future.raise(error);
		} else {
			// unexpected error, throw the exception
			log.error("Attempt to join group failed due to unexpected error: {}", error.message());
			future.raise(new KafkaException("Unexpected error in join group response: " + error.message()));
		}
	}
}

下面我们看一下 SyncGroup 的请求发送流程:

/**
 * 当consumer为follower时,发送SyncGroup获取分配结果
 */
private RequestFuture<ByteBuffer> onJoinFollower() {
	// 发送空消息的SyncGroup
	SyncGroupRequest.Builder requestBuilder =
			new SyncGroupRequest.Builder(
					new SyncGroupRequestData()
							.setGroupId(rebalanceConfig.groupId)
							.setMemberId(generation.memberId)
							.setGroupInstanceId(this.rebalanceConfig.groupInstanceId.orElse(null))
							.setGenerationId(generation.generationId)
							.setAssignments(Collections.emptyList())
			);
	log.debug("Sending follower SyncGroup to coordinator {}: {}", this.coordinator, requestBuilder);
	return sendSyncGroupRequest(requestBuilder);
}

/**
 * 当consumer为leader时,对group下的所有实例进行分配,将assign的结果通过SyncGroup请求发送到GroupCoordinator
 */
private RequestFuture<ByteBuffer> onJoinLeader(JoinGroupResponse joinResponse) {
	try {
		// perform the leader synchronization and send back the assignment for the group
		/** 进行 assign 操作 */
		Map<String, ByteBuffer> groupAssignment = performAssignment(joinResponse.data().leader(), joinResponse.data().protocolName(), joinResponse.data().members());

		List<SyncGroupRequestData.SyncGroupRequestAssignment> groupAssignmentList = new ArrayList<>();
		for (Map.Entry<String, ByteBuffer> assignment : groupAssignment.entrySet()) {
			groupAssignmentList.add(new SyncGroupRequestData.SyncGroupRequestAssignment().setMemberId(assignment.getKey()).setAssignment(Utils.toArray(assignment.getValue())));
		}

		SyncGroupRequest.Builder requestBuilder =
				new SyncGroupRequest.Builder(
						new SyncGroupRequestData()
								.setGroupId(rebalanceConfig.groupId)
								.setMemberId(generation.memberId)
								.setGroupInstanceId(this.rebalanceConfig.groupInstanceId.orElse(null))
								.setGenerationId(generation.generationId)
								.setAssignments(groupAssignmentList)
				);
		log.debug("Sending leader SyncGroup to coordinator {}: {}", this.coordinator, requestBuilder);
		/** 发送 sync-group 请求 */
		return sendSyncGroupRequest(requestBuilder);
	} catch (RuntimeException e) {
		return RequestFuture.failure(e);
	}
}

/**
 * 发送 SyncGroup 请求,获取对 partition 分配的安排
 */
private RequestFuture<ByteBuffer> sendSyncGroupRequest(SyncGroupRequest.Builder requestBuilder) {
	if (coordinatorUnknown())
		return RequestFuture.coordinatorNotAvailable();
	return client.send(coordinator, requestBuilder).compose(new SyncGroupResponseHandler());
}

private class SyncGroupResponseHandler extends CoordinatorResponseHandler<SyncGroupResponse, ByteBuffer> {
	@Override
	public void handle(SyncGroupResponse syncResponse, RequestFuture<ByteBuffer> future) {
		Errors error = syncResponse.error();
		if (error == Errors.NONE) {
			sensors.syncLatency.record(response.requestLatencyMs());
			future.complete(ByteBuffer.wrap(syncResponse.data.assignment()));
		} else {
			// join的标志位设置为true
			requestRejoin();

			if (error == Errors.GROUP_AUTHORIZATION_FAILED) {
				future.raise(new GroupAuthorizationException(rebalanceConfig.groupId));
			} else if (error == Errors.REBALANCE_IN_PROGRESS) {
				// group正在rebalance,任务失败
				log.debug("SyncGroup failed because the group began another rebalance");
				future.raise(error);
			} else if (error == Errors.FENCED_INSTANCE_ID) {
				log.error("Received fatal exception: group.instance.id gets fenced");
				future.raise(error);
			} else if (error == Errors.UNKNOWN_MEMBER_ID || error == Errors.ILLEGAL_GENERATION) {
				log.debug("SyncGroup failed: {}", error.message());
				resetGeneration();
				future.raise(error);
			} else if (error == Errors.COORDINATOR_NOT_AVAILABLE || error == Errors.NOT_COORDINATOR) {
				log.debug("SyncGroup failed: {}", error.message());
				markCoordinatorUnknown();
				future.raise(error);
			} else {
				future.raise(new KafkaException("Unexpected error from SyncGroup: " + error.message()));
			}
		}
	}
}

 

消费者Rebalance的几种场景

我们先看一下Rebalance触发的几个提交件: 1、组成员数量发生变化;2、订阅主题数量发生变化;3、订阅主题的分区数发生变化。对于一个运行中的应用,上面3 中场景中,第一种场景触发Rebalance的可能性比较大。

下面我们看一下Rebalance的各种场景:

新成员入组

 

组成员主动离组

组成员崩溃离组

Rebalance时组内成员需要提交offset

 

 

 

posted @ 2023-01-30 17:01  車輪の唄  阅读(831)  评论(0编辑  收藏  举报  来源