簡體   English   中英

Kafka 消費者偏移量提交檢查以避免提交較小的偏移量

[英]Kafka Consumer offset commit check to avoid committing smaller offsets

我們假設我們有一個消費者發送請求以提交偏移量 10。如果存在通信問題並且代理沒有收到請求,當然也沒有響應。 之后,我們讓另一個消費者處理另一批並成功提交偏移量 20。

問:我想知道是否有一種方法或屬性可以處理,以便我們可以在我們提交偏移量 20 之前檢查日志中的前一個偏移量是否已提交?

您所描述的場景只能在使用異步提交時發生。

請記住,一個特定的 TopicPartition 只能由同一 ConsumerGroup 中的單個消費者使用。 如果您有兩個消費者閱讀相同的 TopicPartition,則只有可能

  1. 如果他們有不同的消費者組,或者
  2. 如果它們具有相同的 ConsumerGroup 並且發生重新平衡。 但是,一次只有一個使用者會讀取該 TopicPartition,而不是同時讀取。

案例#1 非常清楚:如果他們有不同的 ConsumerGroups,他們會並行且獨立地使用分區。 此外,它們的承諾偏移量是單獨管理的。

案例#2:如果第一個消費者由於消費者失敗/死亡並且沒有恢復消費者而未能提交偏移量 10,則將發生消費者重新平衡,另一個活動消費者將拿起該分區。 由於未提交偏移量 10,新使用者將在跳轉到下一批之前再次開始讀取偏移量 10,並可能提交偏移量 20。這會導致“至少一次”語義並可能導致重復。

現在,來到唯一一種情況,您可以在提交更高的偏移量后提交更小的偏移量。 正如開頭所說,如果您異步提交偏移量(使用commitAsync ),這確實可能發生。 想象以下場景,按時間排序:

  • 消費者讀取偏移量 0(后台線程嘗試提交偏移量 0)
  • 提交偏移量 0 成功
  • 消費者讀取偏移量 1(后台線程嘗試提交偏移量 1)
  • 提交偏移量 1 失敗,請稍后再試
  • 消費者讀取偏移量 2(后台線程嘗試提交偏移量 2)
  • 提交偏移量 2 成功
  • 現在,該怎么辦(重新嘗試提交偏移量 1?)

如果你讓重試機制再次提交偏移量 1,看起來你的消費者只提交到偏移量 1。這是因為每個消費者組的最新偏移量參數 TopicPartition 的信息存儲在內部壓縮的Kafka 主題 __consumer_offsets這意味着只為我們的消費者組存儲最新的值(在我們的例子中:偏移量 1)。

在“Kafka - The Definitive Guide”一書中,有一個關於如何緩解這個問題的提示:

重試異步提交:為異步重試獲取正確提交順序的一個簡單模式是使用單調遞增的序列號。 每次提交時增加序列號,並將提交時的序列號添加到 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)
        }
      }
    }
  }

}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM