连接到 Streams
RabbitMQ Streams 概述 介绍了 Streams,RabbitMQ 3.9 中的一项新功能。这篇文章涵盖了当 stream 协议 被使用时,客户端应用程序应如何连接到 RabbitMQ 节点,以从 Streams 中获得最大收益。
Streams 针对高吞吐量场景进行了优化,这就是为什么像数据局部性这样的技术细节对于充分利用您的 RabbitMQ 集群至关重要。客户端库可以处理大部分细节,但是当设置涉及容器和负载均衡器等额外层时,对底层工作原理的基本理解至关重要。如果您想了解更多关于 Streams 的信息,并避免在部署您的第一个 stream 应用程序时遇到一些麻烦,请继续阅读!
Stream 的拓扑结构
一个 stream 是复制和持久化的,它由一个leader Erlang 进程和 replica Erlang 进程组成。这些进程分布在 RabbitMQ 集群的节点上,如下图所示
只有 leader 进程处理写入操作,例如入站消息。stream 的任何成员 – leader 和副本 – 都可以向客户端应用程序分发消息。
使用 stream 协议 发布到 stream 的应用程序可以连接到集群中的任何节点:消息将自动从连接节点发送到托管 leader 进程的节点。如果连接和 stream leader 不在同一个节点上,那么操作不是最优的,这不需要博士学位也能理解。发布到 stream 的应用程序应连接到托管 stream leader 的节点,以避免额外的网络跳跃。
对于消费应用程序来说,情况略有不同。使用 stream 协议,消息通过 sendfile
分发给应用程序:包含消息的文件块直接从节点文件系统发送到网络套接字,而无需经过用户空间。这种优化要求消费应用程序连接的节点必须托管 stream 的成员。无论该成员是 leader 还是副本都无关紧要,只要数据在文件系统中,准备好通过网络套接字使用 sendfile
即可。
对于消费应用程序来说,这个约束似乎不太苛刻:在上图中,每个节点都有 stream 的成员,因此应用程序可以连接到任何节点进行消费。但是想象一下一个 5 节点集群,streams 使用 2 的复制因子:每个 stream 将只在 5 个节点中的 3 个节点上拥有成员。在这种情况下,消费应用程序必须适当地选择它们的节点。
考虑到这些知识,现在让我们尝试提出一些最佳实践。
行为良好的客户端
因此,发布应用程序可以连接到集群的任何节点,它将始终到达 leader 进程。消费应用程序必须连接到托管目标 stream 成员的节点,并且该成员可以是 leader 或副本。客户端应用程序可以坚持这些规则,但应尽可能强制执行以下最佳实践
- 发布应用程序应始终连接到托管目标 stream 的 leader 进程的节点.
- 消费应用程序应始终连接到托管目标 stream 副本的节点.
下图说明了这些最佳实践
如前所述,直接连接到 stream leader 的节点可以避免网络跳跃,因为消息无论如何都必须到达 leader 进程。那么总是使用副本进行消费呢?嗯,这只是卸载了 leader 进程的负担,它已经很忙于处理所有的写入操作了。
这些最佳实践应该理想地由客户端库强制执行,所有这些技术细节都不应该泄漏到应用程序代码中。接下来我们将看到 stream 协议如何允许客户端应用程序了解给定 stream 的拓扑结构。
跨集群的 Streams
在继续讨论 stream 协议的 metadata
命令之前,让我们澄清 streams(复数)如何在 RabbitMQ 集群的节点之间分布。我们提到一个 stream 有一个 leader Erlang 进程位于一个节点上,而副本 Erlang 进程位于其他节点上。对于多个 streams,所有这些 Erlang 进程(可以将 Erlang 进程视为“非常轻量级的线程”)分布在集群节点上,并且在任何时候,给定的 RabbitMQ 节点都不应该托管所有的 stream leaders。
可以将一个 stream 视为 RabbitMQ 集群中的一个小集群,如下图所示,其中有多个 streams
leaders 在集群中分布的方式取决于创建 stream 时的“leader 定位策略”。默认策略是“最少 leaders”,这意味着为新 stream 的 leader 选择 stream leaders 数量最少的节点。还有其他策略,但涵盖它们超出了本文的范围。
考虑到这些澄清,让我们继续讨论如何找出 streams 的拓扑结构。
metadata
命令
stream 协议提供了一个 metadata
命令,用于查询一个或多个 streams 的拓扑结构。对于每个查询的 stream,响应包含托管 leader 和副本的节点的主机名和端口。
下图说明了已经连接到其中一个节点的客户端应用程序如何找出给定 stream 的拓扑结构
因此,一个常见的模式是为客户端库提供一个或多个入口点,在连接到后使用 metadata
命令来了解目标 stream 的拓扑结构,然后根据操作(发布或消费)连接到适当的节点:
metadata
命令是客户端库强制执行上述最佳实践的关键。
不幸的是,默认返回的 metadata 并不总是准确的,或者至少对于客户端应用程序连接来说不够准确。让我们看一个例子。
默认 Metadata 的局限性
stream 插件返回每个节点的主机名作为 host metadata(更准确地说,这是 Erlang 节点名称的主机部分,但这在这里并不重要)。这很好……只要客户端可以解析主机名以连接到节点!
在容器时代,主机名可能是一个模糊的概念,一旦您离开容器,它就没有太多意义了。下图说明了一个 3 节点 RabbitMQ 集群,其中节点是运行在不同虚拟机上的容器。如果端口映射正确,客户端应用程序可以连接到节点,但是它无法通过使用容器的主机名来实现连接。
stream 插件尽了最大努力,但它在这里无法创造奇迹。幸运的是,可以配置当节点被询问其用于 metadata
命令的“坐标”时,节点返回什么。
广告主机和端口
stream 插件的 advertised_host
和 advertised_port
配置条目 可以设置为告诉节点当被问及如何联系时返回什么。这里没有技巧:插件只会信任进行配置的操作员,并盲目地返回这些值。这意味着客户端应用程序必须能够使用此信息连接到节点。
如果客户端应用程序由于不正确的 stream metadata 而无法连接到节点,advertised_host
和 advertised_port
设置应该有助于解决所有麻烦。如果您部署了带有容器化节点的 RabbitMQ 集群并且您使用了 streams,请始终记住它们。
仍然有一种常见的用例,其中这种发现机制可能会出现问题:当负载均衡器位于客户端应用程序和集群节点之间时。
使用负载均衡器
在 RabbitMQ 集群前面放置负载均衡器是一种常见的场景。由于 streams 的工作方式,这可能会导致一些问题,但总是有解决方案的。如果我们回到 metadata 命令,但使用负载均衡器,事情就会出错:客户端将收到节点信息,并使用它直接连接到节点,绕过负载均衡器。下图说明了这种不幸的情况
这可能不是您想要的。
将 advertised_host
和 advertised_port
配置条目设置为使用负载均衡器信息怎么样?这样客户端应用程序将始终连接到负载均衡器!这不是一个好主意,因为我们将无法强制执行最佳实践(发布到 leader,从副本消费),并且在 streams 不在所有节点上的部署中,如果应用程序最终连接到没有 stream 成员的节点,消费将会失败。
好的,这有点令人沮丧,但振作起来,因为客户端库可以实现一种解决方法来解决这个问题。
使用负载均衡器的客户端解决方法
客户端应用程序仍然可以始终连接到负载均衡器,并最终连接到适当的节点。它是如何做到的?两个步骤
- 使用
metadata
命令,但忽略信息并始终连接到负载均衡器 - 重试连接,直到连接到适当的节点
您可能想知道一旦建立连接,如何找到节点的信息?节点的“坐标”(主机名和端口,或者如果配置了 advertised_host
和 advertised_port
)在 stream 协议连接中可用。因此,客户端应用程序可以知道它连接到哪个节点。
这意味着当使用负载均衡器时,不应配置 advertised_host
和 advertised_port
。 在这种情况下,metadata
命令返回的节点的“坐标”不用于连接,因为客户端始终连接到负载均衡器。它们用于关联负载均衡器提供的连接与客户端期望的节点,而主机名非常适合用于此目的。
让我们举个例子
- 发布应用程序通过
metadata
请求的响应知道其目标 stream 的 leader 在node-1
上 - 它使用负载均衡器地址创建一个新连接
- 负载均衡器选择连接到
node-3
- 连接已正确建立,但客户端应用程序发现它连接到
node-3
,它立即关闭连接并重试 - 这次负载均衡器选择
node-1
- 应用程序对其连接到的节点感到满意,它继续使用此连接进行发布
下图说明了这个过程
由于 stream 连接是长连接,并且 stream 应用程序不应该有大量的连接抖动,因此重试连接在这里不是问题。
此解决方案还假设负载均衡器不会始终连接到相同的后端服务器。轮询是这种情况下的适当负载均衡策略。
现在也应该清楚,当使用这种技术时,设置 advertised_host
和 advertised_port
不是必要的,并且将它们设置为所有节点的负载均衡器坐标是一个坏主意。让每个节点返回其主机名在这里是很好的,因为主机名应该在网络中是唯一的。
因此,责任在于客户端库。现在让我们看看如何在 stream Java 客户端中实现这一点。
将 Stream Java 客户端与负载均衡器一起使用
stream Java 客户端 提供了 AddressResolver
扩展点。每当创建新连接时都会使用它:从传入的 Address
(根据 metadata
查询要连接的节点),地址解析器可以提供一些逻辑来计算要使用的实际地址。默认实现只是返回给定的地址。如果您想在使用负载均衡器时实现上述解决方法,请始终返回负载均衡器的地址,如下面的代码片段所示
Address entryPoint = new Address("my-load-balancer", 5552);
Environment environment = Environment.builder()
.host(entryPoint.host())
.port(entryPoint.port())
.addressResolver(address -> entryPoint)
.build();
stream PerfTest 工具 在启用 --load-balancer
选项时也支持此模式。以下是如何告诉该工具始终对发布者和消费者连接使用相同的入口点
# with the Java binary
java -jar stream-perf-test.jar --uris rabbitmq-stream://my-load-balancer:5552 --load-balancer
# with Docker
docker run -it --rm pivotalrabbitmq/stream-perf-test --uris rabbitmq-stream://my-load-balancer:5552 --load-balancer
总结
这篇文章涵盖了客户端应用程序在使用 stream 协议时应该如何连接。以下是概要
- 发布应用程序应连接到托管目标 stream 的 leader 的节点
- 消费应用程序应连接到托管目标 stream 副本的节点
- 客户端应用程序必须使用
metadata
stream 协议命令 来了解它们想要交互的 streams 的拓扑结构 - stream Java 和 Go 客户端强制执行这些最佳实践
metadata
命令默认返回节点主机名和监听器端口,这可能会有问题,例如当使用容器时advertised_host
和advertised_port
配置条目 允许指定节点应该为metadata
命令返回哪些值- 负载均衡器可能会混淆客户端库,客户端库会尝试绕过它以直接连接到节点
- 客户端库可以提供一种解决方法,以便与负载均衡器正常工作
- stream Java 和 Go 客户端实现了这样的解决方法