消费组的订阅状态(SubscriptionState)

SubscriptionState

新版API(KafkaConsumer)为消费者客户端提供了两种消费方式:订阅(subscribe)和分配(assgin)

  1. 订阅模式:消费者会指定订阅的topic,由GroupCoordinator为消费者分配动态的分区。(实际上分区分配的具体分配还是由客户端决定的,GroupCoordinator并不实际参与)
  2. 分配模式:消费者指定消费特定的分区,这种模式失去了GroupCoordinator为消费者动态分配分区的功能。

subscribe方法的参数是topic名称的集合,assgin方法的参数是TopicPartition实例的集合。这两个方法都会更新消费者订阅状态(SubscriptionState)对象SubscriptionState保存了分配给消费者的所有分区及其状态(assignment分配结果)。分配结果(SubscriptionState. assignment)保存了分配给消费者的分区到分区状态映射关系。

分配模式一开始就确定了分区,而订阅模式需要通过消费组协调之后,才会知道自己分配到哪些分区。

消费者要拉取消息,必须确保分配到分区;有了分区后,后续的拉取消息都一样。


TopicPartitionState

新版API使用TopicPartitionState对象表示分区状态。分区对象作为订阅状态(SubscriptionState)的内部类。

private static class TopicPartitionState {
    private Long position; // 拉取偏移量
    private Long highWatermark; // the high watermark from last fetch
    private OffsetAndMetadata committed;  // 消费偏移量
    private boolean paused;  // 消费是否被暂停
    private OffsetResetStrategy resetStrategy;  // 重置策略
    //...
}

拉取状态

消费状态

Long position

OffsetAndMetadata committed

新创建的分区状态对象时,这两个偏移量初始时都为空,表示刚创建时消费者还没有开始“拉取”这个分区,并且还没有为这个分区“提交”过偏移量。但实际上,分区最新的“拉取偏移量”和“提交的偏移量”可能是有状态的。比如分配给消费者1的分区P0,它的拉取偏移量是100,消费偏移量是90。当P0分配给消费者2时,消费者2获取P0的状态应该是有数据的。所以消费者2在开始工作之前,应该先初始化分区的状态。
消费者加入消费组的过程中,当执行完成第三阶段(sync_group)之后会调用如下构造方法进行初始化

public TopicPartitionState() {
    this.paused = false;
    this.position = null;
    this.highWatermark = null;
    this.committed = null;
    this.resetStrategy = null;
}

消费者拉取消息的过程中,更新拉取状态是为了拉取新数据,更新消费状态是为了提交到GroupCoordinator节点。
下图为分区状态更新偏移量的相关方法

分区第一次分配给消费者时,没有提交偏移量,使用默认的重置策略(auto.offset.reset参数设置的值)进行重置。会调用TopicPartitionState#awaitReset方法重置拉取偏移量(在org.apache.kafka.clients.consumer.internals.Fetcher#updateFetchPositions中)

private void awaitReset(OffsetResetStrategy strategy) {
    this.resetStrategy = strategy;
    this.position = null;
}

然后使用TopicPartitionState#seek方法重置拉取偏移量,此方法对应第一次从GroupCoordinator节点读取拉取状态。

private void seek(long offset) {
    this.position = offset;
    this.resetStrategy = null;
}

分区状态的拉取偏移量(position变量)表示分区的拉取进度,它的值不为空,消费者才可以拉取这分区。

消费者的订阅状态表示分配给消费者所有分区的状态,每个分区必须指定拉取偏移量,才可以被消费者拉取。消费者在拉取消息之前,会先判断所有的分区是否都有拉取偏移量(对应SubscriptionState.hasAllFetchPositions()方法),如果没有,则找出相应的分区(对应SubscriptionState.missingFetchPositions()方法),然后调用KafkaConsumer.updateFetchPositions()方法更新这些分区。消费者在创建拉取请求时,只会选择允许拉取的分区集合(对应SubscriptionState.fetchablePartitions()方法),不允许拉取的分区就不会拉取。

消费者KafkaConsumer每一次轮询时,都判断是否需要做准备工作,通过KafkaConsumer. pollOnce()方法。
消费者拉取消息的流程

(1)客户端订阅topic后,通过KafkaConsumer轮询,准备拉取消息
(2)如果所有的分区都有拉取偏移量,进入步骤(6),否则进入步骤(3)
(3)从订阅状态的分配结果中找出所有没有拉取偏移量的分区
(4)通过KafkaConsumer.updateFetchPositions()更新步骤(3)中分区的拉取偏移量
(5)不管是从步骤(2)直接进来还是步骤(4)更新过的分区,现在都允许消费者拉取
(6)对所有存在拉取偏移量并且允许来取的分区,构建拉取请求开始拉取消息

消费者第一次轮询不满足SubscriptionState.hasAllFetchPositions()方法,执行(1),(3),(4),(5)和(6),即图中虚线路径。比如消费者分配了[p0,p1,p2]这3个分区,消费者第一次轮询时分区p0有拉取偏移量,而分区[p1,p2]都没有拉取偏移量。步骤(4)会更新[p1,p2]两个分区的偏移量,最后到步骤(6)时,三个分区都有拉取偏移量。第二次轮询时,所有分区都有拉取偏移量了,所以不需要更新分区,只需要执行步骤(1),(2)和(6),即图中的实线路径。


重置和更新拉取偏移量

调用KafkaConsumer.updateFetchPositions()方法有下面两个步骤:
(1)通过“消费者的协调者”(ConsumerCoordinator)更新分区状态的提交偏移量
(2)通过“拉取器”(Fetcher)更新分区状态的拉取偏移量

private void updateFetchPositions(Set<TopicPartition> partitions) {
    fetcher.resetOffsetsIfNeeded(partitions);
    if (!subscriptions.hasAllFetchPositions(partitions)) {
        //更新分区状态中的已提交偏移量
        coordinator.refreshCommittedOffsetsIfNeeded();
        //更新分区分区状态中的拉取偏移量
        fetcher.updateFetchPositions(partitions);
    }
}

拉取偏移量”用于在发送拉取请求时指定从分区的哪里开始拉取消息,“提交偏移量”表示消费者处理分区的进度。

消费者拉取消息时要更新拉取偏移量,处理消息时需要更新提交偏移量。通常“提交偏移量”会赋值给“拉取偏移量”,尤其是发生再平衡时,分区分配给新的消费者。新消费者之前在本地没有记录这个分区的消费进度,它要获取“拉取偏移量”,需要从协调者获取这个分区的“提交偏移量”,把“提交偏移量”作为分区的起始“拉取偏移量”。

消费者每次轮询时,对于没有拉取偏移量的分区也采用类似的方式,通过消费者的协调者对象发送“获取偏移量”请求(OFFSET_FETCH)给服务端的协调者节点。“获取偏移量”请求返回的结果表示这个分区在协调者节点已经记录的“提交偏移量”。服务端记录的这个偏移量可能是同一个消费组其他消费者提交的。

消费者接收到“获取偏移量”的请求结果,会通过SubscriptionState.committed()方法更新分区状态的“已提交偏移量”

public void refreshCommittedOffsetsIfNeeded() {
    if (subscriptions.refreshCommitsNeeded()) {
        //发送OFFSET_FETCH请求给协调者,获取分区已经提交的偏移量
        Map<TopicPartition, OffsetAndMetadata> offsets = fetchCommittedOffsets(subscriptions.assignedPartitions());
        for (Map.Entry<TopicPartition, OffsetAndMetadata> entry : offsets.entrySet()) {
            TopicPartition tp = entry.getKey();
            if (subscriptions.isAssigned(tp))
               //更新分区状态的committed变量,协调节点保存的数据更新到客户端
               this.subscriptions.committed(tp, entry.getValue());
        }
        this.subscriptions.commitsRefreshed();
    }
}

然后更新“拉取偏移量”的时候,因为分区已经有“已提交偏移量”,可以调用seek()方法,用这个“以提交偏移量”作为分区状态的“拉取偏移量”。

public void updateFetchPositions(Set<TopicPartition> partitions) {
    for (TopicPartition tp : partitions) {
        if (!subscriptions.isAssigned(tp) || subscriptions.hasValidPosition(tp))
            continue;

        //重置拉取偏移量到已经提交过的位置
        if (subscriptions.isOffsetResetNeeded(tp)) { //需要重置
            resetOffset(tp);
        } else if (subscriptions.committed(tp) == null) { //已提交的偏移量为空
            // there's no committed position, so we need to reset with the default strategy
            subscriptions.needOffsetReset(tp);
            resetOffset(tp);
        } else { //分区状态中已经提交的偏移量不为空,直接使用它作为拉取偏移量
            long committed = subscriptions.committed(tp).offset();
            log.debug("Resetting offset for partition {} to the committed offset {}", tp, committed);
            subscriptions.seek(tp, committed);
        }
    }
}

但是消费者发送“获取偏移量”的请求有可能返回空值,说明协调者节点并没有记录这个分区的“已提交偏移量”。那么分区状态的“已提交偏移量”也为空,就不能把为空的“已提交偏移量”赋值给“拉取偏移量”。这时候需要根据消费者客户端设置的重置策略,向分区的leader节点发送“列举偏移量”请求(LIST_OFFSETS)获取分区的偏移量。

private void resetOffset(TopicPartition partition) {
    OffsetResetStrategy strategy = subscriptions.resetStrategy(partition);
    log.debug("Resetting offset for partition {} to {} offset.", partition, strategy.name().toLowerCase(Locale.ROOT));
    final long timestamp;
    if (strategy == OffsetResetStrategy.EARLIEST)
        timestamp = ListOffsetRequest.EARLIEST_TIMESTAMP;
    else if (strategy == OffsetResetStrategy.LATEST)
        timestamp = ListOffsetRequest.LATEST_TIMESTAMP;
    else
        throw new NoOffsetForPartitionException(partition);
    
    Map<TopicPartition, OffsetData> offsetsByTimes = retrieveOffsetsByTimes(
            Collections.singletonMap(partition, timestamp), Long.MAX_VALUE, false);
    OffsetData offsetData = offsetsByTimes.get(partition);
    if (offsetData == null)
        throw new NoOffsetForPartitionException(partition);
    long offset = offsetData.offset;
    // we might lose the assignment while fetching the offset, so check it is still active
    if (subscriptions.isAssigned(partition))
        this.subscriptions.seek(partition, offset);
}

OFFSET_FETCH请求是通过消费者的coordinator发送给管理消费组的服务端协调节点,LIST_OFFSETS请求则是由拉取器发动给分区leader节点。协调者节点和leader是不同的服务端节点。协调者节点保存了消费组的相关数据,即分区的提交偏移量;而分区的leader节点只保存了分区的日志文件,和偏移量相关的也就只有日志文件中的消息偏移量。

当调用过一次seek()方法之后,分区有了拉取偏移量。后续在消费者轮询时,就不要在在通过上面的流程来更新分区的拉取偏移量了,而是在拉取到消息后在拉取器中进行更新。

并不是每次轮询都会调用KafkaConsumer.updateFetchPositions()方法,只有那些没有拉取偏移量的分区才会需要更新拉取偏移量。消费者刚分配到分区,创建新的分区状态对象时,分区状态还没拉取便宜量。第一次轮询时会调用KafkaConsumer.updateFetchPositions()更新所有分区状态的拉取偏移量,后续的轮询因为所有的分区都有拉取偏移量了,就不在需要调用KafkaConsumer.updateFetchPositions()。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
订阅RocketMQ多个消费的死信队列,您需要进行以下步骤: 1. 在Spring Boot应用程序中添加RocketMQ依赖项,例如: ```xml <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> ``` 2. 在应用程序的配置文件中添加RocketMQ相关的配置,例如: ```yaml rocketmq: name-server: localhost:9876 producer: group: my-group consumer: group: my-consumer-group dead-letter-queue: enabled: true topic: my-dlq-topic ``` 3. 在应用程序中创建多个RocketMQ消费者,并将其分配给不同的消费。例如: ```java @Service @RocketMQMessageListener(topic = "my-topic", consumerGroup = "my-group1") public class MyConsumer1 implements RocketMQListener<String> { @Override public void onMessage(String message) { // TODO: 处理消息 } } @Service @RocketMQMessageListener(topic = "my-topic", consumerGroup = "my-group2") public class MyConsumer2 implements RocketMQListener<String> { @Override public void onMessage(String message) { // TODO: 处理消息 } } ``` 4. 创建一个死信队列消费者,并将其分配给一个消费。例如: ```java @Service @RocketMQMessageListener(topic = "my-dlq-topic", consumerGroup = "my-dlq-consumer-group") public class MyDLQConsumer implements RocketMQListener<String> { @Override public void onMessage(String message) { // TODO: 处理死信消息 } } ``` 通过以上步骤,您就可以订阅RocketMQ多个消费的死信队列了。在配置文件中,您需要将死信队列功能启用,并设置死信队列的主题。然后在消费者中,您需要将它们分配给不同的消费。最后,您需要创建一个特定的消费者来处理死信队列中的消息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值