Google Cloud PubSub 将消息发送给多个消费者(在同一订阅中)

时间:2021-03-22 07:04:41

标签: google-cloud-pubsub

我有一个 Java SpringBoot2 应用程序 (app1),它向 Google Cloud PubSub 主题(它是发布者)发送消息。

其他 Java SpringBoot2 应用程序 (app2) 订阅了一个订阅以接收这些消息。但在这种情况下,我有多个实例(启用了 k8s 自动缩放),因此我有多个 pod 用于此应用程序使用来自 PubSub 的消息。

有些消息被一个 app2 实例消费,但许多其他消息被发送到多个 app2 实例,因此这些消息的消息处理是重复的。

这是消费者(app2)的代码:

private final static int ACK_DEAD_LINE_IN_SECONDS = 30;
private static final long POLLING_PERIOD_MS = 250L;
private static final int WINDOW_MAX_SIZE = 1000;
private static final Duration WINDOW_MAX_TIME = Duration.ofSeconds(1L);

@Autowired
private PubSubAdmin pubSubAdmin;

@Bean
public ApplicationRunner runner(PubSubReactiveFactory reactiveFactory) {
    return args -> {
        createSubscription("subscription-id", "topic-id", ACK_DEAD_LINE_IN_SECONDS);
        reactiveFactory.poll(subscription, POLLING_PERIOD_MS) // Poll the PubSub periodically
            .map(msg -> Pair.of(msg, getMessageValue(msg))) // Extract the message as a pair
            .bufferTimeout(WINDOW_MAX_SIZE, WINDOW_MAX_TIME) // Create a buffer of messages to bulk process 
            .flatMap(this::processBuffer) // Process the buffer
            .doOnError(e -> log.error("Error processing event window", e))
            .retry()
            .subscribe();
    };
}

private void createSubscription(String subscriptionName, String topicName, int ackDeadline) {
    pubSubAdmin.createTopic(topicName);
    try {
        pubSubAdmin.createSubscription(subscriptionName, topicName, ackDeadline);
    } catch (AlreadyExistsException e) {
        log.info("Pubsub subscription '{}' already configured for topic '{}': {}", subscriptionName, topicName, e.getMessage());
    }
}

private Flux<Void> processBuffer(List<Pair<AcknowledgeablePubsubMessage, PreparedRecordEvent>> msgsWindow) {
    return Flux.fromStream(
        msgsWindow.stream()
            .collect(Collectors.groupingBy(msg -> msg.getRight().getData())) // Group the messages by same data
            .values()
            .stream()
    )
    .flatMap(this::processDataBuffer);
}

private Mono<Void> processDataBuffer(List<Pair<AcknowledgeablePubsubMessage, PreparedRecordEvent>> dataMsgsWindow) {
    return processData(
        dataMsgsWindow.get(0).getRight().getData(),
        dataMsgsWindow.stream()
            .map(Pair::getRight)
            .map(PreparedRecordEvent::getRecord)
            .collect(Collectors.toSet())
    )
    .doOnSuccess(it ->
        dataMsgsWindow.forEach(msg -> {
            log.info("Mark msg ACK");
            msg.getLeft().ack();
        })
    )
    .doOnError(e -> {
        log.error("Error on PreparedRecordEvent event", e);
        dataMsgsWindow.forEach(msg -> {
            log.error("Mark msg NACK");
            msg.getLeft().nack();
        });
    })
    .retry();
}

private Mono<Void> processData(Data data, Set<Record> records) {
    // For each message, make calculations over the records associated to the data
    final DataQuality calculated = calculatorService.calculateDataQualityFor(data, records); // Arithmetic calculations
    return this.daasClient.updateMetrics(calculated) // Update DB record with a DaaS to wrap DB access
        .flatMap(it -> {
            if (it.getProcessedRows() >= it.getValidRows()) {
                return finish(data);
            }
            return Mono.just(data);
        })
        .then();
}

private Mono<Data> finish(Data data) {
    return dataClient.updateStatus(data.getId, DataStatus.DONE) // Update DB record with a DaaS to wrap DB access
        .doOnSuccess(updatedData -> pubSubClient.publish(
            new Qa0DonedataEvent(updatedData) // Publis a new event in other topic
        ))
        .doOnError(err -> {
            log.error("Error finishing data");
        })
        .onErrorReturn(data);
}

我需要每条消息都被一个且只有一个 app2 实例使用。有谁知道这是否可能?有什么想法可以实现这一目标吗?

也许正确的方法是为每个 app2 实例创建一个订阅并将主题配置为将每条消息发送到一个订阅而不是每个订阅。有可能吗?

根据official documentation,一旦消息发送给订阅者,Pub/Sub 会尝试不将其传递给同一订阅上的任何其他订阅者(app2 实例是同一订阅的订阅者): <块引用>

一旦消息发送给订阅者,订阅者应该 确认消息。一条消息一旦发出就被认为是未完成的 已被送出并在订户确认之前 它。 Pub/Sub 将反复尝试传递任何已 没有被承认。当消息对订阅者来说是未完成的时, 但是,Pub/Sub 尝试不将其交付给任何其他订阅者 相同的订阅。订户有一个可配置的、有限的 时间 - 称为 ackDeadline - 确认 优秀的消息。一旦截止日期过去,消息就没有了 不再被视为未完成,Pub/Sub 将尝试重新交付 消息

3 个答案:

答案 0 :(得分:0)

一般而言,Cloud Pub/Sub 具有至少一次交付语义。这意味着可以重新传递已经确认的消息,并且可以将消息传递给多个订阅者接收相同的订阅消息。对于行为良好的订阅者来说,这两种情况应该比较少见,但是如果不跟踪所有订阅者传递的所有消息的 ID,就无法保证不会有重复。

如果它以某种频率发生,最好检查您的消息是否在 ack 截止日期内得到确认。您将消息缓冲 1 秒,与 30 秒的确认截止时间相比,这应该相对较小,但这也取决于消息最终需要多长时间来处理。例如,如果缓冲区是按顺序处理的,则可能是 1000 条消息缓冲区中较晚的消息没有得到及时处理。您可以查看 subscription/expired_ack_deadlines_count 中的 Cloud Monitoring 指标来确定您的消息确认是否确实是延迟。请注意,即使是少量消息的延迟确认也可能导致更多重复。请参阅 Fine-tuning Pub/Sub performance with batch and flow control settings post 的“消息重新传送和重复率”部分。

答案 1 :(得分:0)

好的,在做了测试、阅读文档和查看代码之后,我发现了一个“小”错误。 我们对“processDataBuffer”方法进行了错误的“重试”,所以当发生错误时,缓冲区中的消息被标记为NACK,因此它们被传递到另一个实例,但是由于重试,它们被再次执行,正确,所以消息也被标记为ACK。 为此,他们中的一些人被起诉了两次。

private Mono<Void> processDataBuffer(List<Pair<AcknowledgeablePubsubMessage, PreparedRecordEvent>> dataMsgsWindow) {
    return processData(
        dataMsgsWindow.get(0).getRight().getData(),
        dataMsgsWindow.stream()
            .map(Pair::getRight)
            .map(PreparedRecordEvent::getRecord)
            .collect(Collectors.toSet())
    )
    .doOnSuccess(it ->
        dataMsgsWindow.forEach(msg -> {
            log.info("Mark msg ACK");
            msg.getLeft().ack();
        })
    )
    .doOnError(e -> {
        log.error("Error on PreparedRecordEvent event", e);
        dataMsgsWindow.forEach(msg -> {
            log.error("Mark msg NACK");
            msg.getLeft().nack();
        });
    })
    .retry(); // this retry has been deleted
}

我的问题解决了。

答案 2 :(得分:0)

一旦纠正了上述错误,我仍然收到重复的消息。可以接受的是,当您使用缓冲区或窗口时,Google Cloud 的 PubSub 不保证“完全交付”。这正是我的场景,所以我必须实现一种机制来根据消息 ID 删除重复项。