Stream 客户端连接
概述
本指南是关于 Streams 主指南 的配套文档,介绍了 RabbitMQ Stream Protocol 客户端如何连接到集群以从流中消费和向流中发布消息。
Stream Protocol 与 RabbitMQ 支持的其他协议(如 AMQP 1.0、AMQP 0-9-1、MQTT 和 STOMP)存在重要区别。
对于 Streams,在集群部署涉及容器和负载均衡器等额外层时,理解协议基础知识和客户端库的功能至关重要。
Streams 针对最大吞吐量进行了优化,因此数据局部性和客户端连接的主题变得更加重要,需要详细介绍。
Stream 拓扑及其对发布者和消费者的意义
消息协议客户端 如何连接到集群节点 在集群指南中有所介绍。
Stream 是一个复制且持久化的组成部分,包含一个 **leader**(主成员/副本)和 **followers**(或次要成员/副本)。这些副本分布在 RabbitMQ 集群的多个节点上,如下图所示
只有 leader 处理写操作,例如向流中添加传入消息。Stream 的任何 **成员** — 包括 leader 和任何 follower — 都可以用于读操作,即向客户端应用程序分发(传递)消息。
使用 stream protocol 向流发布消息的应用程序可以连接到集群中的任何节点:消息将自动从处理客户端连接的节点路由到托管 leader 进程的节点。
但是,在这种情况下,如果连接和 stream leader 不在同一节点上,流量路由将不是最优的。为了获得最佳的数据局部性和效率,向流发布消息的应用程序应连接到托管 stream leader 的节点,以避免额外的网络跳数。
消费者
消费应用程序的行为有所不同。使用 RabbitMQ Stream Protocol,消息通过 sendfile 系统调用分发(传递)给应用程序:包含消息的文件块直接从节点文件系统发送到网络套接字,而无需经过用户空间。
此优化对于 stream 效率至关重要。但是,这也要求消费应用程序连接的节点托管 stream 的成员。该成员是 leader 还是副本并不重要,只要数据位于文件系统上,就可以由内核执行 sendfile 系统调用将其移至套接字。
对于消费应用程序而言,此限制在大多数情况下是可管理的。在上图所示的示例中,每个节点都有一个 stream 成员,因此应用程序可以连接到任何节点进行消费。但是,请考虑一个 5 节点集群,其中 streams 的复制因子为 2:每个 stream 将只在 5 个节点中的 3 个节点上有成员。
在这种情况下,消费应用程序必须适当地选择其连接节点。
发布者和消费者的最佳实践
发布应用程序可以连接到集群中的任何节点,并且始终能够到达 leader 进程。消费应用程序必须连接到托管目标 stream 成员的节点,其中该成员可以是 leader 或 follower。以下最佳实践应尽可能强制执行。
- 发布应用程序应始终连接到托管目标 stream leader 进程的节点。
- 消费应用程序应始终连接到托管目标 stream 副本的节点。
以下图表说明了这些最佳实践。
直接连接到 stream leader 节点可以避免网络跳数,因为发布的消息最终必须到达 leader。对于消费,使用副本可以减轻 leader 的负载,使其能够将更多资源用于处理所有写操作。
这些最佳实践已集成到官方 RabbitMQ Stream Protocol 客户端库 中,避免这些细节使应用程序代码复杂化。
- 发布应用程序应始终连接到托管目标 stream leader 进程的节点。
- 消费应用程序应始终连接到托管目标 stream 副本的节点。
Stream protocol 允许客户端库(和应用程序)通过 metadata 命令发现给定 stream 的拓扑。
集群节点之间的 Stream 分布
在检查 stream protocol 的 metadata 命令之前,了解 streams 如何在 RabbitMQ 集群节点之间分布很重要。一个 stream 有一个位于某个节点上的 leader Erlang 进程,以及位于其他节点上的副本 Erlang 进程。对于多个 streams,leader 和 follower 进程分布在集群节点之间。
除了单节点集群外,没有单个 RabbitMQ 节点应该托管所有 stream leader。
一组 stream 成员(副本)可以被视为 RabbitMQ 集群中的一个小集群,如下图所示,其中包含多个 streams。
leader 在集群中的分布取决于 stream 声明时生效的 leader 选举策略。
使用 metadata 命令发现 Stream 拓扑
stream protocol 提供了一个 metadata 命令,允许客户端查询一个或多个 streams 的拓扑。对于每个查询的 stream,响应包含托管 leader 和副本的节点的 主机名和端口。
以下图表说明了已连接到其中一个节点的客户端应用程序如何发现给定 stream 的拓扑。
一种常见的模式是向客户端库提供一个或多个节点端点,然后在连接后使用 metadata 命令发现目标 stream 的拓扑,然后根据操作(发布或消费)连接到相应的节点。
metadata 命令对于客户端库强制执行上述最佳实践至关重要。
不幸的是,默认返回的元数据并不总是准确的,或者至少不够准确,无法让客户端应用程序成功连接。
Metadata 命令的局限性
RabbitMQ streams 将返回每个节点的 主机名 作为 host 元数据(更具体地说,是节点名称的主机部分,即 rabbit@{hostname} 中的 {hostname} 部分)。只要客户端可以解析目标节点的主机名,这就能正常工作。
然而,当 RabbitMQ 节点部署在容器化环境中时,主机名可能模棱两可,并且可能无法在部署了应用程序的宿主机上解析。
下图说明了一个 3 节点 RabbitMQ 集群,其中节点是运行在不同 VM 上的容器。如果端口映射正确,客户端应用程序可以连接到节点,但无法通过容器的主机名进行连接。
启用了 stream 插件的 RabbitMQ 节点会尽力而为,但它无法知道客户端可以或不可以解析哪些主机名,以及原因。幸运的是,可以配置节点在被要求提供 metadata 命令的“坐标”时返回什么值。
调整 Metadata 命令:广告主机和端口
应使用 stream 插件的 advertised_host 和 advertised_port 配置条目 来指定节点在被询问如何被联系时返回的值。插件将按原样返回这些值,不做任何验证。DNS 设置必须允许客户端应用程序使用这些配置的值连接到节点。实际上,这意味着覆盖的广告主机名必须是稳定的,并且可以被应用程序主机解析。
advertised_host 和 advertised_port 设置应解决客户端应用程序由于使用默认广告的主机名而无法连接到节点的问题。在部署带有容器化节点和 streams 的 RabbitMQ 集群时,考虑这些设置非常重要。
当 RabbitMQ 节点使用应用程序无法解析的主机名时,使用 advertised_host 和 advertised_port 设置变得至关重要。
仍然存在一个常见的用例,这种发现机制可能存在问题:当负载均衡器位于客户端应用程序和集群节点之间时。
连接到负载均衡器后面的节点
在 RabbitMQ 集群前面放置负载均衡器是一种常见场景。负载均衡器可能会使上面概述的数据局部性问题变得更糟。幸运的是,存在解决方案。
当在负载均衡器中使用 metadata 命令时,会出现问题:客户端将收到节点信息并使用它 **直接** 连接到节点,从而绕过负载均衡器。下图说明了这种情况。
这种行为通常是不希望的。
不建议将 advertised_host 和 advertised_port 配置条目设置为使用负载均衡器信息,以便客户端应用程序始终连接到负载均衡器。
这种方法无法强制执行最佳实践(发布到 leader,从副本消费),并且在 streams 未在所有节点上部署的部署中,如果应用程序连接到没有 stream 成员的节点,消费将失败。
客户端库可以实现一种变通方法来解决这个问题。
客户端使用负载均衡器的变通方法
客户端应用程序可以通过以下方法始终连接到负载均衡器并最终连接到适当的节点:
- 使用
metadata命令,但 **故意忽略** 已发现的结果,并始终连接到负载均衡器。 - 重试连接,直到连接到适当的节点。
节点“坐标”(主机名和端口,或如果已配置则为 advertised_host 和 advertised_port)可在 stream 协议连接中获得。客户端应用程序可以确定它连接到哪个节点。
这意味着在 使用负载均衡器时 不应配置 advertised_host 和 advertised_port。在这种情况下,metadata 命令返回的节点的“坐标”不用于连接,因为客户端始终连接到负载均衡器。它们用于 **关联** 负载均衡器提供的连接与客户端期望的节点,主机名足以满足此目的。
这意味着在使用负载均衡器时 不应配置 advertised_host 和 advertised_port。
考虑以下场景
- 发布应用程序通过
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 副本的节点。
- 客户端应用程序必须使用
metadatastream protocol 命令 来了解它们想要交互的 stream 的拓扑。 - Stream Java 和 Go 客户端强制执行这些最佳实践。
metadata命令默认返回节点的 主机名和监听端口,这在容器化环境中可能存在问题。advertised_host和advertised_port配置条目 允许指定节点为metadata命令返回的值。- 负载均衡器可能会混淆客户端库,使其试图绕过它直接连接到节点。
- 客户端库可以提供一种变通方法来与负载均衡器正常工作。
- Stream Java 和 Go 客户端实现了此类变通方法。