RabbitMQ的脑裂踩坑 - ryanrodemoyer


我的手表嗡嗡作响,在黎明前的昏迷中,我无法辨认这是警报还是电话。时间是凌晨 4 点 45 分:我们最大的客户报告说他们的请求需要两个多小时才能返回结果。我们认为这是因为我们的RabbitMQ消息系统。

近三年来,我们一直在为我们的生产系统运行 RabbitMQ,并且 99.5% 的时间完全没有问题。在那段时间里,我们已经扩展到 200 多个并发消费者,在十几个虚拟机上运行,​​同时协调消息处理(1 个队列到 N 个消费者),并在我们的 .NET 应用程序中处理了数亿条消息。

我们的主要用例是对另一个 Web 服务进行 HTTP 调用,以检索 JSON 数据或下载 PDF 文档。我会告诉你我推荐 RabbitMQ,那是因为我推荐。在大多数情况下,使用它非常棒,并且在我们的应用程序中表现良好。
但是,这里有一个很大的问题,所有这些都付出了我们在做出架构决策时不知道的代价。

RabbitMQ 是我们检查作业结果的轮询架构的骨干。典型的动作顺序是:用户通过 Web 应用程序提交请求,后端通过向 RabbitMQ 添加消息来处理该消息。消费者获得该消息并对另一个 Web 服务进行 HTTP 调用以实际提交请求。从那里开始,轮询逻辑接手,队列上的后续消息每条都代表了一次轮询尝试,以检索出结果。如果一项工作没有结果,消费者就会把消息放回队列,这样我们就可以把下一次轮询尝试延迟一定的时间(客户可配置)。我们的延迟逻辑使用了一个具有生存时间(TTL)和死信定义的队列网络。

我们的非开发集群使用两个或三个节点,而生产集群使用三个节点。每个集群都有一个负载平衡器,应用程序严格来说只针对负载平衡器。在运行时,发布者和消费者使用同一个负载平衡器。

在实施三年后,这是我在编写与 RabbitMQ 交互的任何一行代码之前会告诉自己的。

在开始时聘请一位专家
大概花费 2000-3000 美元(猜测),您可以聘请一家 RabbitMQ 咨询公司并获得与专家交流的时间。利用这个机会来审查和验证您的假设、计划、提问、获得建议并进行尽职调查,这样您就可以通过现在做出正确的决定来减少未来的麻烦和问题,并很可能在长期内节省资金。或者你可以采取我们的方法,在情况不妙的时候请专家帮忙。

使用像EasyNetQ或NServiceBus这样的库
我们的应用程序使用 RabbitMQ 的 RabbitMQ.Client 库,这些抽象库(如 EasyNetQ、NServiceBus)也使用它。然而,他们比我更出色,在与 RabbitMQ 在如此低的水平上进行交互方面,他们比我知道得多。来自 RabbitMQ 的驱动程序是低级的、原始的,并期望您了解有关 RabbitMQ 的细微差别。如果这是你第一次使用 RabbitMQ,那么我保证你将没有经验来理解这种细微差别。

在你问 "你为什么不使用包装库?"之前,让我告诉你。在我的案例中,我们的 RabbitMQ 项目在实施接近尾声时落到了我的头上,原开发人员离开了公司,他决定直接使用 RabbitMQ.Client 库。我没有足够的时间来进行这种交换(我也不知道我应该提出一个交换包装库的理由!)。

有这个网络分区的事情,它是一种大问题
就通用术语而言,你的 RabbitMQ 系统被称为一个集群。一个集群是由一个或多个节点组成的。一个节点只是一个运行 RabbitMQ 软件的服务器/容器。集群中的所有节点必须运行完全相同的 RabbitMQ 版本。

RabbitMQ 提供了一种称为集群的机制,因此您可以将其他 RabbitMQ 实例连接起来,以便它们作为单个逻辑代理一起运行。您可以向集群中的任何节点发出任何请求,这些节点将合作发布消息或将消息发送到消费者手中。

节点之间通过交换关于消息、队列、交换等的数据不断进行通信。如果(以及何时)该通信被中断,即使只有几毫秒,那么 RabbitMQ 将进入分区状态并查看配置文件以确定如何处理该通信中断。
默认的分区处理策略是ignore ,这意味着直接进入分区状态,并在这种 "分裂的大脑 "模式下继续前进,从而将您的集群推入完全混乱的状态。这对我们来说是地狱。退出分区的唯一方法是重新启动分区一侧的节点,这样它就会重新加入另一侧并承担他们的数据,从而丢弃它自己在集群被分区时积累的数据集。

我曾亲身经历过网络分区以两种方式发生:集群中的所有节点通过Windows更新和防火墙规则同时被更新。对Windows更新的修复是确保集群中的节点在不同时间打补丁。

我必须停下自己的脚步,因为我可以继续就这一话题滔滔不绝地说上无数的话。正确的配置是将partion_handling策略设置为pause_minority。当集群被分区时,分区的一边将简单地关闭自己,从而完全避免了大脑分裂的情况发生。关闭的那一边将继续监视集群的恢复通信,并在那时重新加入自己。现在你所要做的就是确保你的代码正确地处理断开的连接,你将拥有一个相当强大的队列解决方案。

从CAP规范来看,ignore 意味着以牺牲一致性获得可用性,而pause_minority是以牺牲可用性代价来获得一致性。如果你问我的话,后者是相当值得的。

您打算如何升级 RabbitMQ 版本?
当您的 RabbitMQ 版本已达到寿命终点时,这一天将会到来。然后您会怎么做?继续运行不支持的版本?创建一个新的集群?您将有什么计划将流量从旧集群迁移到新集群?回顾我的说明(上文),集群中的所有节点都需要运行相同的确切版本。希望你能明白,如果你的计划是就地升级节点,这将是很棘手的。

我留给你的只有问题,没有答案。这是因为每个决定都高度依赖于你的组织和运营策略。换句话说,每个人可能有一点不同的方法来解决这些问题。

如果您丢失了 RabbitMQ 中的所有消息,您有什么计划?
如果您丢失 RabbitMQ 中的所有(甚至三分之一)消息,您会有多大的损失?RabbitMQ 是您的记录系统吗?您是否有一个恢复策略来使您的应用程序恢复到正常状态?当您将您的内部服务器转移到云端时会发生什么 - 您如何让您的 RabbitMQ 消息再次流动起来?

构建您的应用程序以支持发布者和消费者的不同连接地址
在未来的某个时间点(也许是在升级期间),您将希望能够灵活地从不同的集群和/或负载均衡器独立发布和消费。这是一个零风险高回报的模式,你可以尽早在你的应用程序中建立,你会在未来拍拍屁股走人。

日志文件将增长到消耗几十千兆字节的磁盘空间
随着时间的推移,来自 RabbitMQ 的日志文件将增长到消耗数千兆字节的磁盘空间。使用 rabbitmqctl rotate_logs 轮回记录这些文件很容易,但要努力实现流程自动化,以便 "磁盘空间耗尽 "不会导致中断。

Reddit:
1、 RabbitMQ 是一个很棒的工具包,根据用例,它可能是您组织的最佳通用消息传递解决方案。即使在云部署中,我可能更喜欢在容器或 VM 中运行 RabbitMQ,而不是运行 AQS 或 Azure ServiceBus 之类的东西,如果没有其他原因,只是因为您可以在本地开发机器上快速轻松地运行 rabbit 以进行测试。
也就是说,RabbitMQ 有点特殊。由于消息传递领域的特定需求或 Erlang 的特定语义,Rabbit 中的工作方式往往与您的直觉所暗示的有所不同。集群中的一个示例,其中队列(可能)仅存在于一台机器上,因此如果分区将该机器排除在集群之外,则这些队列根本不可用。也就是说,您可以与看起来健康的节点建立活动连接,但仍然无法发送或接收消息,因为您未连接的另一个节点处于脱机状态。(您可以通过队列复制在一定程度上改善这一点,但这并非在所有队列上都可用,默认情况下也不可用)。
正确配置和调整 Rabbit 是战斗的重要组成部分,确保您有足够的受过培训的员工来解决常见错误是另一回事。

2、我已经运行RabbitMQ十多年了,有很多事情我希望在我接受它之前有人告诉我,但这些事情都不是那些。(从规模上看,我没有处理任何疯狂的事情,也许每天有5千万条消息,有大约90个队列和大约300个消费者在一个本地数据中心环境中。)

像这样的事情,集群对于并发和性能来说比它对于HA来说更有用。我最终编写了客户端连接库,它将连接到几个独立的 RabbitMQ 服务器。订阅者将同时订阅所有的服务器。编写者将随机写到任何一个服务器上。这些库将处理断开连接等问题。因此,升级到一个新的 RabbitMQ 服务器只是一次升级一个的问题,只要至少有一个服务器是有效的,客户端就可以继续运行。

像这样的事情,你不能期望消息以特定的顺序被处理,因此,以这样一种方式设置你的队列是很重要的,你会得到你所期望的东西。为了简单起见,我倾向于画一个消息处理流程图,并为每个步骤使用一个单独的队列,而不是试图在每个消息中编码状态或使用订阅者/消费者键来引导消息。这种设计的简单性使得在出现问题时更容易处理。

例如,当你有一个会使消费者崩溃的消息,而你使用的是手动ack,该消息会使_所有的消费者崩溃,直到你修复导致崩溃的东西或手动从队列中删除该消息。(我称这些消息为 "毒药"。)

像这样的事情,消费者真的应该正确地关闭他们的连接,当有人忘记这样做的时候,你可以很快地用完插座。

除非你在一个资源受限的环境中工作,否则像这样的事情你现在并不真正需要队列。在现代的云环境中,你可以直接发射lambdas进行异步处理,而忘记队列管理。

综上所述,数据库不是队列,我不会将它们互换使用。


3、我在生产环境中运行 rabbitmq 已经 6 年了,上周才第一次中断。服务器的正常运行时间超过 800 天(当然,它与公共 Internet 隔离了防火墙)。另一方面,我有几十次 Postgres 中断。并不是说 Postgres 不可靠,它坚如磐石,但我们比 RMQ 更努力地推动它。我的观点很简单,RabbitMQ 真的不难维护和使用。