我的信息流有一个名为'类别'我为每个'类别提供了额外的静态元数据。在不同的商店,它每两天更新一次。这种查找的正确方法是什么? Kafka流有两种选择
在Kafka Streams之外加载静态数据,只需使用KStreams#map()
添加元数据。这是可能的,因为Kafka Streams只是一个图书馆。
将元数据加载到Kafka主题,将其加载到KTable
并执行KStreams#leftJoin()
,这似乎更自然,并将分区等留给Kafka Streams。但是,这要求我们保持KTable
加载所有值。请注意,我们必须加载整个查找数据,而不仅仅是更改。
以上哪项是查找元数据的正确方法?
是否可以在重新启动时始终强制从头开始强制读取一个流,这样就可以将所有元数据加载到KTable
。
还有其他方式使用商店吗?
答案 0 :(得分:11)
- 在Kafka Streams之外加载静态数据,只需使用KStreams #map()添加元数据。这是可能的,因为Kafka Streams只是一个图书馆。
醇>
这很有效。但通常人们会选择您列出的下一个选项,因为用于丰富输入流的辅助数据通常不是完全静态的;相反,它正在发生变化但很少发生:
- 将元数据加载到Kafka主题,将其加载到KTable并执行KStreams#leftJoin(),这似乎更自然,并将分区等留给Kafka Streams。但是,这要求我们保持KTable加载所有值。请注意,我们必须加载整个查找数据,而不仅仅是更改。
醇>
这是常用方法,除非您有特殊原因,否则我建议您坚持使用。
但是,这要求我们保持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;更改"如你所说;这里,我们有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
,则通常N
(M
明显小于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的实际表现形式。
另一个例子:
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