RabbitMQ与Kafka:群集中的故障转移和高可用性



容错和高可用性是主要话题,因此RabbitMQ和Kafka将分别撰写文章。 与RabbitMQ相比,本文是关于RabbitMQ的,而下一篇是关于Kafka的。 这篇文章很长,所以让自己舒服一点。

考虑容错,一致性和高可用性(HA)的策略,以及每种策略必须进行的权衡。 RabbitMQ可以在节点集群上运行-然后将其分类为分布式系统。 对于分布式系统,我们经常谈论一致性和可访问性。

这些概念描述了发生故障时系统的行为。 网络连接故障,服务器故障,硬盘故障,由于垃圾收集,数据包丢失或网络连接变慢而导致的临时服务器不可用。 所有这些都可能导致数据丢失或冲突。 事实证明,针对所有类型的故障,建立一个完全一致(无数据丢失,没有数据差异)并且可访问(将接受读取和写入操作)的系统几乎是不可能的。

我们将看到一致性和可访问性处于频谱的不同末端,您需要选择哪种优化方式。 好消息是,有了RabbitMQ,这样的选择是可能的。 您具有一种“书呆子”的杠杆作用,可以使平衡朝着更大的一致性或更大的可访问性转移。

我们将特别注意哪些配置会因确认记录而导致数据丢失。 发布者,经纪人和消费者之间存在责任链。 在消息发送给代理之后,不丢失消息是他的工作。 当经纪人向发布者确认消息的接收时,我们预计它不会丢失。 但是我们会看到,根据经纪人和发布者的配置,这确实可能发生。

一节点稳定性的原语


持续队列/路由


RabbitMQ中有两种类型的队列:持久/非持久。 所有队列都存储在Mnesia数据库中。 节点启动时将重新声明持久队列,从而在重新启动,系统崩溃或服务器崩溃(只要保存了数据)中幸免于难。 这意味着,当您声明路由(交换)和队列具有弹性时,队列/路由的基础结构将在线返回。

主机重新启动时,易变队列和路由将被删除。

持久消息


仅仅因为队列很长并不意味着它的所有消息都将在节点重启后幸存下来。 仅还原由发布者设置为持久性的消息。 持久消息的确给代理增加了负担,但是如果消息丢失是不可接受的,则没有其他方法。


图 1.稳定性矩阵

队列镜像集群


为了避免经纪人的损失,我们需要裁员。 我们可以将几个RabbitMQ节点合并到一个集群中,然后通过在几个节点之间复制队列来添加额外的冗余。 因此,如果一个节点掉落,我们不会丢失数据并保持可用状态。

队列镜像:

  • 一个主队列(主机),接收所有写和读命令
  • 一个或多个从主队列接收所有消息和元数据的镜像。 这些镜像不存在,只能用于冗余扩展。


图 2.镜像队列

镜像由适当的策略设置。 在其中,您可以选择复制速率,甚至可以选择应放置队列的节点。 范例:

  • ha-mode: all
  • ha-mode: exactly, ha-params: 2 (一个主控和一个镜像)
  • ha-mode: nodes, ha-params: rabbit@node1, rabbit@node2

确认发布者


要实现顺序记录,必须确认发布者确认。 没有它们,就有可能丢失消息。 将消息写入磁盘后,将确认发送给发布者。 RabbitMQ不是在收到消息时而是在几百毫秒左右的时间内将消息写到磁盘上。 当对队列进行镜像时,仅在所有镜像也已将其消息副本写入磁盘后,才发送确认。 这意味着使用确认会增加延迟,但是如果数据安全性很重要,则必须使用确认。

故障转移队列


当代理关闭或崩溃时,此节点上的所有主要队列(主服务器)都将随之消失。 然后,集群选择每个主服务器的最旧镜像并将其升级为新的主服务器。


图 3.几个镜像队列及其策略

经纪人3滴。 请注意,代理2上的队列C的镜像已升级为主服务器。 还要注意,已经为代理1上的队列C创建了新的镜像。RabbitMQ始终尝试维护策略中指定的复制速率。


图 4.代理3崩溃,导致队列C失败

下一个经纪人1下跌! 我们只剩下一位经纪人。 队列B的镜像升起为主服务器。


图 5

我们返回了Broker1。无论数据在代理的丢失和恢复中如何成功存活,所有镜像的队列消息在重新启动时都会被丢弃。 注意这一点很重要,因为会导致后果。 我们将尽快考虑这些后果。 因此,Broker 1现在再次成为集群的成员,并且该集群正在尝试遵守策略,因此在Broker 1上创建了镜像。

在这种情况下,Broker 1和数据的丢失已完成,因此非镜像队列D完全丢失。


图 6.经纪人1重新投入使用

Broker 3重新上线,因此A和B行将在其上创建镜像以适应其HA策略。 但是现在所有的主线都在一个节点上! 这不是理想的;节点之间的均匀分布更好。 不幸的是,没有什么特别的方法可以重新平衡母版。 稍后,我们将回到这个问题,因为我们需要首先考虑队列同步。


图 7.代理3重新投入使用。 所有主队列都在一个节点上!

因此,您现在应该了解镜像如何提供冗余和容错能力。 这样可以确保在单节点故障的情况下的可用性,并防止数据丢失。 但是我们还没有完成,因为实际上所有事情都更加复杂。

同步处理


创建新镜像时,所有新消息将始终复制到该镜像和其他任何镜像。 至于主队列中的现有数据,我们可以将其复制到一个新的镜像中,该镜像成为主数据库的完整副本。 当新消息到达末尾而现有消息离开主队列的头部时,我们也不能复制现有消息并允许主队列和新镜像及时收敛。

此同步是自动或手动执行的,并使用队列策略控制。 考虑一个例子。

我们有两条镜像线。 队列A自动同步,队列B手动同步。 这两行都有十条消息。


图 8.两个具有不同同步模式的队列

现在我们正在失去经纪人3。


图 9.经纪人3下跌

经纪人3重新投入使用。 集群为新节点上的每个队列创建一个镜像,并自动将新队列A与主服务器同步。 但是,新的Turn B的镜子仍然是空的。 因此,我们具有队列A的完全冗余,而队列B的现有消息只有一个镜像。


图 10.队列A的新镜像接收所有现有消息,但是队列B的新镜像未接收到

这两行都收到十条消息。 然后,代理2下降,并且队列A回滚到位于代理1上的最早的镜像。发生故障时,不会丢失任何数据。 向导中的队列B中有20条消息,而镜像中只有10条消息,因为此队列从不复制原始的10条消息。


图 11.行A回滚到代理1,而不会丢失消息

这两行都收到十条消息。 现在,代理1崩溃了,队列A可以毫无问题地切换到镜像而不会丢失消息。 但是,队列B有问题。 在这一点上,我们可以优化可访问性或一致性。

如果我们想优化可访问性,则应将ha-momote-on-failure策略设置为always 。 这是默认值,因此您可以完全忽略该策略。 实际上,在这种情况下,我们允许非同步镜像发生故障。 这将导致消息丢失,但是队列仍可读写。


图 12.行A回滚到代理3,而不会丢失消息。 B行回滚到Broker 3,丢失了十条消息

我们还可以将ha-promote-on-failure为when-synced 。 在这种情况下,队列将等待直到Broker 1及其数据返回联机模式,而不是回滚到镜像。 他返回后,主队列再次出现在Broker 1上,而没有数据丢失。 为了数据安全性,牺牲了可访问性。 但这是一种冒险的模式,甚至可能导致数据完全丢失,我们将在不久的将来考虑这种模式。


图 13.失去经纪人1后,B行仍然不可用

您可能会问一个问题:“也许最好不要使用自动同步?”。 答案是同步是一项阻塞操作。 在同步期间,主队列无法执行任何读取或写入操作!

考虑一个例子。 现在我们的行很长。 他们怎么能长到这么大? 由于几个原因:

  • 队列未积极使用。
  • 这些是高速线路,现在消费者很慢
  • 这些是高速队列,发生了故障,消费者正在追赶


图 14.两个具有不同同步模式的大队列

现在,代理3崩溃。


图 15.经纪人3跌倒,每个队列中剩下一个主服务器和一个镜像

Broker 3返回,并创建新的镜像。 主队列A开始将现有消息复制到新的镜像,在此期间,队列A不可用。 数据复制需要两个小时,导致此队列的停机时间为两个小时!

但是,B线在整个期间仍然可用。 为了获得方便,她牺牲了一些冗余。


图 16.同步期间队列仍然不可用

两个小时后,队列A也变得可用,并且可能再次开始接受读取和写入操作。

更新内容


同步期间的这种阻塞行为使升级具有很大队列的群集变得困难。 在某个时候,需要重新启动带有向导的节点,这意味着在服务器更新期间要么切换到镜像,要么关闭队列。 如果选择过渡,则如果镜像未同步,则会丢失消息。 默认情况下,在断开代理的过程中,不执行向非同步镜像的过渡。 这意味着一旦经纪人返回,我们就不会丢失任何消息,唯一的损失只是一个简单的队列。 禁用经纪人受ha-promote-on-shutdown政策的约束。 您可以设置以下两个值之一:

  • always =启用切换到非同步镜像
  • when-synced =仅切换到同步镜像,否则队列将无法访问以进行读写。 代理返回后,队列立即返回

在队列很大的情况下,您必须在数据丢失和无法访问之间进行选择。

当可用性提高数据安全性时


在做出决定之前,必须考虑更多的复杂性。 虽然自动同步更适合冗余,但它如何影响数据安全性? 当然,由于具有更好的冗余性,RabbitMQ丢失现有消息的可能性较小,但是来自发布者的新消息呢?

在这里,您需要考虑以下几点:
  • 发布者可以仅返回错误,而更高的服务或用户以后可以重试吗?
  • 发布者可以在本地或数据库中保存邮件以供稍后重试吗?

如果发布者只能删除消息,那么实际上,提高可访问性也可以提高数据安全性。

因此,您需要寻求一种平衡,并且决定取决于特定的情况。

ha-promote-on-failure的问题=同步时


ha-promote-on-failure = 同步时的想法是,我们防止切换到未同步的镜像,从而避免了数据丢失。 队列仍然不可访问以进行读取或写入。 取而代之的是,我们尝试返回一个损坏的代理,其中包含未损坏的数据,以便该代理恢复作为主服务器的工作而不会丢失数据。

但是(这很大)但是,如果经纪人丢失了他的数据,那么我们有一个大问题:队列丢失了! 所有数据都消失了! 即使您具有基本赶上主队列的镜像,这些镜像也将被丢弃。

要重新添加具有相同名称的节点,我们告诉集群忘记丢失的节点(使用rabbitmqctlgot_cluster_node命令 ),并使用相同的主机名启动一个新的代理。 只要集群记住丢失的节点,它就会记住旧队列和未同步的镜像。 当集群被告知忘记丢失的节点时,该队列也会被忘记。 现在,您需要重新声明它。 我们丢失了所有数据,尽管我们拥有带有部分数据集的镜像。 最好切换到非同步镜像!

因此,我认为手动同步(和同步失败)与ha-promote-on-failure=when-synced结合在一起是非常危险的。 文档说存在此选项是为了保证数据安全,但这是一把双刃小刀。

重新平衡大师


如所承诺的,我们回到一个或多个节点上所有主机的累积的问题。 即使滚动滚动群集更新也可能发生这种情况。 在具有三个节点的群集中,所有主队列将累积在一个或两个节点上。

重新平衡母带可能会出现问题,原因有两个:

  • 没有好的再平衡工具
  • 队列同步

为了实现平衡,有一个第三方插件不受官方支持。 关于第三方插件,RabbitMQ手册说 :“该插件提供了一些其他配置和报告工具,但是RabbitMQ团队不支持该插件,也未对其进行测试。 使用后果自负。”

还有另一种技巧可以通过高可用性策略来移动主队列。 手册中提到了一个脚本 。 其工作方式如下:

  • 使用优先级高于现有高可用性策略的临时策略删除所有镜像。
  • 更改HA临时策略,以将节点模式与需要将主队列移动到的节点一起使用。
  • 同步队列以进行强制迁移。
  • 迁移完成后,删除临时策略。 HA初始策略生效,并创建所需数量的镜像。

缺点是,如果队列很大或有严格的冗余要求,则此方法可能不起作用。

现在,让我们看看RabbitMQ集群如何与网络分区一起工作。

连接中断


分布式系统的节点通过网络链接连接,网络链接可以并且将断开连接。 中断的频率取决于本地基础架构或所选云的可靠性。 无论如何,分布式系统应该能够处理它们。 同样,我们可以在可访问性和一致性之间做出选择,而且好消息是RabbitMQ同时提供了两者(只是不同时提供)。

使用RabbitMQ,我们有两个主要选择:

  • 允许逻辑分隔(裂脑)。 这提供了可访问性,但可能导致数据丢失。
  • 禁止逻辑分离。 可能会导致短期可用性下降,具体取决于客户端如何连接到群集。 它还可能导致在两个节点的群集中完全无法访问。

但是什么是逻辑分离呢? 这是由于网络连接丢失而将群集一分为二的情况。 镜子的每一侧都升起了主镜,因此最后,每匝都有几个主镜。


图 17.主线和两个镜像,每个镜像在单独的节点上。 然后发生网络故障,一个镜像分离。 分离节点发现其他两个已脱落,并将其镜像移至主节点。 现在我们有两条主线,并且都允许书写和阅读。

如果发布者将数据发送给两个母版,我们将获得队列的两个不同副本。

各种RabbitMQ模式提供可访问性或一致性。

忽略模式(默认)


此模式提供辅助功能。 连接断开后,发生逻辑分离。 重新连接后,管理员必须决定首选哪个分区。 丢失方将重新启动,并且从该方收集的所有数据都将丢失。


图 18.三个发行人与三个经纪人相关联。 在内部,集群将所有请求转发到Broker 2上的主队列。

现在我们正在失去经纪人3。他看到其他经纪人已经破产,并将他的镜像移到了主服务器上。 这是逻辑上的分离。


图 19.逻辑分离(裂脑)。 记录分为两行,两份不同。

恢复了连接性,但仍然保持逻辑隔离。 管理员必须手动选择失败者。 在以下情况下,管理员将重新启动Broker3。他未设法传输的所有消息都将丢失。


图 20.管理员禁用Broker 3。


图 21.管理员启动Broker 3,然后他加入集群,丢失了所有保留在该集群中的消息。

在失去连接期间以及恢复之后,群集和此队列可用于读取和写入。

自动修复模式


它的工作方式与忽略模式相似,不同之处在于,群集本身会在拆分和恢复连接后自动选择失败的一面。 丢失的一方返回到群集为空的队列,并且队列丢失了仅发送到该方的所有消息。

暂停少数民族模式


如果我们不想允许逻辑分离,那么我们唯一的选择是拒绝在集群分区之后的较小一侧进行读写。 当经纪人发现他处于次要位置时,他会暂停,即关闭所有现有连接并拒绝任何新连接。 每秒一次,它检查是否重新连接。 恢复连接后,它将恢复工作并加入群集。


图 22.三个发行人与三个经纪人相关联。 在内部,集群将所有请求转发到Broker 2上的主队列。

然后,代理1和2与代理3分离。代理3暂停并无法访问,而不是将其镜像升级为主服务器。


图 23. Broker 3暂停,断开所有客户端的连接,并拒绝连接请求。

恢复连接后,它将返回到群集。

让我们看另一个示例,其中主行位于Broker 3上。


图 24.经纪人3的主线。

然后发生相同的连接丢失。 代理3暂停,因为它位于较小的一侧。 另一方面,节点会看到代理3发生故障,因此来自代理1和2的较旧镜像升为主服务器。


图 25.如果代理3不可用,请转换到代理2。

恢复连接后,Broker 3将加入集群。


图 26.群集恢复正常运行。

重要的是要了解我们正在获得一致性,但是如果我们成功地将客户转移到本节的大部分内容, 那么我们也将获得可访问性。 在大多数情况下,我个人会选择“暂停少数派”模式,但这实际上取决于特定情况。

为了确保可用性,确保客户端成功连接到站点非常重要。 考虑我们的选择。

客户连接


对于失去连接后,如何将客户端发送到群集的主要部分或工作节点(一个节点发生故障之后),我们有几种选择。 首先,让我们回想一下,特定队列托管在特定主机上,但是路由和策略复制在所有主机上。 客户端可以连接到任何节点,内部路由将在必要时引导它们。 但是,当一个节点挂起时,它会拒绝连接,因此客户端必须连接到另一个节点。 如果节点掉线,他将无能为力。

我们的选择:

  • 使用负载平衡器访问群集,该负载平衡器简单地在节点之间循环,客户端反复尝试连接直到成功完成连接。 , , ( ). , .
  • / , . , , .
  • , . , , .
  • / DNS. TTL.

结论


RabbitMQ . , :

  • ;
  • .

. RabbitMQ , . , . RabbitMQ . RabbitMQ :

  • .
  • .
  • .

, :

  • ha-promote-on-failure=always
  • ha-sync-mode=manual
  • cluster_partition_handling=ignore ( autoheal )

  • , , -

( ) :

  • Publisher Confirms Manual Acknowledgements
  • ha-promote-on-failure=when-synced , ! =always .
  • ha-sync-mode=automatic ( ; , , )
  • Pause Minority

; , (, ). Shovel.

- , , .

. , RabbitMQ Docker Blockade, , .

:
№1 — habr.com/ru/company/itsumma/blog/416629
№2 — habr.com/ru/company/itsumma/blog/418389
№3 — habr.com/ru/company/itsumma/blog/437446

Source: https://habr.com/ru/post/zh-CN471858/


All Articles