消费者rebalance机制分析

本文详细介绍了Kafka消费者组rebalance机制的触发时机及整个rebalance过程,包括查找GroupCoordinator、JoinGroup阶段和SynchronizingGroupState阶段的具体流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 触发rebalance的时机

# 有新的消费者加入

# 有消费者宕机或者下线

# 消费者主动退出消费者组

# 消费者组订阅的topic出现分区数量变化

# 消费者调用unsubscrible取消对某topic的订阅

 

二 rebalance过程分析

2.1 查找GroupCoordinator

在poll数据的时候,首先会调用ConsumerCoordinator#poll查找GroupCoordinator

# 调器事件的轮询。这确保了协调器是已知的,并且是消费者加入了这个群组(如果它使用的是组管理)。这也可以处理周期性的offset提交,如果启用了它们。

public void poll(long now) {
    // 调用offset 提交请求的回调函数,如果有
   
invokeCompletedOffsetCommitCallbacks();

    // 判断订阅状态是否自动分配分区且coordinator不为空
   
if (subscriptions.partitionsAutoAssigned() && coordinatorUnknown()) {
        // 确保coordinator已经准备好接收请求了
       
ensureCoordinatorReady();
        now = time.milliseconds();
    }
    // 判断是否需要重新join
   
if (needRejoin()) {
        // 由于初始元数据获取和初始重新平衡之间的竞争条件,我们需要确保元数据在开始加入之前是新鲜的。
        //
确保了我们在join之前至少有一次与集群的主题的模式匹配
       
if (subscriptions.hasPatternSubscription())
            // 刷新元数据
           
client.ensureFreshMetadata();
        // 确保协调器可用,心跳线程开启和组可用
       
ensureActiveGroup();
        now = time.milliseconds();
    }
    // 检测心跳线程的状态
   
pollHeartbeat(now);
    // 异步的自动提交offset
   
maybeAutoCommitOffsetsAsync(now);
}

 

# 判断GroupCoordinator是否存在且可连接

public boolean coordinatorUnknown() {
    return coordinator() == null;
}
protected synchronized Node coordinator() {
    if (coordinator != null && client.connectionFailed(coordinator)) {
        coordinatorDead();
        return null;
    }
    return this.coordinator;
}

 

# 如果存在则确认GroupCoordinator是否已经准备好了

// 判断对于这个组的coordinatior是否已经准备好接受请求,否则一直阻塞
public synchronized void ensureCoordinatorReady() {
    while (coordinatorUnknown()) {
        // GroupCoordinator,并返回一个请求结果
        RequestFuture<Void> future = lookupCoordinator();
        client.poll(future);
        // 异常处理
        if (future.failed()) {
            if (future.isRetriable())
                // 阻塞更新metadata中的集群元数据
                client.awaitMetadataUpdate();
            else
                throw future.exception();
        } else if (coordinator != null && client.connectionFailed(coordinator)) {
            // 如果连接不上GroupCoordinator,则退避一段时间,然后重试
            coordinatorDead();
            time.sleep(retryBackoffMs);
        }
    }
}

 

# 它会找GroupCoordinator,发送GroupCoordinatorRequest请求,并返回一个FutureRequest

protected synchronized RequestFuture<Void> lookupCoordinator() {
    if (findCoordinatorFuture == null) {
        // 查找集群负载最低的Node节点
        Node= this.client.leastLoadedNode();
        // 如果找到了,则调用sendGroupCoordinatorRequest
        if (node == null) {
            return RequestFuture.noBrokersAvailable();
        } else
            findCoordinatorFuture = sendGroupCoordinatorRequest(node);
    }
    return findCoordinatorFuture;
}

 

private RequestFuture<Void> sendGroupCoordinatorRequest(Node node) {
    // 创建GroupCoordinatorRequest请求
    log.debug("Sending coordinator request for group {} to broker {}", groupId, node);
    GroupCoordinatorRequest metadataRequest = new GroupCoordinatorRequest(this.groupId);
    // ConsumerNetworkClient将创建的GroupCoordinatorRequest请求放入一个unsent列表,等待发送
    // 并返回RequestFuture对象,返回的RequestFuture对象经过compose的适配
    return client.send(node, ApiKeys.GROUP_COORDINATOR, metadataRequest)
                 .compose(new GroupCoordinatorResponseHandler());
}

 

 

这里对返回的RequestFuture做了一些拦截处理:

private class GroupCoordinatorResponseHandler extends RequestFutureAdapter<ClientResponse, Void> {

    @Override
    public void onSuccess(ClientResponse resp, RequestFuture<Void> future) {
        log.debug("Received group coordinator response {}", resp);
        // 创建GroupCoordinatorResponse对象
        GroupCoordinatorResponse= new GroupCoordinatorResponse(resp.responseBody());
        Errors error = Errors.forCode(groupCoordinatorResponse.errorCode());
        clearFindCoordinatorFuture();
        if (error == Errors.NONE) {// 如果没有错误
            synchronized (AbstractCoordinator.this) {
                AbstractCoordinator.this.coordinator = new Node(// 构建一个Node对象赋给coordinator
                        Integer.MAX_VALUE - groupCoordinatorResponse.node().id(),
                        groupCoordinatorResponse.node().host(),
                        groupCoordinatorResponse.node().port());
                log.info("Discovered coordinator {} for group {}.", coordinator, groupId);
                client.tryConnect(coordinator);// 开始尝试和coordinator建立连接
                heartbeat.resetTimeouts(time.milliseconds());
            }
            future.complete(null);// 将正常收到的GroupCoordinatorResponse的事件传播出去
        } else if (error == Errors.GROUP_AUTHORIZATION_FAILED) {// 否则传播异常出去
            future.raise(new GroupAuthorizationException(groupId));
        } else {
            log.debug("Group coordinator lookup for group {} failed: {}", groupId, error.message());
            future.raise(error);
        }
    }

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

 

这个阶段的大致流程如下:


2.2 进入Join Group 阶段

当成功找到GroupCoordinator之后,则进入Join Group阶段,此阶段消费者会向GroupCoordinator发送JoinGroupRequest请求,并处理响应。

我们先看一下JoinGroupRequest和 JoinGroupResponse的消息体格式:


JoinGroupRequest:

group_id: 消费者组的id

session_timeout: GroupCoordinator超过session_time指定时间没有收到心跳,认为消费者下线

member_id: GroupCoordinator分配给消费者的id

protocol_type: 消费者组实现的协议

group_protocols: 包含此消费者全部支持的PartitionAssignor类型

protocol_name:PartitionAssignor名字

protocol_metadata: 针对不同的PartitionAssignor,序列化后的消费者的订阅信息,包含用户自定义数据userData

 

JoinGroupResponse:

error_code: 错误码

generation_id: GroupCoordinator分配给Generation的id

group_protocol: GroupCoordinator选择的PartitionAssignor

leader_id: Leader的member_id

member_id: GroupCoordinator分配给消费者的id

members: 消费者组中所有消费者的订阅信息

member_metadata: 对应消费者的订阅信息

 

JoinGroupRequest大致的请求流程如下图所示:



详细的流程和源码如下:

1 消费者订阅模式是否是自动订阅(AUTO_TOPICS || AUTO_PATTERN),因为USER_ASSIGNED用户指定主题订阅是不需要进行rebalance操作的, 如果满足条件,确保coordinator已经准备好接收请求了

if (subscriptions.partitionsAutoAssigned() && coordinatorUnknown()) {
    // 确保coordinator已经准备好接收请求了
    ensureCoordinatorReady();
    now = time.milliseconds();
}

 

2 判断是否需要重新join

public boolean needRejoin() {
    // 如果不是自动分配partition,不如用户自己指定,则不需要rejoin
    if (!subscriptions.partitionsAutoAssigned())
        return false;

    // 如果我们执行了任务并且元数据改变了,我们需要重新加入
    if (assignmentSnapshot != null && !assignmentSnapshot.equals(metadataSnapshot))
        return true;

    // 如果我们的订阅自上一次join之后改变了,那么我们需要加入
    if (joinedSubscription != null && !joinedSubscription.equals(subscriptions.subscription()))
        return true;
    // 默认需要重新join
    return super.needRejoin();
}

 

3 如果需要重新join,且属于AUTO_PATTERN订阅模式,则需要刷新Metadata; 然后确保协调器可用,心跳线程开启

if (needRejoin()) {
    // 由于初始元数据获取和初始重新平衡之间的竞争条件,我们需要确保元数据在开始加入之前是新鲜的。
    // 确保了我们在join之前至少有一次与集群的主题的模式匹配
    if (subscriptions.hasPatternSubscription())
        // 刷新元数据
        client.ensureFreshMetadata();
    // 确保协调器可用,心跳线程开启和组可用
    ensureActiveGroup();
    now = time.milliseconds();
}

 

public void ensureActiveGroup() {
    // 确保coordinator已经准备好接收请求
    ensureCoordinatorReady();
    // 开启心跳线程
    startHeartbeatThreadIfNeeded();
    // 加入组
    joinGroupIfNeeded();
}

 

4 是否需要做一些Join前的准备工作,如果需要则调用OnJoinPrepare方法

# 如果开启了自动提交,则在rebalance之前进行自动提交offset

# 执行注册在SubscriptionState上的ConsumerRebalanceListener的回调方法

# 准备工作完成之后,把needsJoinPrepare置为fasle

 

if (needsJoinPrepare) {
    // 进行发送JoinGroupRequest之前的准备
    onJoinPrepare(generation.generationId, generation.memberId);
    //  needsJoinPrepare置为false,表示已经准备好了
    needsJoinPrepare = false;
}

 

protected void onJoinPrepare(int generation, String memberId) {
    // 如果开启了自动提交,则在rebalance之前进行自动提交offset
    maybeAutoCommitOffsetsSync();

    // 执行注册在SubscriptionState上的ConsumerRebalanceListener的回调方法
    ConsumerRebalanceListener listener = subscriptions.listener();
    log.info("Revoking previously assigned partitions {} for group {}", subscriptions.assignedPartitions(), groupId);
    try {
        Set<TopicPartition> revoked = new HashSet<>(subscriptions.assignedPartitions());
        listener.onPartitionsRevoked(revoked);
    } catch (WakeupException e) {
        throw e;
    } catch (Exception e) {
        log.error("User provided listener {} for group {} failed on partition revocation",
                listener.getClass().getName(), groupId, e);
    }

    isLeader = false;
    // 重置该组的订阅,只包含该用户订阅的主题。
    subscriptions.resetGroupSubscription();
}

 

5 停止心跳线程,构造JoinGroupRequest,更改客户端状态为rebalance,

并且发送JoinGroupRequest,针对返回的RequestFuture,添加监听器,如果成功,则更改客户端状态为stable,并且开启心跳线程

private synchronized RequestFuture<ByteBuffer> initiateJoinGroup() {
    // 我们存储join future,以防止我们在开始rebalance之后,就被用户唤醒
    // 这确保了我们不会错误地尝试在尚未完成的再平衡完成之前重新加入。
    if (joinFuture == null) {
        // 对心跳线程进行明确的隔离,这样它就不会干扰到连接组。
        // 注意,这个后必须调用onJoinPrepare因为我们必须能够继续发送心跳,如果回调需要一些时间。
        disableHeartbeatThread();
        // 更改状态
        state = MemberState.REBALANCING;
        // 发送JoinGroupRequest,返回RequestFuture对象
        joinFuture = sendJoinGroupRequest();
        // 针对RequestFuture添加监听器
        joinFuture.addListener(new RequestFutureListener<ByteBuffer>() {
            @Override
            public void onSuccess(ByteBuffer value) {
                // 如果成功,则更新状态为消费者客户端已成功加入
                synchronized (AbstractCoordinator.this) {
                    log.info("Successfully joined group {} with generation {}", groupId, generation.generationId);
                    state = MemberState.STABLE;
                    // 然后开始心跳检测
                    if (heartbeatThread != null)
                        heartbeatThread.enable();
                }
            }

            @Override
            public void onFailure(RuntimeException e) {
                synchronized (AbstractCoordinator.this) {
                    state = MemberState.UNJOINED;
                }
            }
        });
    }
    return joinFuture;
}

 

private RequestFuture<ByteBuffer> sendJoinGroupRequest() {
    // 检测coordinator是否可用
    if (coordinatorUnknown())
        return RequestFuture.coordinatorNotAvailable();

    // 创建JoinGroupRequest
    log.info("(Re-)joining group {}", groupId);
    JoinGroupRequest request = new JoinGroupRequest(
            groupId,
            this.sessionTimeoutMs,
            this.rebalanceTimeoutMs,
            this.generation.memberId,
            protocolType(),
        metadata());

    log.debug("Sending JoinGroup ({}) to coordinator {}", request, this.coordinator);
    // 将这个请求放入unsent集合,等待被发送,并返回一个RequestFuture对象
    return client.send(coordinator, ApiKeys.JOIN_GROUP, request)
            .compose(new JoinGroupResponseHandler());
}

 

6 阻塞等待Join Group的完成

client.poll(future);

 

7 重置JoinGroup的RequestFuture

resetJoinGroupFuture();

 

8 如果请求成功,则重置needsJoinPrepare状态,并且调用onJoinComplete做一些扫尾的工作
if (future.succeeded()){
    needsJoinPrepare = true;
    onJoinComplete(generation.generationId, generation.memberId, generation.protocol, future.value());
}

 

protected void onJoinComplete(int generation, String memberId,
        String assignmentStrategy, ByteBuffer assignmentBuffer) {
    // 只有leader才会监控元数据的改变
    if (!isLeader)
        assignmentSnapshot = null;

    // 获取partition分配策略
    PartitionAssignor assignor = lookupAssignor(assignmentStrategy);
    if (assignor == null)
        throw new IllegalStateException("Coordinator selected invalid assignment protocol: " + assignmentStrategy);

    // 获取分配结果
    Assignment= ConsumerProtocol.deserializeAssignment(assignmentBuffer);

    // 设置标记刷新最近一次提交的offset
    subscriptions.needRefreshCommits();

    // 更新分区的分配
    subscriptions.assignFromSubscribed(assignment.partitions());

    // 给一个分区分配策略一个机会更新分配结果
    assignor.onAssignment(assignment);

    // reschedule the auto commit starting from now
    this.nextAutoCommitDeadline = time.milliseconds() + autoCommitIntervalMs;

    // 执行rebalance之后的回调
    ConsumerRebalanceListener listener = subscriptions.listener();
    log.info("Setting newly assigned partitions {} for group {}", subscriptions.assignedPartitions(), groupId);
    try {
        Set<TopicPartition> assigned = new HashSet<>(subscriptions.assignedPartitions());
        listener.onPartitionsAssigned(assigned);
    } catch (WakeupException e) {
        throw e;
    } catch (Exception e) {
        log.error("User provided listener {} for group {} failed on partition assignment",
                listener.getClass().getName(), groupId, e);
    }
}

 

JoinGroupResponse返回的时候,我们会调用JoinGroupResponseHand

Ler#handle()方法

public void handle(JoinGroupResponse joinResponse, RequestFuture<ByteBuffer> future) {
        // 获取响应是否有错误
        Errors error = Errors.forCode(joinResponse.errorCode());
        // 如果没有错误
        if (error == Errors.NONE) {
            log.debug("Received successful join group response for group {}: {}", groupId, joinResponse.toStruct());
            sensors.joinLatency.record(response.requestLatencyMs());

            synchronized (AbstractCoordinator.this) {
                // 如果客户端状态不是REBALANCING
                if (state != MemberState.REBALANCING) {
                    // 把这个异常广播出去
                    future.raise(new UnjoinedGroupException());
                } else {
                    // 解析JoinGroupResponse
                    AbstractCoordinator.this.generation = new Generation(joinResponse.generationId(),
                            joinResponse.memberId(), joinResponse.groupProtocol());
                    // 标记rejoinNeededfalse
                    AbstractCoordinator.this.rejoinNeeded = false;
                    // 判断响应的节点是不是leader,如果是leader则进入onJoinLeader方法
                    // 否则进入onJoinFollower方法
                    if (joinResponse.isLeader()) {
                        onJoinLeader(joinResponse).chain(future);
                    } else {
                        onJoinFollower().chain(future);
                    }
                }
            }
        } else if (error == Errors.GROUP_LOAD_IN_PROGRESS) {
            log.debug("Attempt to join group {} rejected since coordinator {} is loading the group.", groupId,
                    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.", groupId);
            future.raise(Errors.UNKNOWN_MEMBER_ID);
        } else if (error == Errors.GROUP_COORDINATOR_NOT_AVAILABLE
                || error == Errors.NOT_COORDINATOR_FOR_GROUP) {
            // re-discover the coordinator and retry with backoff
            coordinatorDead();
            log.debug("Attempt to join group {} failed due to obsolete coordinator information: {}", groupId, error.message());
            future.raise(error);
        } else if (error == Errors.INCONSISTENT_GROUP_PROTOCOL
                || error == Errors.INVALID_SESSION_TIMEOUT
                || error == Errors.INVALID_GROUP_ID) {
            // log the error and re-throw the exception
            log.error("Attempt to join group {} failed due to fatal error: {}", groupId, error.message());
            future.raise(error);
        } else if (error == Errors.GROUP_AUTHORIZATION_FAILED) {
            future.raise(new GroupAuthorizationException(groupId));
        } else {
            // unexpected error, throw the exception
            future.raise(new KafkaException("Unexpected error in join group response: " + error.message()));
        }
    }
}

 

1 解析JoinGroupResponse,获取memberId,generationId,和protocol等信息

2 判断响应的节点是不是leader,如果是leader则进入onJoinLeader方法,否则进入onJoinFollower方法

onLeaderJoin: 根据JoinGroupResponse的分组信息,然后leader开始进行分区的分配;构造SyncGroupRequest并发送

onFollowerJoin: 不负责分配分区,只是发送同步请求

构造SyncGroupRequest并发送

private RequestFuture<ByteBuffer> onJoinLeader(JoinGroupResponse joinResponse) {
    try {
        // 执行leader同步,并且根据JoinGroupResponse的分组信息,然后leader开始进行分区的分配
        Map<String, ByteBuffer> groupAssignment = performAssignment(joinResponse.leaderId(), joinResponse.groupProtocol(),
                joinResponse.members());
        // 构造SyncGroupRequest并发送
        SyncGroupRequest request = new SyncGroupRequest(groupId, generation.generationId, generation.memberId, groupAssignment);
        log.debug("Sending leader SyncGroup for group {} to coordinator {}: {}", groupId, this.coordinator, request);
        return sendSyncGroupRequest(request);
    } catch (RuntimeException e) {
        return RequestFuture.failure(e);
    }
}

 

protected Map<String, ByteBuffer> performAssignment(String leaderId,
        String assignmentStrategy, Map<String, ByteBuffer> allSubscriptions) {
    // 查找分区分配使用的PartitionAssignor
    PartitionAssignor assignor = lookupAssignor(assignmentStrategy);
    if (assignor == null)
        throw new IllegalStateException("Coordinator selected invalid assignment protocol: " + assignmentStrategy);

    Set<String> allSubscribedTopics = new HashSet<>();
    Map<String, Subscription> subscriptions = new HashMap<>();
    // 根据JoinGroupResponse的分组信息(group_protocols)Subscriptiontopic分类汇总
    for (Map.Entry<String, ByteBuffer> subscriptionEntry : allSubscriptions.entrySet()) {
        Subscription= ConsumerProtocol.deserializeSubscription(subscriptionEntry.getValue());
        subscriptions.put(subscriptionEntry.getKey(), subscription);
        allSubscribedTopics.addAll(subscription.topics());
    }

    // the leader will begin watching for changes to any of the topics the group is interested in,
    // which ensures that all metadata changes will eventually be seen
    // leader会获取所有组内所有消费者的订阅的topic
    this.subscriptions.groupSubscribe(allSubscribedTopics);
    metadata.setTopics(this.subscriptions.groupSubscription());

    // update metadata (if needed) and keep track of the metadata used for assignment so that
    // we can check after rebalance completion whether anything has changed
    // 更新metadata
    client.ensureFreshMetadata();

    isLeader = true;
    // 记录快照
    assignmentSnapshot = metadataSnapshot;

    log.debug("Performing assignment for group {} using strategy {} with subscriptions {}",
            groupId, assignor.name(), subscriptions);
    // 进行分区分配
    Map<String, Assignment> assignment = assignor.assign(metadata.fetch(), subscriptions);

    log.debug("Finished assignment for group {}: {}", groupId, assignment);
    // 分区分配结果序列化并保存到到groupAssignment中,返回分区结果
    Map<String, ByteBuffer> groupAssignment = new HashMap<>();
    for (Map.Entry<String, Assignment> assignmentEntry : assignment.entrySet()) {
        ByteBuffer buffer = ConsumerProtocol.serializeAssignment(assignmentEntry.getValue());
        groupAssignment.put(assignmentEntry.getKey(), buffer);
    }

    return groupAssignment;
}

 

private RequestFuture<ByteBuffer> onJoinFollower() {
    // send follower's sync group with an empty assignment
    SyncGroupRequest request = new SyncGroupRequest(groupId, generation.generationId,
            generation.memberId, Collections.<String, ByteBuffer>emptyMap());
    log.debug("Sending follower SyncGroup for group {} to coordinator {}: {}", groupId, this.coordinator, request);
    return sendSyncGroupRequest(request);
}

 

2.3 开始Synchronizing Group State阶段

完成分区分配后,就进入SynchronizingGroup State阶段,主要就是向GroupCoordinator发送SyncGroupRequest请求并处理SyncGroup

Response响应

我们先看一下SyncGroupRequest和SyncGroupResponse的消息体格式:



SyncGroupRequest:

group_id: 消费者组的id

generation_id: 消费者保存到generation信息

member_id: GroupCoordinator分配给消费者的id

member_assignment: 分区分配结果

 

SyncGroupResponse:

error_code: 错误码

member_assignment:分配给当前消费者的分区

 

在前面onLeaderJoin方法,我们知道在获取分区分配结果之后,Leader将其封装成SyncGroupRequest, 但是我们看到了onFollowerJoin方法里也封装了SyncGroupRequest,但是这里边的分区分配结果是空的

 

然后调用ConsumerNetworkClient#send方法将请求放入unsent请求等待集合,等待被发送

 

对于SyncGroupResponse:解析SyncGroupResponse,并且传播分区分配结果

public void handle(SyncGroupResponse syncResponse,
                   RequestFuture<ByteBuffer> future) {
    Errors error = Errors.forCode(syncResponse.errorCode());
    if (error == Errors.NONE) { // 如果没有错误
        sensors.syncLatency.record(response.requestLatencyMs());
        // 传播分区分配结果
        future.complete(syncResponse.memberAssignment());
    } else {
        requestRejoin();//rejoinNeeded置为true

        if (error == Errors.GROUP_AUTHORIZATION_FAILED) {
            future.raise(new GroupAuthorizationException(groupId));
        } else if (error == Errors.REBALANCE_IN_PROGRESS) {
            log.debug("SyncGroup for group {} failed due to coordinator rebalance", groupId);
            future.raise(error);
        } else if (error == Errors.UNKNOWN_MEMBER_ID
                || error == Errors.ILLEGAL_GENERATION) {
            log.debug("SyncGroup for group {} failed due to {}", groupId, error);
            resetGeneration();
            future.raise(error);
        } else if (error == Errors.GROUP_COORDINATOR_NOT_AVAILABLE
                || error == Errors.NOT_COORDINATOR_FOR_GROUP) {
            log.debug("SyncGroup for group {} failed due to {}", groupId, error);
            coordinatorDead();
            future.raise(error);
        } else {
            future.raise(new KafkaException("Unexpected error from SyncGroup: " + error.message()));
        }
    }
}

 

从SyncGroupResponse得到的分区分配结果最终由ConsumerCoordina

tor#onJoinComplete()方法处理

protected void onJoinComplete(int generation, String memberId,
        String assignmentStrategy, ByteBuffer assignmentBuffer) {
    // 只有leader才会监控元数据的改变
    if (!isLeader)
        assignmentSnapshot = null;

    // 获取partition分配策略
    PartitionAssignor assignor = lookupAssignor(assignmentStrategy);
    if (assignor == null)
        throw new IllegalStateException("Coordinator selected invalid assignment protocol: " + assignmentStrategy);

    // 通过反序列化,获取分配结果
    Assignment= ConsumerProtocol.deserializeAssignment(assignmentBuffer);

    // 设置标记刷新最近一次提交的offset
    subscriptions.needRefreshCommits();

    // 根据分配的分区,更新分区的分配
    subscriptions.assignFromSubscribed(assignment.partitions());

    // 当消费者组的成员接收leader的分区分配的一个回调函数
    assignor.onAssignment(assignment);

    // reschedule the auto commit starting from now
    this.nextAutoCommitDeadline = time.milliseconds() + autoCommitIntervalMs;

    // 执行rebalance之后的回调
    ConsumerRebalanceListener listener = subscriptions.listener();
    log.info("Setting newly assigned partitions {} for group {}", subscriptions.assignedPartitions(), groupId);
    try {
        Set<TopicPartition> assigned = new HashSet<>(subscriptions.assignedPartitions());
        listener.onPartitionsAssigned(assigned);
    } catch (WakeupException e) {
        throw e;
    } catch (Exception e) {
        log.error("User provided listener {} for group {} failed on partition assignment",
                listener.getClass().getName(), groupId, e);
    }
}

 

我们在这里分析的是加入组的情况,其实当消费者离开组的时候,也会触发rebalance操作。


<think>我们有两个问题需要回答:1.KafkaRebalance机制详解2.消费者如何从上次消费的offset继续消费(消费流程和offset恢复)根据用户提供的引用,我们可以整合信息来回答。首先,关于Rebalance机制:引用[1][2][3][4][5]都提到了Rebalance机制,尤其是引用[3]详细列举了触发Rebalance的条件。其次,关于offset恢复:在之前的回答中,我们已经解释了offset存储在`__consumer_offsets`主题中,消费者重启后会从该主题获取上次提交的offset继续消费。而Rebalance过程中,分区的分配会发生变化,但offset的存储与消费者组绑定,因此新的消费者会从该组最后提交的offset开始消费。因此,我们可以将回答分为两部分:第一部分:Rebalance机制详解-定义:Rebalance(再平衡)是Kafka消费者组内部分区重新分配的过程。-触发条件(根据引用[3]):1.消费者加入或离开消费者组2.消费者崩溃或失去连接3.主题的分区数量发生变化4.消费者组协调器变更5.消费者组成员发送心跳失败-过程(需要结合引用[4][5]):-当触发条件发生时,消费者组会进入Rebalance状态。-所有消费者停止消费,等待重新分配分区。-组协调器(GroupCoordinator)负责管理再平衡过程,选举出组领导者(LeaderConsumer),然后由领导者根据分配策略(如RangeAssignor、RoundRobinAssignor等)为所有成员分配分区,并将分配结果发送给协调器,协调器再分发给所有消费者。-消费者收到新的分区分配后,开始从分配到的分区的offset继续消费。-影响(引用[1][2]):-重复消费:在Rebalance期间,消费者停止消费,但可能已经处理了部分消息但未提交offset,重新分配后新消费者可能从之前的offset开始消费,导致重复消费。-延迟增加:Rebalance期间整个消费者组停止工作,导致消费延迟。-优化(引用[1][2]):-避免频繁的Rebalance:增加`session.timeout.ms`(消费者被认为失效的时间)和`heartbeat.interval.ms`(发送心跳的时间间隔)的配置,让消费者有更多时间发送心跳。-减少Rebalance的影响:使用增量RebalanceKafka2.4+支持,即CooperativeStickyAssignor),允许消费者Rebalance期间继续消费未受影响的分区。-确保快速完成Rebalance:减少一次性处理的消息数量(减少`max.poll.records`),避免消费者处理消息时间过长导致心跳超时。第二部分:消费者从上次消费的offset继续消费的流程-在正常情况下(无Rebalance),消费者通过提交offset到`__consumer_offsets`主题来记录消费位置。当消费者重启时,它会从该主题读取该消费者组在该分区的最后提交的offset,然后从这个offset开始消费。-在Rebalance过程中,分区被重新分配给消费者,新的消费者会从该消费者组在该分区最后提交的offset开始消费(即offset恢复)。注意,这里的offset是消费者组级别的,所以即使消费者换了,只要在同一个组,就会从同一个offset继续。因此,整个消费流程包括:1.消费者启动,加入消费者组(可能触发Rebalance)。2.消费者被分配分区(可能是Rebalance的结果)。3.消费者从分配到的分区的最后提交的offset开始拉取消息。4.消费者处理消息,并定期提交offset(自动或手动)。5.如果消费者故障退出,触发Rebalance,其他消费者(或重启后的该消费者)重新分配分区,并重复步骤3。注意:在Rebalance期间,如果消费者处理了消息但还没来得及提交offset,那么这些消息可能会被重新消费(因为offset没有提交,新消费者会从旧的已提交offset开始)。因此,建议在消息处理完成后立即手动提交offset,以减少重复消费。根据以上分析,我们可以组织回答。</think>### Kafka Rebalance 机制详解与消费者 Offset 恢复流程 #### 一、Rebalance(再平衡)机制详解 RebalanceKafka 消费者组实现负载均衡的核心机制,当消费者组状态变化时,会触发分区重新分配。以下是关键要点: 1. **触发条件**(引用[3]) - ✅ 消费者加入/离开组(如新增 B2 消费者) - ✅ 消费者崩溃或失联(如 A1 宕机) - ✅ 主题分区数变更(如 topic 分区从 3→5) - ✅ 协调器(Coordinator)变更 - ✅ 心跳超时(默认 10 秒未发送心跳) ```mermaid graph LR A[消费者A1宕机] --> B[协调器检测心跳超时] B --> C[标记消费者失效] C --> D[触发Rebalance] ``` 2. **执行流程**(引用[2][4]) - **阶段1**:所有消费者停止消费,向协调器发送 JoinGroup 请求 - **阶段2**:选举 Leader 消费者,执行分区分配策略(Range/RoundRobin) - **阶段3**:协调器同步分配方案(SyncGroup) - **阶段4**:消费者获取新分配的分区并恢复消费 3. **性能问题与优化**(引用[1]) | 问题 | 原因 | 解决方案 | |---|---|---| | 重复消费 | Rebalance 期间 offset 未提交 | 减小 `max.poll.interval.ms` | | 消费延迟 | 全组暂停消费 | 使用增量 RebalanceKafka 2.4+) | | 频繁触发 | 心跳超时敏感 | 调整 `session.timeout.ms=45s` | #### 二、消费者 Offset 恢复机制消费者 A1 故障恢复后,其消费流程如下: 1. **Offset 存储原理** - Offset 存储在 Kafka 内部主题 `__consumer_offsets` - 提交方式: - **自动提交**:`enable.auto.commit=true`(默认每 5 秒) - **手动提交**:`consumer.commitSync()`(更可靠) 2. **恢复流程**(以 A1 重启为例) ```mermaid sequenceDiagram A1->>Coordinator: 发送 JoinGroup 请求 Coordinator->>A1: 分配分区 P0 A1->>__consumer_offsets: 查询最后提交的 offset __consumer_offsets-->>A1: 返回 offset=102 A1->>Broker: 从 offset=103 开始拉取消息 ``` 3. **关键配置** ```java // 消费者配置示例 props.put("group.id", "groupA"); props.put("auto.offset.reset", "latest"); // 无offset时策略 props.put("enable.auto.commit", "false"); // 推荐手动提交 ``` 4. **异常处理** - 当 offset 不存在时(如首次启动): - `earliest`:从分区首条消息开始 - `latest`:从最新消息开始(可能丢数据) - 重复消费防护:在业务逻辑中实现幂等性处理 #### 三、完整消费流程与 Rebalance 关联 1. **正常消费流程** `拉取消息 → 业务处理 → 提交offset → 周期性心跳` 2. **故障场景联动** - A1 宕机 → 心跳超时 → 触发 Rebalance - B1 接管 A1 的分区 → 从 __consumer_offsets 读取 A 组 offset - A1 恢复后 → 新 Rebalance → A1 重新分配分区并恢复 offset > **最佳实践**:使用手动提交 offset + 合理心跳配置,可减少 70% 的异常 Rebalance(引用[1][4])
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫言静好、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值