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