Kafka Streams 和 RPC:在 map() 运算符中调用 REST 服务是否被视为反模式?

Kafka Streams and RPC: is calling REST service in map() operator considered an anti-pattern?

实现使用参考数据丰富存储在 Kafka 中的传入事件流的用例的天真的方法是调用 map() 运算符提供此参考的外部服务 REST API数据,针对每个传入事件。

eventStream.map((key, event) -> /* query the external service here, then return the enriched event */)

另一种方法是让第二个事件流包含参考数据并将其存储在 KTable 中,这将是一个轻量级嵌入式 "database",然后将其加入主事件流。

KStream<String, Object> eventStream = builder.stream(..., "event-topic");
KTable<String, Object> referenceDataTable = builder.table(..., "reference-data-topic");
KTable<String, Object> enrichedEventStream = eventStream 
    .leftJoin(referenceDataTable , (event, referenceData) -> /* return the enriched event */)
    .map((key, enrichedEvent) -> new KeyValue<>(/* new key */, enrichedEvent)
    .to("enriched-event-topic", ...);

"naive" 方法可以被视为反模式吗?可以推荐“KTable”方法作为首选方法吗?

Kafka 每分钟可以轻松管理数百万条消息。从 map() 运算符调用的服务也应该能够处理高负载并且具有高可用性。这些是服务实施的额外要求。但是,如果服务满足这些标准,可以使用 "naive" 方法吗?

我怀疑您从互联网上听到的大部分建议都是这样的,"OMG, if this REST call takes 200ms, how wil I ever process 100,000 Kafka messages per second to keep up with my demand?"

这在技术上是正确的:即使您为 REST 服务扩展服务器,如果此应用的响应通常需要 200 毫秒 - 因为它与 70 毫秒外的服务器通信(光速有点慢,如果该服务器与你隔着大陆......)即使你在源头上正确测量,调用微服务也需要 130 毫秒....

对于 kstreams,问题可能比看起来更严重。也许你每秒收到 100,000 条消息进入你的流管道,但是一些 kstream 运算符 flatMaps 和你应用程序中的那个操作为每个对象创建 2 条消息......所以现在你真的有 200,000 条消息每秒崩溃你的 REST 服务器。

BUT 也许您正在一个每秒有 100 条消息的应用程序中使用 Kstreams,或者您可以对数据进行分区,这样每个分区甚至可能只收到一次消息一秒。那样的话,你可能就没事了。

也许您的 Kafka 数据只需要转到其他地方:即流的末尾返回到 Good Ol' RDMS。在这种情况下,是的,在处理潜在 "slow" 系统的最佳方式上有一些谨慎的平衡,同时确保您自己不进行 DDOS,同时确保您可以解决积压问题。

那么它是 anti-pattern 吗?嗯,可能吧,如果你的 Kafka 集群是 LinkedIn 规模的话。这对你重要吗?取决于您需要驾驶多少 messages/second,您的 REST 服务的实际速度有多快,它的扩展效率如何(即您的新 kstreams 管道突然向其提供 5 倍的正常流量...)

是的,可以在Kafka Streams内部做RPC操作,比如map()操作。您只需要了解这样做的利弊,见下文。此外,您应该在您的操作中 同步 执行任何此类 RPC 调用(我不会在这里详细说明原因;​​如果需要,我建议创建一个新问题)。

在 Kafka Streams 操作中进行 RPC 调用的优点:

  • 您的应用程序将更容易适应现有架构,例如REST API 和 request/response 范式的使用很常见。这意味着您可以更快地取得第一个 proof-of-concept 或 MVP 的进步。
  • 根据我的经验,对于许多开发人员(尤其是那些刚开始使用 Kafka 的开发人员)来说,这种方法更容易理解,因为他们在过去的项目中熟悉以这种方式进行 RPC 调用。思考:从 request-response 架构逐渐迁移到 event-driven 架构(由 Kafka 提供支持)会有所帮助。
  • 没有什么能阻止您从 RPC 调用和 request-response 开始,然后再迁移到更 Kafka-idiomatic 的方法。

缺点:

  1. 您正在将 Kafka Streams 支持的应用程序的可用性、可伸缩性和 latency/throughput 与您正在调用的 RPC 服务的可用性、可伸缩性和 latency/throughput 相结合。这也与考虑 SLA 相关。
  2. 与前一点相关,Kafka 和 Kafka Streams 的扩展性非常好。如果您 运行 大规模使用,您的 Kafka Streams 应用程序可能会以 DDoS 攻击您的 RPC 服务,因为后者可能无法像 Kafka 那样扩展。你应该能够很容易地判断这是否是你在实践中遇到的问题。
  3. RPC 调用(例如来自 map() 的调用)是一个 side-effect,因此是 Kafka Streams 的黑盒。 Kafka Streams 的处理保证不会扩展到此类副作用。
    • 示例:Kafka Streams(默认情况下)根据 event-time(= 基于现实世界中事件发生的时间)处理数据,因此您可以轻松地 re-process 旧数据并仍然获得返回与旧数据仍然是新数据时相同的结果。但是您在此类重新处理期间调用的 RPC 服务可能 return 与 "back then" 不同的响应。确保后者是您的责任。
    • 示例:在失败的情况下,Kafka Streams 将重试操作,即使在这种情况下,它也会保证 exactly-once 处理(如果启用)。但它本身并不能保证您在 map() 中进行的 RPC 调用是幂等的。确保后者是您的责任。

备选方案

如果您想知道您还有哪些其他选择:例如,如果您正在进行 RPC 调用以查找数据(例如,使用 side/context 信息丰富传入的事件流),您可以通过直接在 Kafka 中提供查找数据来解决上述缺点。如果查找数据在 MySQL 中,您可以设置 Kafka 连接器以将 MySQL 数据连续提取到 Kafka 主题中(想想:CDC)。在 Kafka Streams 中,您可以将查找数据读入 KTable 并通过 stream-table 连接执行输入流的丰富。