Kafka Consumer偏移量提交检查以避免提交较小的偏移量

时间:2020-10-04 06:42:16

标签: apache-kafka kafka-consumer-api

我们假设我们有一个使用者发送请求以提交偏移量10。 如果存在通信问题,并且经纪人没有收到请求,当然也没有响应。此后,我们有另一个使用者流程另一个批次并成功提交了偏移量20。

问:我想知道是否有一种方法或属性可以处理,因此我们可以在提交偏移量20之前检查是否提交了日志中的上一个偏移量? / p>

1 个答案:

答案 0 :(得分:1)

您所描述的场景只能在使用异步提交时发生。

请记住,一个特定的TopicPartition只能由同一ConsumerGroup中的单个使用者使用。如果您有两个使用者正在读取相同的TopicPartition,则只能这样做

  1. 如果他们有不同的ConsumerGroup,或者
  2. 如果他们具有相同的ConsumerGroup并发生重新平衡。但是,仍然只有一个使用者一次会读取该TopicPartition,而不会同时读取。

案例1很清楚:如果它们具有不同的ConsumerGroup,它们将并行且独立地使用分区。另外,它们的提交偏移量是分开管理的。

情况2:如果第一个使用者由于失败/死亡而未能恢复使用者,而未能提交偏移量10,则将发生重新平衡,而另一个活动的使用者将选择该分区。由于未提交偏移量10,因此新使用者将再次读取偏移量10,然后跳至下一个批次并可能会提交偏移量20。这将导致“至少一次”语义,并可能导致重复。

现在,进入唯一的情况,您可以在提交较高的偏移量之后提交较小的偏移量。如开头所述,如果您异步提交偏移量(使用commitAsync),确实会发生这种情况。想象以下场景,按时间排序:

  • 消费者读取偏移量0(后台线程尝试提交偏移量0)
  • 提交偏移量0成功
  • 消费者读取偏移量1(后台线程尝试提交偏移量1)
  • 提交偏移量1失败,请稍后再试
  • 消费者读取偏移量2(后台线程尝试提交偏移量2)
  • 提交偏移量2成功
  • 现在,要做什么(重试提交偏移量1?)

如果让重试机制再次提交偏移量1,则看来您的使用者只提交了直到偏移量1。这是因为有关最新偏移量的TopicPartition上每个使用者组的信息都存储在内部< em> compacted Kafka主题__consumer_offsets,旨在为我们的消费者组仅存储最新值(在我们的示例中为offset 1)。

在“ Kafka-权威指南”一书中,有关于如何缓解此问题的提示:

重试异步提交:获得异步重试正确的提交顺序的一种简单模式是使用单调递增的序列号。每次提交时都增加序列号,并在提交时将序列号添加到commitAsync回调中。当您准备发送重试时,请检查回调获得的提交序列号是否与实例变量相等;如果是,则没有较新的提交,可以重试。如果实例序列号更高,请不要重试,因为已经发送了新的提交。

作为示例,您可以在下面的Scala中看到此想法的实现:

import java.util._
import java.time.Duration
import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord, KafkaConsumer, OffsetAndMetadata, OffsetCommitCallback}
import org.apache.kafka.common.{KafkaException, TopicPartition}
import collection.JavaConverters._

object AsyncCommitWithCallback extends App {

  // define topic
  val topic = "myOutputTopic"

  // set properties
  val props = new Properties()
  props.put(ConsumerConfig.GROUP_ID_CONFIG, "AsyncCommitter5")
  props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092")
  // [set more properties...]
  

  // create KafkaConsumer and subscribe
  val consumer = new KafkaConsumer[String, String](props)
  consumer.subscribe(List(topic).asJavaCollection)

  // initialize global counter
  val atomicLong = new AtomicLong(0)

  // consume message
  try {
    while(true) {
      val records = consumer.poll(Duration.ofMillis(1)).asScala

      if(records.nonEmpty) {
        for (data <- records) {
          // do something with the records
        }
        consumer.commitAsync(new KeepOrderAsyncCommit)
      }

    }
  } catch {
    case ex: KafkaException => ex.printStackTrace()
  } finally {
    consumer.commitSync()
    consumer.close()
  }


  class KeepOrderAsyncCommit extends OffsetCommitCallback {
    // keeping position of this callback instance
    val position = atomicLong.incrementAndGet()

    override def onComplete(offsets: util.Map[TopicPartition, OffsetAndMetadata], exception: Exception): Unit = {
      // retrying only if no other commit incremented the global counter
      if(exception != null){
        if(position == atomicLong.get) {
          consumer.commitAsync(this)
        }
      }
    }
  }

}