繁体   English   中英

当主题有多个分区时,KTable-KTable 外键连接不生成所有消息

[英]KTable-KTable foreign-key join not producing all messages when topics have more than one partition

请参阅下面的更新以显示潜在的解决方法

我们的应用程序使用 2 个主题作为 KTables,执行左连接,并输出到一个主题。 在测试过程中,我们发现当我们的 output 主题只有 1 个分区时,这会按预期工作。 当我们增加分区数量时,我们注意到生成到 output 主题的消息数量减少了。

在启动应用程序之前,我们使用多个分区配置测试了这一理论。 使用 1 个分区,我们可以看到 100% 的消息。 对于 2,我们看到一些消息(少于 50%)。 有 10 个时,我们几乎看不到(不到 10%)。

因为我们正在加入,所以从主题 1 使用的每条消息都应该写入我们的 output 主题,但我们发现这并没有发生。 消息似乎卡在了从 Ktables 的外键连接创建的“中间”主题中,但没有错误消息。

任何帮助将不胜感激!

服务.java

@Bean
public BiFunction<KTable<MyKey, MyValue>, KTable<MyOtherKey, MyOtherValue>, KStream<MyKey, MyEnrichedValue>> process() {

    return (topicOne, topicTwo) ->
            topicOne
                    .leftJoin(topicTwo,
                            value -> MyOtherKey.newBuilder()
                                    .setFieldA(value.getFieldA())
                                    .setFieldB(value.getFieldB())
                                    .build(),
                            this::enrich)
                    .toStream();
}

build.gradle

plugins {
    id 'org.springframework.boot' version '2.3.1.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'com.commercehub.gradle.plugin.avro' version '0.9.1'
}

...

ext {
    set('springCloudVersion', "Hoxton.SR6")
}

...

implementation 'org.springframework.cloud:spring-cloud-stream-binder-kafka-streams'
implementation 'io.confluent:kafka-streams-avro-serde:5.5.1'

注意:由于 spring-cloud-stream 中包含的版本存在错误,我们排除了 org.apache.kafka 依赖项

应用.yml

spring:
  application:
    name: app-name
    stream:
      bindings:
        process-in-0:
          destination: topic1
          group: ${spring.application.name}
        process-in-1:
          destination: topic2
          group: ${spring.application.name}
        process-out-0:
          destination: outputTopic
      kafka:
        streams:
          binder:
            applicationId: ${spring.application.name}
            brokers: ${KAFKA_BROKERS}
            configuration:
              commit.interval.ms: 1000
              producer:
                acks: all
                retries: 20
              default:
                key:
                  serde: io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde
                value:
                  serde: io.confluent.kafka.streams.serdes.avro.SpecificAvroSerde
            min-partition-count: 2

测试场景:

举一个具体的例子,如果我向主题 1 发布以下 3 条消息:

{"fieldA": 1, "fieldB": 1},,{"fieldA": 1, "fieldB": 1}
{"fieldA": 2, "fieldB": 2},,{"fieldA": 2, "fieldB": 2}
{"fieldA": 3, "fieldB": 3},,{"fieldA": 3, "fieldB": 3}
{"fieldA": 4, "fieldB": 4},,{"fieldA": 4, "fieldB": 4}

output主题只会收到2条消息。

{"fieldA": 2, "fieldB": 2},,{"fieldA": 2, "fieldB": 2}
{"fieldA": 3, "fieldB": 3},,{"fieldA": 3, "fieldB": 3}

另外2个人怎么了? 似乎某些键/值对无法写入 output 主题。 重试这些“丢失”的消息也不起作用。

更新:

我能够通过将主题 1 作为 KStream 而不是 KTable 使用并在继续执行 KTable-KTable 连接之前调用 toTable toTable()来正常运行。 我仍然不确定为什么我的原始解决方案不起作用,但希望此解决方法可以阐明实际问题。

@Bean
public BiFunction<KStream<MyKey, MyValue>, KTable<MyOtherKey, MyOtherValue>, KStream<MyKey, MyEnrichedValue>> process() {

    return (topicOne, topicTwo) ->
            topicOne
                    .map(...)
                    .toTable()
                    .leftJoin(topicTwo,
                            value -> MyOtherKey.newBuilder()
                                    .setFieldA(value.getFieldA())
                                    .setFieldB(value.getFieldB())
                                    .build(),
                            this::enrich)
                    .toStream();
}

鉴于问题的描述,似乎(左)KTable 输入主题中的数据未按其键正确分区。 对于一个单独的分区主题,好吧,只有一个分区,所有数据都进入这个分区,并且连接结果是完整的。

但是,对于多分区的输入主题,您需要确保数据按 key 分区,否则具有相同 key 的两条记录可能最终在不同的分区中,因此连接失败(因为连接是在每个-分区基础)。

请注意,即使外键连接不要求两个输入主题是共同分区的,但仍然需要每个输入主题本身都按其键进行分区!

如果您使用map().toTable() ,则基本上会触发数据的内部重新分区,以确保数据按键进行分区,从而解决了问题。

我有一个类似的问题。 我有两个传入的 KStreams,我将其转换为 KTables,并执行了 KTable-KTable FK 连接。 Kafka 流完全没有产生任何记录,连接从未执行过。

重新分区 KStreams 对我不起作用。 相反,我不得不手动将分区大小设置为 1。

这是一个不起作用的精简示例:

注意我使用的是 Kotlin,带有一些扩展辅助函数

fun enrichUsersData(
  userDataStream: KStream<UserId, UserData>,
  environmentDataStream: KStream<RealmId, EnvironmentMetaData>,
) {

  // aggregate all users on a server into an aggregating DTO
  val userDataTable: KTable<ServerId, AggregatedUserData> =
    userDataStream
      .groupBy { _: UserId, userData: UserData -> userData.serverId }
      .aggregate({ AggregatedUserData }) { serverId: ServerId, userData: UserData, usersAggregate: AggregatedUserData ->
        usersAggregate
          .addUserData(userData)
          .setServerId(serverId)
        return@aggregate usersAggregate
      }

  // convert all incoming environment data into a KTable
  val environmentDataTable: KTable<RealmId, EnvironmentMetaData> =
    environmentDataStream
      .toTable()

  // Now, try to enrich the user's data with the environment data
  // the KTable-KTable FK join is correctly configured, but...
  val enrichedUsersData: KTable<ServerId, AggregatedUserData> =
    userDataTable.join(
      other = environmentDataTable,
      tableJoined = tableJoined("enrich-user-data.join"),
      materialized = materializedAs(
        "enriched-user-data.store",
        jsonMapper.serde(),
        jsonMapper.serde(),
      ),
      foreignKeyExtractor = { usersData: AggregatedUserData -> usersData.realmId },
    ) { usersData: AggregatedUserData, environmentData: EnvironmentMetaData ->
      usersData.enrichUserData(environmentData)
      // this join is never called!!
      return@join usersData
    }
}

如果我手动将分区大小设置为 1,那么它就可以工作。

fun enrichUsersData(
  userDataStream: KStream<UserId, UserData>,
  environmentDataStream: KStream<RealmId, EnvironmentMetaData>,
) {

  // manually set the partition size to 1 *before* creating the table
  val userDataTable: KTable<ServerId, AggregatedUserData> =
    userDataStream
      .repartition(
        repartitionedAs(
          "user-data.pre-table-repartition",
          jsonMapper.serde(),
          jsonMapper.serde(),
          numberOfPartitions = 1,
        )
      )
      .groupBy { _: UserId, userData: UserData -> userData.serverId }
      .aggregate({ AggregatedUserData }) { serverId: ServerId, userData: UserData, usersAggregate: AggregatedUserData ->
        usersAggregate
          .addUserData(userData)
          .setServerId(serverId)
        return@aggregate usersAggregate
      }

  // again, manually set the partition size to 1 *before* creating the table
  val environmentDataTable: KTable<RealmId, EnvironmentMetaData> =
    environmentDataStream
      .repartition(
        repartitionedAs(
          "environment-metadata.pre-table-repartition",
          jsonMapper.serde(),
          jsonMapper.serde(),
          numberOfPartitions = 1,
        )
      )
      .toTable()

  // this join now works as expected!
  val enrichedUsersData: KTable<ServerId, AggregatedUserData> =
    userDataTable.join(
      other = environmentDataTable,
      tableJoined = tableJoined("enrich-user-data.join"),
      materialized = materializedAs(
        "enriched-user-data.store",
        jsonMapper.serde(),
        jsonMapper.serde(),
      ),
      foreignKeyExtractor = { usersData: AggregatedUserData -> usersData.realmId },
    ) { usersData: AggregatedUserData, environmentData: EnvironmentMetaData ->
      usersData.enrichUserData(environmentData)
      return@join usersData
    }
}

选择加入主题的键可能会有所帮助。 主题的分区配置应该相同。

return (topicOne, topicTwo) ->
        topicOne
            .leftJoin(topicTwo,
                value -> MyOtherKey.newBuilder()
                    .setFieldA(value.getFieldA())
                    .setFieldB(value.getFieldB())
                    .build(),
                this::enrich)
            .toStream().selectKey((key, value) -> key);

这是一个奇怪的问题,我从来没有听说过一些 output 主题分区控制数据写入频率。 但是我知道toStream()仅在缓存已满时才将数据写入下游,因此请尝试设置cache.max.bytes.buffering = 0 此外,KTable 仅保留每个键的最新记录,因此如果您对同一个键有多个值,则只有最新值会保留并写入下游。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM