KafkaConsumer:一次性使用所有可用消息,然后退出

时间:2018-09-25 14:26:37

标签: apache-kafka

我想用Kafka 2.0.0创建一个KafkaConsumer,使用一次所有可用消息,然后立即退出。这与标准控制台使用者实用程序稍有不同,因为该实用程序waits for a specified timeout用于发送新消息,并且仅在该超时到期后才退出。

使用KafkaConsumer似乎很难完成这项任务。我的直觉反应是以下伪代码:

consumer.assign(all partitions)
consumer.seekToBeginning(all partitions)
do
  result = consumer.poll(Duration.ofMillis(0))
  // onResult(result)
while result is not empty

但是这不起作用,因为poll总是返回一个空集合,即使该主题上有很多消息。

对此进行了研究,似乎一个原因可能是分配/预订为considered lazy,并且直到poll循环完成才分配分区(尽管我找不到对此断言的任何支持)在文档中)。但是,以下伪代码在每次调用poll时都返回一个空集合:

consumer.assign(all partitions)
consumer.seekToBeginning(all partitions)
// returns nothing
result = consumer.poll(Duration.ofMillis(0))
// returns nothing
result = consumer.poll(Duration.ofMillis(0))
// returns nothing
result = consumer.poll(Duration.ofMillis(0))
// deprecated poll also returns nothing
result = consumer.poll(0)
// returns nothing
result = consumer.poll(0)
// returns nothing
result = consumer.poll(0)
...

显然,“懒惰”不是问题。

javadoc指出:

  

如果有可用记录,此方法将立即返回。

似乎暗示上面的第一个伪代码应该起作用。但是,事实并非如此。

唯一可行的方法是在poll上指定一个非零的超时,而不仅仅是任何一个非零的值,例如1都不起作用。这表明poll内发生了某些不确定性行为,该行为假设poll总是在无限循环中执行,尽管有可用性,偶尔也返回空集合并不重要消息。遍历poll实现及其被调用方的代码seems to confirm this遍历了各种调用,以检查超时是否过期。

因此,采用幼稚的方法显然需要更长的超时时间(最好是Long.MAX_VALUE,以避免较短的轮询间隔的不确定性行为),但是不幸的是,这将导致消费者阻止上一次轮询,在这种情况下是不希望的。使用幼稚的方法,我们现在可以在想要行为的确定性与我们在上一次民意测验中无理由等待的时间之间进行权衡。我们如何避免这种情况?

2 个答案:

答案 0 :(得分:0)

完成此操作的唯一方法似乎是使用一些自我管理偏移量的附加逻辑。这是伪代码:

consumer.assign(all partitions)
consumer.seekToBeginning(all partitions)
// record the current ending offsets and poll until we get there
endOffsets = consumer.endOffsets(all partitions)

do
  result = consumer.poll(NONTRIVIAL_TIMEOUT)
  // onResult(result)
while given any partition p, consumer.position(p) < endOffsets[p]

以及Kotlin中的实现:

val topicPartitions = consumer.partitionsFor(topic).map { TopicPartition(it.topic(), it.partition()) }
consumer.assign(topicPartitions)
consumer.seekToBeginning(consumer.assignment())

val endOffsets = consumer.endOffsets(consumer.assignment())
fun pendingMessages() = endOffsets.any { consumer.position(it.key) < it.value }

do {
  records = consumer.poll(Duration.ofMillis(1000))
  onResult(records)
} while(pendingMessages())

现在可以将轮询持续时间设置为一个合理的值(例如1s),而不必担心丢失消息,因为循环继续进行,直到使用者到达循环开始时标识的结束偏移为止。

还有另外一种情况可以正确处理:如果结束偏移量已更改,但是在当前偏移量和结束偏移量之间实际上没有消息,则轮询将阻塞并超时。因此,重要的是,不要将超时设置得太低(否则,使用者将在检索 可用的消息之前超时),并且也不得将其设置得太高(否则,使用者将花费太长时间来检索不可用的消息时超时)。如果删除了这些消息,或者删除并重新创建了主题,则可能发生后一种情况。

答案 1 :(得分:0)

如果没有人同时生产,则还可以使用endOffsets来获取最后一条消息的位置并消耗直到那条消息。

所以,用伪代码:

long currentOffset = -1
long endOffset = consumer.endOffset(partition)
while (currentOffset < endOffset) {
  records = consumer.poll(NONTRIVIAL_TIMEOUT) // discussed in your answer
  currentOffset = records.offsets().max()
}

通过这种方式,我们避免了最终的非零挂断,因为我们始终确定会收到一些东西。

如果消费者的头寸等于终端偏移量,则可能需要添加保护措施(因为那里没有任何消息)。

此外,您可能希望将max.poll.records设置为1,这样,如果有人并行生成,则不会消耗位于 结束偏移量之后的消息。