让我们来谈谈通信工具和模式。随着 Redis 中 Streams 的引入,我们现在除了 Redis Pub/Sub 和其他工具(如 Kafka 和 RabbitMQ)之外,还有另一种通信模式需要考虑。在本文中,我将引导您了解各种通信模式的定义特征,并简要介绍用于实现每种模式的最流行工具。最后,我会给您一些小建议,希望可以帮助您更快地构建更好的解决方案。
在这个语境中,同步意味着所有参与方都需要同时处于活动状态才能进行通信。最简单的形式是服务 A 和服务 B 进行直接远程过程调用 (RPC),例如通过从服务 A 调用服务 B 的 HTTP REST 端点。如果服务 B 离线,服务 A 将无法与 B 通信,因此 A 需要实现内部故障恢复程序,这大多数时候意味着进行优雅降级。例如,如果推荐服务无法访问,Netflix 的“接下来观看”部分可以显示节目的随机样本。
这些服务只进行优雅降级,因为对于更敏感的用例(例如,支付服务要求订单服务开始处理已支付订单),我将在下面描述的其他异步机制更为常见。虽然 RPC 范式在点对点通信方面运作良好,但您偶尔需要支持一对多或多对多。此时,您有两个主要选项:无代理或代理工具。
无代理意味着参与者仍然直接连接,但可以选择使用与 RPC 不同的模式。在此类别中,我们有诸如 ZeroMQ 和较新的 nanoMsg 之类的库。它们被恰当地描述为“增强型 TCP 套接字”。实际上,您将库导入到您的代码中,并使用它实例化一个可以采用各种内置消息路由机制(如 Pub/Sub、Push/Pull、Dealer/Router 等)的连接。
代理意味着参与者连接到同一个服务,该服务充当(顾名思义)中央代理,以实现整个消息路由机制。虽然这种架构通常被描述为星形,代理是星形的中心,但代理本身可以(而且通常是)一个集群系统。
就我所知,在这个类别中,Redis Pub/Sub 独树一帜。您仍然可以使用像 NATS 或 RabbitMQ 这样的具有持久性的工具来处理这种用例,因为它们确实允许您关闭持久性,但我所知道的唯一纯同步消息代理是 Redis。区别不仅在于持久性,还在于可靠传递(即应用程序级别的确认)与即发即忘的一般概念。RabbitMQ 默认使用前者行为,而 Redis Pub/Sub 则专注于仅对即发即忘执行最少的工作量。您可以想象,这会影响性能(毕竟没有免费的午餐),但可靠传递适用于更广泛的用例。
由于 Redis 5 版本之前没有 Streams,所以有些人选择在需要更强传递保证的情况下使用 Pub/Sub,现在正在切换到 Streams。因此,如果您正在构建一个新应用程序或对当前使用 Pub/Sub 的应用程序不满意,那么如果您需要的是“Pub/Sub,但具有断开连接后恢复而不会丢失消息的功能”,请考虑使用 Redis Streams。
无代理工具是您可以想到的最快的通信方法,甚至比 Redis Pub/Sub 还要快。不幸的是,它们无法抽象化所有复杂性——例如,每个参与者都需要知道所有其他参与者的位置才能连接到它们,或者您通常不必在代理系统中处理的复杂故障场景(例如,参与者在扇出过程中死亡的情况)。
在这种情况下,使用 Redis Pub/Sub 的好处在于,您不必放弃太多吞吐量,并且可以获得一个简单、普遍存在的基础设施,以及一个小的集成面。您只需要一个适合您语言的 Redis 客户端,并且可以使用 PUBLISH 和 (P)SUBSCRIBE 来传递消息。
当然,异步意味着即使并非所有参与者都同时在场,通信仍然可以发生。为了启用这种模式,持久化消息是强制性的,否则在出现故障的情况下无法保证传递。此类别中的工具主要包括基于队列或基于流的解决方案。
这是执行异步通信的“传统”方式,也是大多数面向服务的架构 (SOA) 的基础。其思想是,当一个服务需要与另一个服务通信时,它会在一个中央系统中留下一个消息,另一个服务会稍后拾取该消息。实际上,这些消息收件箱就像任务队列。
这些系统的另一个预期是,任务彼此独立。这意味着它们可以(而且几乎总是)由多个相同的消费者并行处理,这些消费者通常称为工作者。此属性还支持独立故障,这对于许多工作负载来说都是一个很好的特性。例如,无法处理来自一个用户的付款(可能是由于缺少个人资料信息或其他琐碎问题)不会阻止所有用户的整个付款处理流程。
此类别中最著名的工具是 RabbitMQ,其次是大量其他工具和云服务,这些工具主要使用 AMQP(Rabbit 的本机协议)或 MQTT(类似的开放标准)。使用 RabbitMQ 通过提供一种简单方法来实现各种重试策略(例如,指数回退和死信)以及使消息处理在特定客户端生态系统中更具特色的糖衣接口是常见做法。其中一些框架是“简单的”任务队列,如 Sidekiq (Ruby)、Celery (Python)、Dramatiq (Python) 等。其他框架是“更严肃的”企业服务总线 (ESB),如 NServiceBus (C#)、MassTransit (C#)、Apache Synapse (Java) 或 Mulesoft (Java)。
这种模式的简单版本(任务队列)也可以使用 Redis 列表直接实现。Redis 具有阻塞和原子操作,使构建定制解决方案变得非常容易。特别值得一提的是 Kue,它在 Redis 中使用了一种巧妙的任务队列实现方式,适用于 JavaScript。
首先,值得注意的是,使用流的最简单方法只是作为一种存储形式。流是不可变的、仅追加的按时间排序的条目序列,许多类型的数据自然地适合这种格式。例如,传感器读数或日志包含按创建时间的自然索引且仅追加的值(您无法更改过去)。它们还具有相当规律的结构(因为它们倾向于保持相同的一组字段),这是一个流可以利用以提高空间效率的属性。这种类型的数据非常适合流,因为访问数据的最直接方法是检索给定的时间范围,流可以非常有效地执行此操作。
回到我们的通信用例,所有流实现都允许客户端尾随流,并在添加新条目时接收实时更新。这有时被称为观察或订阅流。使用流作为通信工具的最简单方法是将您本来在 Pub/Sub 上发布的内容推送到流中,基本上创建了一个可恢复的 Pub/Sub。每个订阅者只需要记住它处理的最后一个条目 ID,这样它就可以在发生崩溃或断开连接时轻松恢复。
也可以(有时更可取)在流上实现服务到服务的通信,进入 **流式架构** 领域。这里的主要概念是,我们之前描述的任务/消息现在将成为一个事件。在基于队列的设计中,任务由想要执行某些操作的其他服务推送到服务的队列,但在流式架构中,情况则相反:每个服务将状态更新推送到自己的流中,而该流又由其他服务观察。
这种设计变更带来许多微妙的影响。例如,您可以稍后添加新服务,并让它们遍历整个流历史记录。在队列中,这是不可能的,因为任务一旦完成就会被删除,并且这些系统中通常表达通信的方式也不允许这样做(想想命令式编程与函数式编程)。
流具有双重性质:数据结构和通信模式。一些数据自然地适合这种模式(例如日志),服务之间的通信不一定需要基于任务队列。完全拥抱这种双重性质的做法被称为 事件溯源。
使用 **事件溯源**,您可以将您的业务模型定义为无休止的事件流,并让业务逻辑和其他服务对它做出反应。这种转换并不总是容易,但当处理诸如“对象 X 在时间 Y 的状态如何?”之类的难题时,它的好处可以很大,否则在没有适当的审计日志的情况下,这个问题很难回答,甚至根本无法回答。
也许您普通的移动应用程序不需要事件溯源,但对于必须处理客户个人数据、运输和其他“混乱”领域的企业软件,它可能很有帮助。
为了实现这些类型的模式,您可以使用许多工具。当然,我们应该从房间里的大象开始:Apache Kafka,以及 Apache Pulsar(来自雅虎)等替代方案和 Kafka 在其他语言中的重新实现,以及一些 SaaS 产品。最后,还有一个新来者:Redis Streams。
首先,请注意,Redis 所谓的“流”,Kafka 称之为“主题分区”,而在 Kafka 中,流是一个完全不同的概念,它围绕着处理 Kafka 主题的内容展开。
也就是说,就表达能力而言,这两个系统是等效的:您可以在两者上实现相同的应用程序,而无需对数据建模方式进行任何实质性更改。当您深入实践细节时,差异就出现了,而且差异很多,而且很大。
Kafka 已经存在很长时间了,人们成功地构建了可靠的流式架构,其中 Kafka 是唯一的真相来源。但是,如果您愿意尝试新技术,重视开发和运营的简单性,并且需要亚毫秒级的延迟,那么 Redis Streams 可以填补您架构中的类似位置。
我说简单,我是认真的。如果您从未尝试过 Redis Streams,即使您计划在生产中使用 Kafka,我也建议您尝试使用 Redis Streams 对您的应用程序进行原型设计,因为只需几分钟即可在您的笔记本电脑上启动并运行。
让我们考虑一些示例,看看哪些问题最适合由每种模式解决。
如果您试图让几个客户端设备(例如手机、Arduino)在局域网内相互通信或与运行在计算机上的程序通信,那么通往工作解决方案的最短路径可能是“增强型 TCP 连接”。
IRC 风格的聊天应用程序(即没有历史记录),或者用于易变日志/事件的即插即用实时处理管道,非常适合代理方法。 Benjamin Sergeant 在旧金山 RedisConf19 上谈到了最后一个用例 (幻灯片).
网络爬虫经常依赖这种模式,以及许多 Web 服务,这些服务的操作无法立即响应请求完成。例如,想想 YouTube 中的视频编码。
除了 Slack 风格的聊天应用程序(即带有历史记录)外,此技术最适合日志处理、物联网 (IoT) 设备和微服务。
在结束之前,我想留下最后一个思考。Redis 的真正强大之处在于它不仅仅是一个发布/订阅消息系统、队列或流服务。它也不仅仅是一个通用数据库。实际上,只要有足够的毅力,您就可以在关系型 DBMS 之上实现上面描述的每一种模式,但是从实际角度来看,这样做是一个坏主意。
Redis 提供了 **真正的** 发布/订阅即发即忘系统,以及 **真正的** 流数据类型。此外,使用 Redis 模块,Redis 还支持许多不同数据类型的真实实现。让我将这种断言映射回我们持久和非持久聊天应用程序的用例。
上面,我得出结论,发布/订阅将是正确的选择,因为这种类型的聊天应用程序只需要将消息发送到已连接的客户端。但是,即使要实现这个应用程序的简单版本,您仍然必须考虑一个全局频道列表和每个频道的用户存在列表。您将此状态存储在哪里?您如何保持它的最新状态,尤其是在服务实例意外死亡时?在 Redis 中,答案很简单:有序集合、过期键和原子操作。如果您使用的是 RabbitMQ,您将需要一个 DBMS。
对话可以非常自然地表达为消息流。我们甚至不必在这里引入事件溯源,因为它已经是这种数据类型的原生结构。那么为什么 Redis 比 Kafka 更适合这个例子呢?正如之前一样,您的聊天系统不仅仅是消息流。还有频道和其他状态,最好以不同的方式表示。还有“用户 X 正在输入...”功能:这些信息是易变的,您希望将其发送给所有参与者,但仅在他们连接时发送。如果您使用的是 Kafka,您将需要无论如何都启动一个发布/订阅系统。
**在分布式系统中,当您需要协调时,您通常需要共享状态,反之亦然。** 忽视这一事实经常会导致过于复杂的解决方案。Redis 非常了解这一点,这也是其独特设计背后的原因之一。如果您接受这个原则,您会发现,有了正确的基本元素,一些难题就可以用很少的命令解决,而这正是 Redis 为您提供的。例如,这就是您可以事务性地将条目追加到流、将任务推送到(队列的开头)以及发布到发布/订阅的方式
MULTI
XADD logs:service1 * level error req-id 42 stack-trace "..."
LPUSH actions-queue "RESTART=service1"
PUBLISH live-notifs "New error event in service1!"
EXEC
要获得工作解决方案,您需要克服七个邪恶的并发问题。
《Redis 朝圣者大战世界》的情节
我希望这能让您了解分布式系统通常采用的主要通信模式。下次您需要将两个服务连接在一起时,这将有助于您了解您的选择。如果您喜欢谈论增强型 TCP 连接和流式架构,请随时在 Twitter 上联系我 @croloris.
Redis Streams 数据类型是 Redis 的一个很棒的功能,它将成为许多应用程序的构建块,尤其是现在 Redis 有一个模块池,为 时间序列、图 和 搜索 添加了新的完整功能。要了解有关 Redis Streams 的更多信息,请查看此 由 Antirez 撰写的介绍性博客文章,以及 官方文档。但不要忘记,流不是每个工作的正确工具:有时您需要发布/订阅,或者仅仅是 Redis 列表(或有序集合,Redis 也有这个功能)上的简单阻塞操作。