使用查找数据丰富KStream的理想方式

时间:2016-12-08 00:26:44

标签: apache-kafka-streams

我的信息流有一个名为'类别'我为每个'类别提供了额外的静态元数据。在不同的商店,它每两天更新一次。这种查找的正确方法是什么? Kafka流有两种选择

  1. 在Kafka Streams之外加载静态数据,只需使用KStreams#map()添加元数据。这是可能的,因为Kafka Streams只是一个图书馆。

  2. 将元数据加载到Kafka主题,将其加载到KTable并执行KStreams#leftJoin(),这似乎更自然,并将分区等留给Kafka Streams。但是,这要求我们保持KTable加载所有值。请注意,我们必须加载整个查找数据,而不仅仅是更改。

    • 例如,最初说只有一个类别' c1'。 Kafka流应用程序优雅地停止,然后重新启动。重启后,新类别' c2'加入。我的假设是,table = KStreamBuilder()。table(' metadataTopic')只有值' c2',因为这是自app开始以来第二次改变时间。我希望它有' c1'和' c2'。
    • 如果确实有' c1'是否可以从KTable中删除数据(可能通过设置发送key = null消息?)?
  3. 以上哪项是查找元数据的正确方法?

    是否可以在重新启动时始终强制从头开始强制读取一个流,这样就可以将所有元数据加载到KTable

    还有其他方式使用商店吗?

3 个答案:

答案 0 :(得分:11)

  
      
  1. 在Kafka Streams之外加载静态数据,只需使用KStreams #map()添加元数据。这是可能的,因为Kafka Streams只是一个图书馆。
  2.   

这很有效。但通常人们会选择您列出的下一个选项,因为用于丰富输入流的辅助数据通常不是完全静态的;相反,它正在发生变化但很少发生:

  
      
  1. 将元数据加载到Kafka主题,将其加载到KTable并执行KStreams#leftJoin(),这似乎更自然,并将分区等留给Kafka Streams。但是,这要求我们保持KTable加载所有值。请注意,我们必须加载整个查找数据,而不仅仅是更改。
  2.   

这是常用方法,除非您有特殊原因,否则我建议您坚持使用。

  

但是,这要求我们保持KTable加载所有值。请注意,我们必须加载整个查找数据,而不仅仅是更改。

所以我猜你也更喜欢第二种选择,但你担心这是否有效。

简短回答是:是的,KTable将加载每个键的所有(最新)值。该表将包含整个查找数据,但请记住,KTable在幕后进行分区:例如,如果您的输入主题(对于表)具有3分区,那么您最多可以运行{ {1}}你的应用程序的实例,每个实例都获得表的3分区(假设数据在分区之间均匀分布,那么表的每个分区/共享将占据表的1/3左右) ; s数据)。所以在实践中更有可能它只是工作"。

  

是否可以在重新启动时始终强制从头开始强制读取一个流,这样就可以将所有元数据加载到KTable中。

您不必担心这一点。简单地说,如果没有本地"副本"在可用的表中,Streams API将自动确保从头开始完全读取表的数据。如果有可用的本地副本,那么您的应用程序将重新使用该副本(并在表的输入主题中提供新数据时更新其本地副本)。

使用示例更长的答案

想象一下1的以下输入数据(想想:更改日志流),请注意此输入如何包含KTable条消息:

6

这里是"逻辑"的各种状态。此输入产生的(alice, 1) -> (bob, 40) -> (alice, 2) -> (charlie, 600), (alice, 5), (bob, 22) 是每个新接收的输入消息(例如KTable)将导致表的新状态:

(alice, 1)

你可以在这里看到的是,即使输入数据可能有很多很多消息(或者#34;更改&#34;如你所说;这里,我们有Key Value -------------- alice | 1 // (alice, 1) received | V Key Value -------------- alice | 1 bob | 40 // (bob, 40) received | V Key Value -------------- alice | 2 // (alice, 2) received bob | 40 | V Key Value -------------- alice | 2 bob | 40 charlie | 600 // (charlie, 600) received | V Key Value -------------- alice | 5 // (alice, 5) received bob | 40 charlie | 600 | V Key Value -------------- alice | 5 bob | 22 // (bob, 22) received charlie | 600 ),结果6中的条目/行(基于新接收的输入正在进行连续突变)是输入中唯一键的数量(此处:从KTable开始,逐渐增加到{{ 1}}),通常明显小于消息数量。因此,如果输入中的消息数为1且这些消息的唯一键数为3,则通常NM明显小于M << N {1}};另外,对于记录,我们有不变的M)。

这是&#34;这要求我们保持KTable加载所有值的第一个原因&#34;通常不是问题,因为每个密钥只保留最新值。

第二个原因是,正如Matthias J. Sax所指出的那样,Kafka Streams使用RocksDB作为此类表的默认存储引擎(更确切地说:支持表的状态存储)。 RocksDB允许您维护大于应用程序的可用主内存/ Java堆空间的表,因为它可以溢出到本地磁盘。

最后,第三个原因是N被分区。因此,如果表的输入主题是(比方说)配置了M <= N分区,那么幕后发生的事情是KTable本身在分区中(思考:分片)同样的方式。在上面的例子中,这里有你可以得到的东西,尽管确切的&#34;分裂&#34;取决于原始输入数据如何分布在表的输入主题的分区中:

Logical KTable(我上面展示的最后一个状态):

3

实际KTable,分区(假设表格的输入主题为KTable分区,加上密钥=用户名在分区之间均匀分布):

Key      Value
--------------
alice   |   5
bob     |  22
charlie | 600

在实践中,输入数据的这种分区 - 除其他外 - 允许你&#34; size&#34; KTable的实际表现形式。

另一个例子:

  • 想象一下,你的KTable的最新状态通常是1 TB的大小(同样,大概的大小是表格输入数据中唯一消息密钥数量乘以平均大小的函数)相关的消息值)。
  • 如果表的输入主题只有3分区,则KTable本身也只有Key Value -------------- alice | 5 // Assuming that all data for `alice` is in partition 1 Key Value -------------- bob | 22 // ...for `bob` is in partition 2 Key Value -------------- charlie | 600 // ...for `charlie` is in partition 3 分区,大小为1 TB。在这里,因为输入主题只有1分区,所以你可以用最多1个app实例来运行你的应用程序(所以不是很多并行性,呵呵)。
  • 如果表格的输入主题有1个分区,那么KTable也有1个分区,每个分区的大小约为2 GB(假设数据均匀分布在分区上)。在这里,您可以使用最多500个应用实例运行您的应用。如果您要准确运行500个实例,那么每个应用程序实例将获得逻辑KTable的500分区/分片,最终得到2 GB的表数据;如果你只运行500个实例,那么每个实例都会获得表的1个分区/分片,最后得到大约100个表数据。

答案 1 :(得分:6)

您的整体观察是正确的,这取决于哪些权衡对您来说更重要。如果元数据很小,则选项1似乎更好。如果元数据很大,似乎选项2是可行的方法。

如果您使用map(),则需要在每个应用程序实例中获得元数据的完整副本(因为您无法确切知道Streams将如何对您KStream数据进行分区)。因此,如果您的元数据不适合使用map()的主内存,则无法轻松完成。

如果使用KTable,Streams会注意在所有正在运行的应用程序实例上正确分片元数据,这样就不需要重复数据。此外,KTable使用RocksDB作为状态存储引擎,因此可以溢出到磁盘。

编辑开始

关于在KTable中包含所有数据:如果您对同一个键有两个类别,那么如果您直接从主题中将数据读入KTable来{builder.table(...),则第二个值将覆盖第一个值{1}}(changelog语义)。但是,您可以通过将主题作为记录流(即builder.stream(...))轻松解决此问题,并应用聚合来计算KTable。您的聚合只会发出每个键的所有值的列表

关于删除:KTable使用changelog语义并确实理解tombstone消息以删除键值对。因此,如果您从主题中读取KTable并且主题包含<key:null>消息,则KTable中包含此密钥的当前记录将被删除。当KTable是聚合的结果时,这很难实现,因为具有null键或null值的聚合输入记录将被忽略,并且不会更新聚合结果。

解决方法是在聚合之前添加map()步骤并引入NULL值(即,用户定义的&#34;对象&#34;代表墓碑但不是{ {1}} - 在您的情况下,您可以将其称为null)。在聚合中,如果输入记录的值为null-category,则只返回null值作为aggegation结果。然后,这将转换为null-category的逻辑删除消息,并删除此密钥的当前类别列表。

编辑结束

当然,您始终可以通过Processor API构建自定义解决方案。但是,如果DSL可以满足您的需求,那么就没有充分的理由这样做。

答案 2 :(得分:0)

从2017年2月发布的Kafka 0.10.2.0开始,GlobalKTable概念可能是使用查找数据丰富流的更好选择。

https://docs.confluent.io/current/streams/concepts.html#globalktable