【问题标题】:confluent_kafka: how to reliably seek before reading data (avoiding Erroneous state)confluent_kafka:如何在读取数据之前可靠地寻找(避免错误状态)
【发布时间】:2022-10-01 02:05:02
【问题描述】:

我正在尝试将 Python 代码从 aiokafka 切换到 confluent_kafka 并且在读取历史数据时遇到问题。

系统对于给定的主题只有一个生产者,以及几个独立的消费者(每个都有一个单独的组 ID)。 当每个消费者启动时,它想读取主题子集的最新历史消息(称为历史主题),然后读取所有新消息。 历史数据的确切起点并不重要,因为重点是获取很少写的主题的信息。 需要历史数据的主题永远只有一个分区。

它得到了让我适应的历史数据。

我宁愿在搜索之前不必阅读任何消息,因为该消息可能比我想开始的要新。但似乎至少要在 Kafka 分配主题分区之前调用 Consumer.poll。

推荐的顺序是什么?

我尝试了两种基本方法:

  • 使用自动主题分区分配和on_assign 回调参数Consumer.subscribe 读取当前偏移量并调用seek。
  • 手动分配分区并使用这些分区读取当前偏移量并调用 seek。

在这两种情况下:

  • Consumer.seek 通常或总是以“本地:错误状态”失败。
  • Consumer.positions 总是返回 -1001,这可能是一个线索。 为了解决这个问题,我打电话给Consumer.get_watermark_offsets

下面是一个使用 on_assign 的简单示例:

from confluent_kafka import Consumer
from confluent_kafka.admin import AdminClient, NewTopic
from confluent_kafka.error import KafkaError
import base64
import os

max_history = 3
broker_addr = \"broker:29092\"
topic_names = [\"test.message\"]


def seek_back(
    consumer,
    partitions,
):
    print(f\"seek_back({partitions})\")

    # Show that consumer.position returns nothing useful
    position_partitions = consumer.position(partitions)
    print(f\"{position_partitions=}\")

    for partition in partitions:
        _, offset = consumer.get_watermark_offsets(partition)
        print(f\"{partition.topic} has offset {offset}\")
        if offset <= 0:
            continue

        partition.offset = max(0, offset - max_history)
        try:
            consumer.seek(partition)
        except Exception as e:
            print(f\"{partition.topic} seek to {partition.offset} failed: {e!r}\")
        else:
            print(f\"{partition.topic} seek to {partition.offset} succeeded\")


def run(topic_names):
    random_str = base64.urlsafe_b64encode(os.urandom(12)).decode().replace(\"=\", \"_\")
    consumer = Consumer(
        {
            \"group.id\": random_str,
            \"bootstrap.servers\": broker_addr,
            \"allow.auto.create.topics\": False,
        }
    )
    new_topic_list = [
        NewTopic(topic_name, num_partitions=1, replication_factor=1)
        for topic_name in topic_names
    ]
    broker_client = AdminClient({\"bootstrap.servers\": broker_addr})
    create_result = broker_client.create_topics(new_topic_list)
    for topic_name, future in create_result.items():
        exception = future.exception()
        if exception is None:
            continue
        elif (
            isinstance(exception.args[0], KafkaError)
            and exception.args[0].code() == KafkaError.TOPIC_ALREADY_EXISTS
        ):
            pass
        else:
            print(f\"Failed to create topic {topic_name}: {exception!r}\")
            raise exception

    consumer.subscribe(topic_names, on_assign=seek_back)
    while True:
        message = consumer.poll(timeout=0.1)
        if message is not None:
            error = message.error()
            if error is not None:
                raise error
            print(f\"read {message=}\")
            return


run(topic_names)

在为该主题编写一些消息(使用其他代码)之后运行它会给我:

seek_back([TopicPartition{topic=test.topic,partition=0,offset=-1001,error=None}])
position_partitions=[TopicPartition{topic=test.topic,partition=0,offset=-1001,error=None}]
test.topic has offset 10
seek_partitions=[TopicPartition{topic=test.topic,partition=0,offset=7,error=None}]
test.topic seek to 0 failed: KafkaException(KafkaError{code=_STATE,val=-172,str=\"Failed to seek to offset 7: Local: Erroneous state\"})

我正在使用:confluent_kafka 1.8.2 并使用 Docker 映像 confluentinc/cp-enterprise-kafka:6.2.4 运行代理 (以及相同版本的 zookeper 和模式注册表,因为我的正常代码使用 Avro 模式)。

  • 在调用 subscribe 后立即分配分区似乎有点帮助:然后 seek 成功,但代码仍然没有读取历史数据(poll 一直返回 None),并且即使调用 consumer.poll,consumer.position 仍然返回 unknown

标签: python apache-kafka confluent-kafka-python


【解决方案1】:

https://github.com/confluentinc/confluent-kafka-python/issues/11#issuecomment-230089107 看来,一种解决方案是为 Consumer.subscribe 指定一个 on_assign 回调,然后调用 Consumer.assign里面on_assign 回调,例如:

def on_assign_callback(
    consumer,
    partitions,
):
    """Modify assigned partitions to read up to MAX_HISTORY old messages"""
    for partition in partitions:
        min_offset, max_offset = consumer.get_watermark_offsets(partition)
        desired_offset = max_offset - MAX_HISTORY
        if desired_offset <= min_offset:
            desired_offset = OFFSET_BEGINNING
        partition.offset = desired_offset
    consumer.assign(partitions)

细微之处:

  • 回调必须分配所有主题分区,即使您不想要某些主题的历史数据。
  • 使用选项"auto.offset.reset": "earliest" 构造消费者。这样,如果代理在 on_assign 回调运行时丢弃数据,删除指定偏移量的数据,消费者将从头开始读取。

【讨论】:

    【解决方案2】:

    我找到了你的帖子,因为我遇到了类似的挑战,并且有一个适合我的解决方案。这不是基于水印,而是基于提交的偏移量:

    consumer.subscribe([topic_name])
    messages = []
    seeked = False
    while True:
        msg = consumer.poll(5)
        tps_comm = consumer.committed(consumer.assignment())
        if len(tps_comm) == 0:
            continue
        else:
            tp = tps_comm[0]
            if tp.offset == OFFSET_INVALID and not seeked:
                tp.offset = OFFSET_BEGINNING
                consumer.seek(tp)
                seeked = True
        if msg is None:
            continue
        elif msg.error():
            raise Exception(msg.error())
        else:
            print(f"got message at offset: {msg.offset()}")
            messages.append(msg)    
    

    我已经从我的实际解决方案中省略了max_messages 和循环超时逻辑,以支持上面共享的更简单的代码示例,该示例缺少任何循环外的break

    我收集到的是,当消费者连接到代理并订阅一个主题时,它不会立即被分配一个主题分区,如果你的poll 调用超时太短,甚至不会很快。在测试中,第一次尝试几秒钟可能就足够了。但是,通过尝试直到主题分区分配作为非空列表返回,然后检查组分区分配的提交偏移量,如果需要,我的消费者可以决定寻找主题分区的开头,否则,正常情况是poll 将开始为组主题分区分配返回任何新的未提交消息。

    由于我的消费者在提交消息之前需要对消息执行其他操作,因此我将 "enable.auto.commit": False 作为消费者配置设置。这是接收消息并在处理后提交其偏移量的不相交代码:

    tp_offsets = []
    for msg in messages:
        tp = TopicPartition(
            topic=msg.topic(),
            partition=msg.partition(),
            offset=msg.offset() + 1,
        )
        tp_offsets.append(tp)
    consumer.commit(offsets=tp_offsets)        
    

    注意:如果您订阅多个主题,上面的代码可能需要重新编写。

    【讨论】:

    • 我发现有趣的是,您的代码仅在位置为 OFFSET_INVALID 时才回溯。我的愿望是寻找特定数量的消息(通常是 1 条)。我需要有效的偏移量。我试过你的代码,只看到过 OFFSET_INVALID。到目前为止,我无法让 consumer.committed(...) 返回有效的偏移量。到目前为止,我更喜欢我的解决方案,因为它返回真正的偏移量。你的更好有技术原因吗?我不是卡夫卡专家。
    • 在我的实验中,消费者需要订阅并投票为了被分配一个分区;这不会同步发生。短时间过去后,broker 会为消费者分配一个分区;为我的组 id 获取提交偏移量的请求返回一个有效值。当您让on_assign 回调执行诸如寻求偏移量之类的操作时,这种方式是有意义的。如果您不先调用poll,则不会调用回调,因为它尚未分配分区。在撰写这篇文章和评论时:我也不是 Kafka 专家。
    • 我做了更多的实验,在每次调用 Consumer.poll 后发现:(a)Consumer.committed 总是返回 offset=-1001。 (b) Consumer.position 返回一个已知的偏移量,但只有在 poll 首先返回该主题的数据之后。一旦分配了分区,我期望已知的偏移量。显然很多东西要学。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-22
    • 1970-01-01
    • 2010-09-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多