我使用kafka已经有一段时间了,但我对Kafka StreamsAPI和一般流处理的概念相当陌生。
想象一个kafka流应用程序,它通过使用一个由附加kafka主题提供的查找表来执行编码和解码。业务主题都是共同分区的,有5个分区。查找映射流是一个更改数据流——它是压缩的,只有一个分区。
+------------------------+ /-------\ +--------------------------+
| "Plain request" stream | -> | Encoder | -> | "Encoded request" stream | -----+
+------------------------+ \-------/ +--------------------------+ |
|
^ |
| v
+-------------------------+
| "Lookup mapping" stream | -> (Cache) (Some other app)
+-------------------------+
| |
v |
|
+-------------------------+ /-------\ +---------------------------+ |
| "Plain response" stream | <- | Decoder | <- | "Encoded response" stream | <---+
+-------------------------+ \-------/ +---------------------------+
让我们假设编码是ASCII('a'
-
我目前的草案是在GlobalKTable上使用LeftJoin(它实现了Lookup映射流):
final var mappingTable = streamsBuilder.globalTable(LOOKUP_MAPPINGS, ...);
streamsBuilder.stream(PLAIN_REQUESTS, ...)
.leftJoin(mappingTable, (key, value) -> value.getPlain(), EncoderHelper::encode)
.to(ENCODED_REQUESTS, ...);
编码工作符合预期。但是解码呢?
物化的GlobalKTable包含如下数据:
对于编码,LeftJoin操作可以根据普通值执行查找——该值用作表的键。但是对于解码,LeftJoin操作需要根据不是表中键的编码值(即97)执行查找。
我不知道解码应该如何设计。
通过kafka消费者API消费主题,存储数据并“手动”执行编码/解码。这可行,但有一些陷阱(需要确保在流处理开始之前读取所有数据)。我不太喜欢这个解决方案,因为它感觉像重新发明轮子。
我意识到我可以创建另一个主题(在我的程序之外),它使用编码值作为键,然后复制上面的解决方案。但这意味着两个强耦合的主题,感觉通常是错误的。
在将主题具体化为GlobalKTable之前,我还考虑过在我的程序中重新划分主题。基本上与之前的概念相同,但这一次,额外的主题只需要存在于我的程序中。这仍然会导致重复映射,因此增加内存占用。我可以接受,因为我的用例中只有少量映射(大约1000个)。这仍然感觉不是最佳的(我也不认为它是GlobalKTable的预期用例)。
我可以重新分区并使用KTable。但是由于共同分区的要求,这看起来很复杂,而且感觉也不对。
我正在寻找这样一个双向查找系统如何根据kafka流API设计原则实现的提示。更有经验的流人将如何解决这个问题?
相似但不相同:
使用kafka流的数据浓缩,KStream-GlobalKtable Join:想象一下,我想在他们的场景中从"john"
到XDFER
进行查找。
KStream通过非键值与GlobalKTable连接:纯值('a'
)不存在于编码的响应流中。所以它不能用作键。
使用DSL,您已经提供了答案。您需要两个主题和两个全局表。
如果您愿意接受一些每次点击,您可以回退到处理器API并使用“全局存储”(它与全局表基本相同,但API更灵活)。而不是使用join()
你实现了一个自定义的处理器
(或变压器
;取决于你使用的Kafka Streams的版本),在process()
函数中,你可以对全局存储进行全表扫描(而不仅仅是键查找)以找到正确的条目。--鉴于你没有存储大量数据,你甚至可以在内存中缓存一个反向映射以避免一直扫描整个存储(当然,如果全局存储得到更新,你会得到缓存失效问题…)