容错和高可用性是重要主题,因此我们将专门撰写文章来介绍 RabbitMQ 和 Kafka。 这篇文章是关于RabbitMQ的,下一篇是关于Kafka的,与RabbitMQ进行比较。 这是一篇很长的文章,所以请放心。
让我们看看容错、一致性和高可用性 (HA) 策略以及每种策略所做的权衡。 RabbitMQ 可以在节点集群上运行 - 因此被归类为分布式系统。 说到分布式系统,我们经常谈论一致性和可用性。
这些概念描述了系统发生故障时的行为方式。 网络连接故障、服务器故障、硬盘故障、由于垃圾收集、数据包丢失或网络连接速度减慢而导致服务器暂时不可用。 所有这些都可能导致数据丢失或冲突。 事实证明,建立一个对于所有故障场景都完全一致(无数据丢失、无数据分歧)且可用(将接受读取和写入)的系统几乎是不可能的。
我们将看到一致性和可用性处于相反的两端,您需要选择优化哪种方式。 好消息是,使用 RabbitMQ,这种选择是可能的。 您可以使用这些“书呆子”杠杆来将平衡转向更高的一致性或更高的可访问性。
我们会特别关注哪些配置会导致确认记录导致数据丢失。 出版商、经纪人和消费者之间存在一条责任链。 一旦消息被传输到经纪人,他的工作就是不丢失消息。 当代理确认发布者收到消息时,我们不希望消息丢失。 但我们会看到这实际上可能会发生,具体取决于您的代理和发布商配置。
单节点弹性原语
弹性排队/路由
RabbitMQ 中有两种类型的队列:持久队列和非持久队列。 所有队列都保存在 Mnesia 数据库中。 持久队列在节点启动时重新通告,因此在重新启动、系统崩溃或服务器崩溃时仍能幸存(只要数据被持久化)。 这意味着只要您声明路由(交换)和队列具有弹性,排队/路由基础设施就会恢复在线。
当节点重新启动时,易失性队列和路由将被删除。
持久消息
仅仅因为队列是持久的并不意味着它的所有消息都将在节点重新启动后继续存在。 仅由发布者设置的消息 持续 (执着的)。 持久消息确实会给代理带来额外的负载,但如果消息丢失是不可接受的,那么就没有其他选择。
米。 1. 可持续发展矩阵
使用队列镜像进行集群
为了承受失去经纪人的影响,我们需要冗余。 我们可以将多个 RabbitMQ 节点组合成一个集群,然后通过在多个节点之间复制队列来添加额外的冗余。 这样,如果一个节点发生故障,我们就不会丢失数据并保持可用。
队列镜像:
- 一个主队列(master),接收所有写和读命令
- 从主队列接收所有消息和元数据的一个或多个镜像。 这些镜子不是为了缩放而纯粹是为了冗余。
米。 2.队列镜像
镜像由适当的策略设置。 您可以在其中选择复制系数,甚至可以选择队列应位于的节点。 例子:
ha-mode: all
ha-mode: exactly, ha-params: 2
(一主一镜)ha-mode: nodes, ha-params: rabbit@node1, rabbit@node2
出版商确认
为了实现一致的记录,需要发布者确认。 如果没有它们,消息就有丢失的风险。 消息写入磁盘后,将向发布者发送确认信息。 RabbitMQ 不是在收到消息时才将消息写入磁盘,而是定期(大约几百毫秒)将消息写入磁盘。 当队列被镜像时,只有在所有镜像也将其消息副本写入磁盘后才会发送确认。 这意味着使用确认会增加延迟,但如果数据安全很重要,那么它们是必要的。
故障转移队列
当代理退出或崩溃时,该节点上的所有队列领导者(主节点)都会随之崩溃。 然后,集群选择每个主服务器中最旧的镜像并将其提升为新主服务器。
米。 3. 多个镜像队列及其策略
经纪人 3 宕机了。 请注意,Broker 2 上的队列 C 镜像正在升级为主镜像。 另请注意,已为代理 1 上的队列 C 创建了一个新镜像。RabbitMQ 始终尝试维护策略中指定的复制因子。
米。 4. Broker 3故障,导致队列C故障
下一个经纪人1号陨落! 我们只剩下一名经纪人了。 队列B镜像被提升为主镜像。
图。 5
我们已返回 Broker 1。无论数据在 Broker 丢失和恢复过程中保存得如何,所有镜像队列消息在重新启动时都会被丢弃。 值得注意这一点很重要,因为这会产生后果。 我们很快就会看看这些影响。 因此,Broker 1 现在再次成为集群的成员,并且集群尝试遵守策略,因此在 Broker 1 上创建镜像。
在这种情况下,Broker 1 完全丢失,数据也完全丢失,因此未镜像的队列 B 也完全丢失。
米。 6. 经纪商 1 恢复服务
Broker 3 重新上线,因此队列 A 和 B 取回在其上创建的镜像以满足其 HA 策略。 但现在所有的主队列都在一个节点上! 这并不理想,节点之间均匀分布更好。 不幸的是,这里没有太多用于重新平衡大师的选项。 我们稍后会再讨论这个问题,因为我们需要首先考虑队列同步。
米。 7. 经纪商 3 恢复服务。 所有主队列都在一个节点上!
所以现在你应该知道镜像是如何提供冗余和容错的。 这可以确保单个节点发生故障时的可用性并防止数据丢失。 但我们还没有完成,因为实际上情况要复杂得多。
同步
创建新镜像时,所有新消息将始终复制到该镜像和任何其他镜像。 至于master队列中现有的数据,我们可以将其复制到一个新的镜像中,该镜像就成为了master的完整副本。 我们也可以选择不复制现有消息,让主队列和新镜像及时汇聚,新消息到达尾部,现有消息离开主队列头部。
此同步是自动或手动执行的,并使用队列策略进行管理。 让我们看一个例子。
我们有两个镜像队列。 队列A自动同步,队列B手动同步。 两个队列都包含十条消息。
米。 8. 两个不同同步模式的队列
现在我们正在失去 Broker 3。
米。 9. 经纪人3倒下
经纪商 3 恢复服务。 集群为新节点上的每个队列创建一个镜像,并自动将新的队列A与master同步。 然而,新队列B的镜像仍然是空的。 这样,我们就可以在队列 A 上实现完全冗余,并且只有一个镜像来存储现有的队列 B 消息。
米。 10. 队列 A 的新镜像接收所有现有消息,但队列 B 的新镜像不接收。
两个队列中又有十条消息到达。 然后,Broker 2 崩溃,队列 A 回滚到位于 Broker 1 上的最旧的镜像。失败时不会丢失数据。 在队列 B 中,主队列中有 XNUMX 条消息,镜像中只有 XNUMX 条消息,因为该队列从未复制原始的 XNUMX 条消息。
米。 11. Queue A回滚到Broker 1而不丢失消息
两个队列中又有十条消息到达。 现在Broker 1崩溃了,Queue A轻松切换到镜像,并且不会丢失消息。 但是,队列 B 遇到了问题。 此时我们可以优化可用性或一致性。
如果我们想优化可访问性,那么策略 ha-失败时升级 应该安装在 时刻。 这是默认值,因此您可以根本不指定策略。 在这种情况下,我们本质上是允许不同步镜像出现故障。 这会导致消息丢失,但队列将保持可读可写。
米。 12. 队列A回滚到Broker 3,消息不会丢失。 队列 B 回滚到 Broker 3,丢失 XNUMX 条消息
我们还可以安装 ha-promote-on-failure
转化为意义 when-synced
。 在这种情况下,队列将等待 Broker 1 及其数据返回到在线模式,而不是回滚到镜像。 返回后,主队列返回到 Broker 1,没有任何数据丢失。 为了数据安全而牺牲了可用性。 但这是一种有风险的模式,甚至可能导致数据完全丢失,我们将很快讨论这一点。
米。 13. 失去 Broker 1 后队列 B 仍然不可用
您可能会问,“不使用自动同步是不是更好?” 答案是同步是一个阻塞操作。 同步期间,主队列不能执行任何读或写操作!
让我们看一个例子。 现在我们排了很长的队。 它们怎么能长到这么大呢? 有几个原因:
- 队列未被积极使用
- 这些是高速队列,而现在消费者速度很慢
- 这是高速排队,出现了故障,消费者正在迎头赶上
米。 14、两个同步方式不同的大队列
现在经纪人3倒下了。
米。 15. Broker 3 宕机,每个队列中留下一个 master 和一个mirror
Broker 3 重新上线并创建新的镜像。 主队列A开始将现有消息复制到新镜像,在此期间队列不可用。 复制数据需要两个小时,导致该队列停机两个小时!
然而,队列 B 在整个期间保持可用。 她为了可访问性牺牲了一些冗余。
米。 16. 同步期间队列保持不可用
两个小时后,队列 A 也变得可用,并且可以再次开始接受读取和写入。
更新
同步过程中的这种阻塞行为使得更新具有非常大队列的集群变得困难。 在某些时候,主节点需要重新启动,这意味着在服务器升级时切换到镜像或禁用队列。 如果我们选择转换,如果镜像不同步,我们将会丢失消息。 默认情况下,在代理中断期间,不会执行到不同步镜像的故障转移。 这意味着一旦代理返回,我们就不会丢失任何消息,唯一的损坏是一个简单的队列。 代理断开连接时的行为规则由策略设置 ha-promote-on-shutdown
。 您可以设置两个值之一:
always
= 启用到不同步镜像的转换when-synced
= 仅转换为同步镜像,否则队列将变得不可读且不可写。 代理返回后队列立即恢复服务
不管怎样,对于大型队列,您必须在数据丢失和不可用之间做出选择。
当可用性提高数据安全性时
在做出决定之前,还有一个复杂的问题需要考虑。 虽然自动同步更有利于冗余,但它如何影响数据安全? 当然,有了更好的冗余,RabbitMQ 丢失现有消息的可能性较小,但是来自发布者的新消息呢?
这里你需要考虑以下几点:
- 发布者是否可以简单地返回错误并让上游服务或用户稍后重试?
- 发布者可以将消息保存在本地或数据库中以便稍后重试吗?
如果发布者只能丢弃消息,那么实际上提高可访问性也提高了数据安全性。
因此,必须寻求一个平衡点,解决方案要根据具体情况而定。
ha-promote-on-failure=when-synced 的问题
想法 ha-失败时升级= 何时同步 是我们防止切换到不同步的镜像,从而避免数据丢失。 队列仍然不可读取或不可写入。 相反,我们尝试恢复崩溃的代理并保持其数据完整,以便它可以恢复作为主服务器的功能而不会丢失数据。
但是(这是一个很大的但是)如果经纪人丢失了他的数据,那么我们就会遇到一个大问题:队列丢失了! 所有数据都消失了! 即使您的镜像大部分赶上主队列,这些镜像也会被丢弃。
要重新添加同名节点,我们告诉集群忘记丢失的节点(使用命令 rabbitmqctlforget_cluster_node)并启动一个具有相同主机名的新代理。 当集群记住丢失的节点时,它会记住旧的队列和不同步的镜像。 当集群被告知忘记一个孤立节点时,该队列也会被忘记。 现在我们需要重新声明它。 尽管我们有包含部分数据集的镜像,但我们丢失了所有数据。 换成非同步镜像会更好!
因此,手动同步(和同步失败)结合 ha-promote-on-failure=when-synced
在我看来,风险很大。 文档说这个选项的存在是为了数据安全,但它是一把双刃刀。
掌握再平衡
正如所承诺的,我们回到所有主节点在一个或多个节点上的累积问题。 这甚至可能由于滚动集群更新而发生。 在三节点集群中,所有主队列将累积在一个或两个节点上。
重新平衡主机可能会出现问题,原因有两个:
- 没有好的工具来执行重新平衡
- 队列同步
有第三方进行再平衡
还有另一个技巧可以通过 HA 策略来移动主队列。 说明书上提到
- 使用优先级高于现有 HA 策略的临时策略删除所有镜像。
- 将 HA 临时策略更改为使用节点模式,指定主队列应传输到的节点。
- 同步推送迁移的队列。
- 迁移完成后,删除临时策略。 初始HA策略生效,并创建所需数量的镜像。
缺点是,如果您有较大的队列或严格的冗余要求,则此方法可能不起作用。
现在让我们看看 RabbitMQ 集群如何处理网络分区。
失去连接
分布式系统的节点通过网络链路连接,网络链路可以而且将会被断开。 中断频率取决于本地基础设施或所选云的可靠性。 无论如何,分布式系统必须能够应对它们。 我们再次在可用性和一致性之间做出选择,好消息是 RabbitMQ 提供了这两个选项(只是不同时)。
使用 RabbitMQ,我们有两个主要选项:
- 允许逻辑划分(裂脑)。 这保证了可用性,但可能会导致数据丢失。
- 禁用逻辑分离。 可能会导致短期可用性丧失,具体取决于客户端连接到集群的方式。 还可能导致两节点集群完全不可用。
但什么是逻辑分离呢? 这是由于网络连接丢失而导致集群分裂为两部分的情况。 在每一侧,镜像都被提升为主服务器,因此每个队列最终有多个主服务器。
米。 17. 主队列和两个镜像,每个镜像位于一个单独的节点上。 然后发生网络故障并且一个镜像断开。 分离的节点看到另外两个节点已经脱落,并将其镜像提升为主节点。 我们现在有两个主队列,既可写又可读。
如果发布者将数据发送到两个主服务器,我们最终会得到队列的两个不同副本。
RabbitMQ 的不同模式提供可用性或一致性。
忽略模式(默认)
此模式确保可访问性。 失去连接后,就会发生逻辑分离。 连接恢复后,管理员必须决定优先考虑哪个分区。 失败的一方将重新开始,并且该方积累的所有数据都将丢失。
米。 18. 三个出版商与三个经纪人有关联。 在内部,集群将所有请求路由到 Broker 2 上的主队列。
现在我们正在失去 Broker 3。他看到其他 Broker 已经掉队,并将他的镜像提升为 Master。 这就是逻辑分离发生的方式。
米。 19.逻辑划分(裂脑)。 记录进入两个主队列,两个副本分开。
连接已恢复,但逻辑分离仍然存在。 管理员必须手动选择失败的一方。 在下面的情况下,管理员重新启动 Broker 3。他未能成功传输的所有消息都会丢失。
米。 20. 管理员禁用 Broker 3。
米。 21. 管理员启动 Broker 3,它加入集群,丢失留在那里的所有消息。
在连接丢失期间以及连接恢复后,集群和该队列可用于读取和写入。
自动修复模式
工作原理与忽略模式类似,不同之处在于集群本身在分裂和恢复连接后自动选择失败的一方。 失败的一方返回到集群为空,队列将丢失仅发送到该方的所有消息。
暂停少数派模式
如果我们不想允许逻辑分区,那么我们唯一的选择就是放弃集群分区后较小一侧的读写操作。 当代理发现自己处于较小的一侧时,它会暂停工作,即关闭所有现有连接并拒绝任何新连接。 它每秒检查一次连接恢复情况。 一旦连接恢复,它就会恢复操作并加入集群。
米。 22. 三个出版商与三个经纪人有关联。 在内部,集群将所有请求路由到 Broker 2 上的主队列。
然后,代理 1 和 2 从代理 3 中分离出来。代理 3 不会将其镜像提升为主镜像,而是挂起并变得不可用。
米。 23. Broker 3 暂停,断开所有客户端连接,并拒绝连接请求。
一旦连接恢复,它就会返回集群。
让我们看另一个示例,其中主队列位于 Broker 3 上。
米。 24. Broker 3 上的主队列。
然后会发生同样的连接丢失。 Broker 3 暂停,因为它位于较小的一侧。 另一方面,节点发现 Broker 3 已失效,因此 Brokers 1 和 2 中的旧镜像被提升为主镜像。
米。 25. 如果 Broker 2 不可用,则转换到 Broker 3。
当连接恢复时,Broker 3 将加入集群。
米。 26. 集群已恢复正常运行。
这里要理解的重要一点是我们可以获得一致性,但我们也可以获得可用性, 如果 我们将成功地将客户转移到大部分区域。 对于大多数情况,我个人会选择暂停少数模式,但这实际上取决于具体情况。
为了确保可用性,确保客户端成功连接到主机非常重要。 让我们看看我们的选择。
确保客户连接
对于如何在失去连接后将客户端定向到集群的主要部分或工作节点(在一个节点发生故障后),我们有多种选择。 首先,让我们记住,特定队列托管在特定节点上,但路由和策略会在所有节点之间复制。 客户端可以连接到任何节点,内部路由会将它们引导到需要去的地方。 但是当一个节点挂起时,它会拒绝连接,因此客户端必须连接到另一个节点。 如果节点脱落,他就无能为力了。
我们的选择:
- 使用负载均衡器访问集群,该负载均衡器简单地循环浏览节点,客户端重试连接直到成功。 如果节点已关闭或挂起,则尝试连接到该节点将失败,但后续尝试将转到其他服务器(以循环方式)。 这适用于短期失去连接或服务器宕机但需要快速恢复的情况。
- 通过负载均衡器访问集群,并在检测到挂起/故障的节点后立即从列表中删除它们。 如果我们快速做到这一点,并且客户端能够重试连接,那么我们将实现持续的可用性。
- 给每个客户端一个所有节点的列表,客户端在连接时随机选择其中一个。 如果尝试连接时收到错误,它将移动到列表中的下一个节点,直到连接为止。
- 使用 DNS 删除故障/挂起节点的流量。 这是使用小 TTL 完成的。
发现
RabbitMQ 集群有其优点和缺点。 最严重的缺点是:
- 加入集群时,节点会丢弃其数据;
- 阻塞同步会导致队列变得不可用。
所有困难的决定都源于这两个架构特征。 如果 RabbitMQ 能够在集群重新加入时保存数据,那么同步会更快。 如果能够实现非阻塞同步,那就更好支持大队列了。 解决这两个问题将极大地提高 RabbitMQ 作为容错和高可用消息传递技术的性能。 在以下情况下,我会犹豫是否推荐使用集群的 RabbitMQ:
- 网络不可靠。
- 存储不可靠。
- 队伍很长。
当涉及高可用性设置时,请考虑以下事项:
ha-promote-on-failure=always
ha-sync-mode=manual
cluster_partition_handling=ignore
(autoheal
)- 持久消息
- 确保当某个节点出现故障时客户端连接到活动节点
为了一致性(数据安全),请考虑以下设置:
- 发布者确认和消费者端手动确认
ha-promote-on-failure=when-synced
,如果发布者可以稍后再试并且如果您有非常可靠的存储! 否则放=always
.ha-sync-mode=automatic
(但对于大型非活动队列可能需要手动模式;还要考虑不可用是否会导致消息丢失)- 暂停少数派模式
- 持久消息
我们还没有涵盖容错和高可用性的所有问题; 例如,如何安全地执行管理程序(例如滚动更新)。 我们还需要讨论联邦和 Shovel 插件。
如果我还遗漏了其他内容,请告诉我。
另请参阅我的
本系列的前几篇文章:
第 1 号 -
第 2 号 -
第 3 号 -
来源: habr.com