流式客户端连接
概述
作为流指南主文档的配套指南,本文档介绍了 RabbitMQ 流协议客户端如何连接到集群以消费和发布流数据。
流协议与 RabbitMQ 支持的其他协议(如 AMQP 1.0、AMQP 0-9-1、MQTT 和 STOMP)存在重要区别。
在使用流时,如果集群部署涉及容器和负载均衡器等额外层,了解协议的基础知识及客户端库的功能至关重要。
流经过优化以实现最大吞吐量,因此数据局部性和客户端连接的主题在详细说明中显得尤为重要。
流拓扑及其对发布者和消费者的意义
消息协议客户端如何连接到集群节点在集群指南中有详细介绍。
流是可复制且持久化的,由一个领导者(主成员/副本)和多个跟随者(或从成员/副本)组成。这些副本分布在 RabbitMQ 集群的多个节点上,如下图所示
只有领导者处理写操作,例如将入站消息添加到流中。流的任何成员(包括领导者和任何跟随者)都可以用于读操作,即向客户端应用程序分发(调度)消息。
使用流协议向流发布数据的应用程序可以连接到集群中的任何节点:消息将自动从处理客户端连接的节点路由到托管领导者进程的节点。
然而,如果连接的节点与流领导者所在的节点不是同一个,流量路由将不是最优的。为了获得最佳的数据局部性和效率,向流发布的应用程序应连接到托管该流领导者的节点,以避免额外的网络跳转。
消费者
消费应用程序的行为有所不同。使用 RabbitMQ 流协议时,消息通过 sendfile 系统调用进行交付(调度):包含消息的文件块直接从节点文件系统发送到网络套接字,而无需经过用户空间。
这种优化对于流效率至关重要。但是,它也要求消费应用程序连接的节点必须托管流的一个成员。该成员是领导者还是副本并不重要,只要数据在文件系统上,且内核能够通过执行 sendfile 系统调用将其移动到套接字即可。
消费应用程序的这一约束在大多数情况下是可管理的。在上图中,每个节点都有流的一个成员,因此应用程序可以连接到任何节点进行消费。但是,考虑一个使用复制因子为 2 的流的 5 节点集群:每个流仅在 5 个节点中的 3 个节点上有成员。
在这种情况下,消费应用程序必须适当地选择其连接节点。
发布者和消费者的最佳实践
发布应用程序可以连接到集群的任何节点,并且总是能到达领导者进程。消费应用程序必须连接到托管目标流成员的节点,该成员可以是领导者,也可以是跟随者。在可能的情况下,应强制执行以下最佳实践
- 发布应用程序应始终连接到托管目标流领导者进程的节点
- 消费应用程序应始终连接到托管目标流副本的节点
下图说明了这些最佳实践
直接连接到流领导者节点避免了网络跳转,因为发布的消息最终必须到达领导者。使用副本进行消费可以减轻领导者的负载,使其能投入更多资源处理所有写操作。
这些最佳实践已集成到官方 RabbitMQ 流协议客户端库中,使这些细节不会让应用程序代码变得复杂。
- 发布应用程序应始终连接到托管目标流领导者进程的节点
- 消费应用程序应始终连接到托管目标流副本的节点
流协议允许客户端库(和应用程序)通过元数据命令发现给定流的拓扑结构。
流在集群节点间的分布
在检查流协议的 metadata 命令之前,了解流如何分布在 RabbitMQ 集群的节点上非常重要。流拥有一个位于一个节点上的 Erlang 领导者进程,以及位于其他节点上的 Erlang 副本进程。对于多个流,领导者和跟随者进程会分散在集群节点中。
除单节点集群外,不应由单个 RabbitMQ 节点托管所有流领导者。
一组流成员(副本)可以被视为 RabbitMQ 集群内的一个小型集群,如下图中所示的多个流所示
领导者在集群中的分布取决于声明流时所采用的领导者定位策略。
使用 metadata 命令发现流拓扑
流协议提供了一个 metadata 命令,允许客户端查询一个或多个流的拓扑结构。对于每个被查询的流,响应都包含托管领导者和副本的节点的主机名和端口。
下图说明了已经连接到其中一个节点的客户端应用程序如何发现给定流的拓扑结构
一种常见的模式是向客户端库提供一个或多个节点端点,连接后使用 metadata 命令发现目标流的拓扑结构,然后根据具体操作(发布或消费)连接到适当的节点。
metadata 命令对于客户端库强制执行上述最佳实践至关重要。
遗憾的是,在所有默认设置下返回的元数据并不总是准确的,或者至少不足以让客户端应用程序成功连接。
Metadata 命令的局限性
RabbitMQ 流将返回每个节点的主机名作为主机元数据(更具体地说,是节点名称的主机部分,即 rabbit@{hostname} 中的 {hostname} 部分)。只要客户端能够解析目标节点的主机名,此方法就有效。
但是,当 RabbitMQ 节点部署在容器化环境中时,主机名可能会产生歧义,并且可能无法在部署应用程序的主机上解析。
下图展示了一个 3 节点 RabbitMQ 集群,其中节点是运行在不同虚拟机上的容器。如果端口映射正确,客户端应用程序可以连接到这些节点,但无法使用容器的主机名进行连接。
启用了流插件的 RabbitMQ 节点会尽力而为,但它无法知道客户端可以或无法解析哪些主机名,以及原因。幸运的是,可以配置节点在处理 metadata 命令请求时返回其“坐标”。
调整 Metadata 命令:广播主机和端口
应使用流插件的 advertised_host 和 advertised_port 配置项来指定节点在被询问如何联系时应返回的内容。插件将原样返回这些值,不进行任何验证。DNS 设置必须允许客户端应用程序使用这些配置的值连接到节点。在实践中,这意味着覆盖后的广播主机名必须是稳定的,并且可由应用程序主机解析。
advertised_host 和 advertised_port 设置应能解决因客户端应用程序使用默认广播主机名而导致的连接问题。在部署带有容器化节点和流的 RabbitMQ 集群时,考虑这些设置非常重要。
当 RabbitMQ 节点使用应用程序无法解析的主机名时,使用 advertised_host 和 advertised_port 设置就变得至关重要。
这种发现机制在一种常见用例中仍可能存在问题:当负载均衡器位于客户端应用程序和集群节点之间时。
连接到负载均衡器背后的节点
在 RabbitMQ 集群前端使用负载均衡器是一种常见场景。负载均衡器可能会加剧上述数据局部性问题。幸运的是,解决方案是存在的。
在使用负载均衡器的同时使用 metadata 命令时,会出现问题:客户端将接收到节点信息并使用它直接连接到节点,从而绕过负载均衡器。下图说明了这种情况
这种行为通常是不希望出现的。
不建议将 advertised_host 和 advertised_port 配置项设置为使用负载均衡器信息,从而使客户端应用程序始终连接到负载均衡器。
这种方法会阻碍最佳实践的强制执行(发布到领导者,从副本消费),并且在流未部署在所有节点上的部署中,如果应用程序连接到没有流成员的节点,消费将失败。
客户端库可以实现变通方案来解决此问题。
使用负载均衡器的客户端变通方案
客户端应用程序可以始终连接到负载均衡器,并通过以下方法最终连接到适当的节点
- 使用
metadata命令,但刻意忽略发现的结果,始终连接到负载均衡器 - 重试连接,直到连接到适当的节点
节点的“坐标”(主机名和端口,或配置后的 advertised_host 和 advertised_port)在流协议连接中可用。客户端应用程序可以确定其连接到了哪个节点。
这意味着在使用负载均衡器时,不应配置 advertised_host 和 advertised_port。在这种情况下,metadata 命令返回的节点“坐标”不用于连接,因为客户端总是连接到负载均衡器。它们仅用于关联负载均衡器提供的连接与客户端期望的节点,而主机名对于此目的已足够。
这意味着在使用负载均衡器时,不应配置 advertised_host 和 advertised_port。
考虑以下场景
- 发布应用程序通过
metadata请求的响应得知其目标流的领导者在node-1上 - 它使用负载均衡器地址创建新连接
- 负载均衡器选择连接到
node-3 - 连接已正常建立,但客户端应用程序发现它连接到了
node-3,它会立即关闭连接并重试 - 负载均衡器在下一次尝试时选择
node-1 - 应用程序连接到正确的节点,并使用此连接继续发布
下图说明了此过程
由于流连接通常是长连接,且流应用程序通常没有严重的连接流失,因此重试连接不会导致高连接流失场景,无需担忧。
此解决方案假设负载均衡器不会总是连接到同一个后端服务器。轮询(Round robin)是此类情况下的适当平衡策略。
使用此技术时无需设置 advertised_host 和 advertised_port,且将所有节点设置为负载均衡器坐标可能难以或无法实现。在这里允许每个节点返回其主机名是合适的,因为主机名在网络中应该是唯一的。
此责任在于客户端库。下一节描述如何使用流 Java 客户端实现这一点。
在流 Java 客户端中使用负载均衡器
流 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();
当启用 --load-balancer 选项时,流 PerfTest 工具也支持此模式。以下命令配置该工具,使其始终为发布者和消费者连接使用相同的入口点
# 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
最佳实践总结
使用流协议连接的客户端应用程序应遵循以下准则
- 发布应用程序应连接到托管目标流领导者的节点
- 消费应用程序应连接到托管目标流副本的节点
- 客户端应用程序必须使用
metadata流协议命令来了解它们想要交互的流的拓扑结构 - 流的 Java 和 Go 客户端强制执行这些最佳实践
metadata命令默认返回节点的主机名和监听端口,这在容器化环境中可能会产生问题advertised_host和advertised_port配置项允许指定节点应为metadata命令返回的值- 负载均衡器可能会混淆尝试绕过它并直接连接到节点的客户端库
- 客户端库可以提供变通方案以与负载均衡器正常工作
- 流的 Java 和 Go 客户端实现了此类变通方案