consumer消费

consumer本地会记录两种offset:
• 消费拉取的offset, 初始化和rebalance时会fetchInitialOffset从broker获取上次消费的offset然后从指定的offset拉取消息。接下来Consumer会不断通过fetchNewMessages() 到broker从指定offset拉取消息。
• 消费提交的offset,消费后自动/手动提交已经消费的offset
图片.png

讲下提交步骤:
1.本地先标记offset, MarkOffset:

  • 注意此时只是本地标记已经消费的, 并没有真正提交到broker

2.再执行CommitOffset提交到broker,而CommitOffset有下面两种方式:

  • 自动提交(默认都是这种),Consumer.Offsets.AutoCommit.Enable(默认打开)

    两种情况触发CommitOffset自动提交:
    1.定时提交,与Consumer.Offsets.AutoCommit.Interval相关
    2.停止consumer sessions中断消费时退出的时候会触发提交
  • 手动提交(不建议)

_consumer_offsets
__consumer_offsets这个topic里面存储的是consumer offsets信息,清理策略log.cleanup.policy默认是compact,简单的说就是压缩相同key, 保留最后一个。其他topic log默认log.cleanup.policy默认是delete。 log.retention参数针对的是delete的清理策略,对compact不生效。compact是压缩后会清理掉垃圾文件主要和log.cleaner配置有关。可阅读:
Kafka 2.2.0 消息日志清理机制:日志删除 日志压缩

演示

kafka broker版本:v2.0.0, sarama (go sdk)版本 v1.26.1
修改配置:
• log.retention.minutes: 5 (默认log.retention.hours=168)

日志保留时间

• log.retention.check.interval.ms: 1000 (默认600000ms,10分钟)

日志过期检查频率

• offsets.retention.minutes: 1 (以前默认24小时,2.0版本后默认10080, 7天)

消费者的offset保留时间

• offsets.retention.check.interval.ms: 1000 (默认300000ms, 5分钟)

消费者的过期offset检查频率

• 消费者初始化消费设置为OffsetOldest

消费者过期后从最老的log offset消费

consumer offset 过期时间

1.打开生产者和消费者
图片.png

图片.png
其中offset10是上一轮生产的

2.读取__consumer_offsets消息查看消费者offset的情况

./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic __consumer_offsets --formatter 'kafka.coordinator.group.GroupMetadataManager$OffsetsMessageFormatter'|grep -v console-consumer-

其中ExpirationTime-CommitTime=1分钟,说明保留时间是一分钟了
图片.png

3.关闭生产者,一分钟(与offsets.retention.minutes和offsets.retention.check.interval.ms有关) 后__consumer_offsets里面[my-group,sync_time_test3,0] 接收到了null消息,代表consumer的offsets过期了。
图片.png

4.重启生产者
consumer继续上次消费,此时如果不重启消费者是没事的。此时虽然broker里面已经没有了consumer消费的offset,但是consumer本地记住了上次fetch的offset, 会继续拉取下一条消息。只有初始化和rebalance时会fetchInitialOffset从broker获取上次消费的offset然后从指定的offset拉取消息。

图片.png

图片.png

5.再次关闭生产者,等待consumer offset过期
图片.png

6.马上重启consumer会重新消费

图片.png

重新消费了,可以和第4步对比消费时间

consumer offset 过期后onsumer为什么还在?

1.先打开生产者消费者
图片.png

图片.png

2.关闭生产者
consumer过期了,但是查看consumer还有:
图片.png

图片.png
关闭消费者,consumer消失:
图片.png
但是里面内容没了,内容可以使用下面命令查看:

./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group my-group --describe
# 此命令在kafka v2.0.0会报错 https://github.com/apache/kafka/pull/4980

3.分析应该是因为心跳保持consumer一直在

Consumer.Group.Session.Timeout用于检测worker程序失败的超时。worker定期发送心跳,以向代理表明其活性。如果在此会话超时过期之前代理没有接收到心跳,则代理将从组中删除。请注意,该值必须位于broker配置中配置group.min.session.timeout.msgroup.max.session.timeout.ms之间。

我们先设置成30s:
图片.png
然后设置sleep长时间不让heartbeat:
图片.png

4.只打开消费者
my-group 三十秒后因为没有心跳被剔除
图片.png

log过期时间

查看topic命令
./kafka-run-class.sh kafka.tools.GetOffsetShell --topic sync_time_test3 --broker-list localhost:9092 --time -1 
# 查看topic的offset(不是消费者的offset),最后的参数-1表示显示获取当前offset最大值,-2表示offset的最小值

1.先生产消息,产生几条后关闭生产
查看topic offset
图片.png

产生消息后,查看topic offset
图片.png

图片.png

2.打开消费者,消费后关闭
图片.png

3.隔一分多钟重新打开消费者,此时会重新消费
图片.png

4.隔5分多钟后log的offset最大值和最小值一致, 其余的已经过期,只会保留下一次的offset
图片.png

5.此时再打开消费者也没有消息了
图片.png

consumer commit code

下面是部分关于CommitOffset的代码:

// Consume implements ConsumerGroup.
func (c *consumerGroup) Consume(ctx context.Context, topics []string, handler ConsumerGroupHandler) error {
    // Ensure group is not closed
    select {
    case <-c.closed:
        return ErrClosedConsumerGroup
    default:
    }
    ......
    // Refresh metadata for requested topics
    if err := c.client.RefreshMetadata(topics...); err != nil {
        return err
    }

    // Init session 注意这里面进入后会一直消费等待退出
    sess, err := c.newSession(ctx, topics, handler, c.config.Consumer.Group.Rebalance.Retry.Max)
    if err == ErrClosedClient {
        return ErrClosedConsumerGroup
    } else if err != nil {
        return err
    }
    ...
    // Gracefully release session claims  里面会执行offsets.Close(), 会flushToBroker  
    return sess.release(true)
}

//sess.release(true)--> offsets.Close():
func (om *offsetManager) Close() error {
    om.closeOnce.Do(func() {
        // exit the mainLoop
        close(om.closing)
        if om.conf.Consumer.Offsets.AutoCommit.Enable {
            <-om.closed
        }

        // mark all POMs as closed
        om.asyncClosePOMs()

        // flush one last time,最后一次刷入
        if om.conf.Consumer.Offsets.AutoCommit.Enable {
            for attempt := 0; attempt <= om.conf.Consumer.Offsets.Retry.Max; attempt++ {
                om.flushToBroker()
                if om.releasePOMs(false) == 0 {
                    break
                }
            }
        }
....
    })
    return nil
}

//提交offset到broker
func (om *offsetManager) Commit() {
    om.flushToBroker()
    om.releasePOMs(false)
}

func (om *offsetManager) flushToBroker() {
    req := om.constructRequest() //这里面会检查是否有新的offset需要提交
    ......
    resp, err := broker.CommitOffset(req) //这里就是提交了
    ......
    om.handleResponse(broker, req, resp)
}


func (om *offsetManager) constructRequest() *OffsetCommitRequest {
    var r *OffsetCommitRequest
    var perPartitionTimestamp int64
    
    //这个是是否手动设置offset保留时间,如果为0,以broker的保留时间为准
    if om.conf.Consumer.Offsets.Retention == 0 {
        perPartitionTimestamp = ReceiveTime
        r = &OffsetCommitRequest{
            Version:                 1,
            ConsumerGroup:           om.group,
            ConsumerID:              om.memberID,
            ConsumerGroupGeneration: om.generation,
        }
    } else {
        r = &OffsetCommitRequest{
            Version:                 2,
            RetentionTime:           int64(om.conf.Consumer.Offsets.Retention / time.Millisecond),
            ConsumerGroup:           om.group,
            ConsumerID:              om.memberID,
            ConsumerGroupGeneration: om.generation,
        }
    }

    for _, topicManagers := range om.poms {
        for _, pom := range topicManagers {
            pom.lock.Lock()
            if pom.dirty { //这里的dirty就是判断数据是否有更新,是否有新的offset需要提交
                r.AddBlock(pom.topic, pom.partition, pom.offset, perPartitionTimestamp, pom.metadata)
            }
            pom.lock.Unlock()
        }
    }

    if len(r.blocks) > 0 {
        return r
    }

    return nil
}

总结:

  • topic日志的保留与log.retention.check.interval.ms和log.retention.check.interval.ms有关(可局部针对topic配置),即使log清理后也会保留最新offset信息,超过保留时间后下次生产也会继续从上次开始。但是topic: __consumer_offsets默认是compact保留策略和log.retention无关。
  • 消费者信息的保留在__consumer_offsets。 与offsets.retention.minutes和offsets.retention.check.interval.ms有关(不可针对指定topic进行consumer配置,是全局设置), 如果非要单独改可以更改consumer客户端的参数Consumer.Offsets.Retention, 如设置为2分钟:
    图片.png
    此时__consumer_offsets中指定consumer的ExpirationTime-CommitTime=2分钟:
    图片.png
  • 推荐设置消费者的过期时间offsets.retention > log保留时间log.retention

推荐阅读:

【kafka原理】 消费者偏移量__consumer_offsets_相关解析
Kafka 中的消费者位移 __consumer_offsets
Kafka 2.2.0 消息日志清理机制:日志删除 日志压缩
https://kafka.apache.org/20/documentation.html


AVOli
6 声望0 粉丝