如何使用Kafka流式DSL功能处理重复消息

时间:2019-04-23 01:46:19

标签: apache-kafka-streams

我的要求是跳过或避免使用kafka流DSL API从INPUT主题收到重复的消息(具有相同的密钥)。

如果发生任何故障,源系统可能会将重复的消息发送到INPUT主题。

流量-

源系统->输入主题-> Kafka流->输出主题

当前,我正在使用flatMap生成有效载荷中的多个密钥,但是flatMap是无状态的,因此无法避免从INPUT Topic接收到重复的消息处理。

我正在寻找DSL API,该API可以跳过从INPUT主题接收到的重复记录,并且还可以在发送到OUTPUT主题之前生成多个键/值。

“精确思考一次”配置在此处可用于基于密钥对从INPUT主题接收到的消息进行重复数据删除,但看起来它无法正常工作,可能我不了解“精确修饰一次”的用法。

您能给它点灯吗?

3 个答案:

答案 0 :(得分:0)

仅一次即可用于确保使用和处理输入主题不会导致输出主题重复。但是,从一次角度来看,您描述的输入主题中的重复项实际上并不是重复项,而是两条常规输入消息。

要除去输入主题重复项,可以使用带有附加状态存储的transform()步骤(DSL中没有内置的运算符可以执行您想要的操作)。对于每个输入记录,首先要检查是否在商店中找到相应的键。如果没有,则将其添加到商店中并转发消息。如果在商店中找到它,则将输入重复删除。请注意,如果您在Kafka Streams应用程序中启用了一次精确处理,则只有在100%正确性保证的情况下才能使用。其他人,即使您尝试进行重复数据删除,如果发生故障,Kafka Streams可能会重新引入重复数据。

此外,您需要确定要将条目保留在商店中的时间。如果您确定输入主题中没有其他重复项,则可以使用Punctuation从存储中删除旧数据。一种方法是将记录时间戳(或偏移量)也存储在存储中。这样,您可以将当前时间与punctuate()中的商店记录时间进行比较,并删除旧记录(即,您可以通过store#all()对商店中的所有条目进行迭代)。

transform()之后,您可以应用flatMap()(或者也可以将flatMap()代码直接合并到transform()中。

答案 1 :(得分:0)

  

我的要求是跳过或避免使用kafka流DSL API从INPUT主题收到重复的消息(具有相同的密钥)。

看看https://github.com/confluentinc/kafka-streams-examples上的EventDeduplication示例,它就是这样做的。然后,您可以使用特定于您的用例的必需flatMap功能来修改示例。

这是示例的要点:

final KStream<byte[], String> input = builder.stream(inputTopic);
final KStream<byte[], String> deduplicated = input.transform(
    // In this example, we assume that the record value as-is represents a unique event ID by
    // which we can perform de-duplication.  If your records are different, adapt the extractor
    // function as needed.
    () -> new DeduplicationTransformer<>(windowSize.toMillis(), (key, value) -> value),
    storeName);
deduplicated.to(outputTopic);

    /**
     * @param maintainDurationPerEventInMs how long to "remember" a known event (or rather, an event
     *                                     ID), during the time of which any incoming duplicates of
     *                                     the event will be dropped, thereby de-duplicating the
     *                                     input.
     * @param idExtractor extracts a unique identifier from a record by which we de-duplicate input
     *                    records; if it returns null, the record will not be considered for
     *                    de-duping but forwarded as-is.
     */
    DeduplicationTransformer(final long maintainDurationPerEventInMs, final KeyValueMapper<K, V, E> idExtractor) {
      if (maintainDurationPerEventInMs < 1) {
        throw new IllegalArgumentException("maintain duration per event must be >= 1");
      }
      leftDurationMs = maintainDurationPerEventInMs / 2;
      rightDurationMs = maintainDurationPerEventInMs - leftDurationMs;
      this.idExtractor = idExtractor;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void init(final ProcessorContext context) {
      this.context = context;
      eventIdStore = (WindowStore<E, Long>) context.getStateStore(storeName);
    }

    public KeyValue<K, V> transform(final K key, final V value) {
      final E eventId = idExtractor.apply(key, value);
      if (eventId == null) {
        return KeyValue.pair(key, value);
      } else {
        final KeyValue<K, V> output;
        if (isDuplicate(eventId)) {
          output = null;
          updateTimestampOfExistingEventToPreventExpiry(eventId, context.timestamp());
        } else {
          output = KeyValue.pair(key, value);
          rememberNewEvent(eventId, context.timestamp());
        }
        return output;
      }
    }

    private boolean isDuplicate(final E eventId) {
      final long eventTime = context.timestamp();
      final WindowStoreIterator<Long> timeIterator = eventIdStore.fetch(
          eventId,
          eventTime - leftDurationMs,
          eventTime + rightDurationMs);
      final boolean isDuplicate = timeIterator.hasNext();
      timeIterator.close();
      return isDuplicate;
    }

    private void updateTimestampOfExistingEventToPreventExpiry(final E eventId, final long newTimestamp) {
      eventIdStore.put(eventId, newTimestamp, newTimestamp);
    }

    private void rememberNewEvent(final E eventId, final long timestamp) {
      eventIdStore.put(eventId, timestamp, timestamp);
    }

    @Override
    public void close() {
      // Note: The store should NOT be closed manually here via `eventIdStore.close()`!
      // The Kafka Streams API will automatically close stores when necessary.
    }

  }
  

我正在寻找DSL API,该API可以跳过从INPUT主题接收到的重复记录,并且还可以在发送到OUTPUT主题之前生成多个键/值。

DSL没有开箱即用的功能,但是上面的示例显示了如何通过将DSL与Kafka Streams的Processor API结合使用,轻松构建自己的重复数据删除逻辑。 1}}。

  

“精确思考一次”配置在此处可用于基于密钥对从INPUT主题接收到的消息进行重复数据删除,但看起来它无法正常工作,可能我不了解“精确修饰一次”的用法。

正如Matthias J. Sax在回答中提到的那样,从Kafka的角度来看,从其一次处理语义的角度来看,这些“重复项”不是重复项。 Kafka确保自己不会引入任何此类重复项,但不能为上游数据源即开即用地做出此类决定,而上游数据源是Kafka的黑匣子。

答案 2 :(得分:0)

感谢Matt和Michel的帮助。非常感激。

我当时正在考虑结合使用FlatMap和FilterNot API。只是模拟者到州立商店,我们将交易详细信息存储到canssandra中。

FilterNot-逻辑可以包括连接Cassandra和检查重复项。 FlatMap-逻辑包括生成多个键/值并将其发送到OUTPUT主题。

这里要考虑到与Cassandra的连接是否失败以及第一种建议的方法-在每天数百万笔交易,保留期等情况下,状态存储的可持续性。

请让我知道哪种方法更好。