簡體   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