RabbitMQ高可用队列的实现

1. 背景

  RabbitMQ在单机模式、集群非镜像模式下存在单点故障,当队列创建时绑定的节点故障时,服务整体不可用。镜像队列(Mirrored queue)机制解决了RabbitMQ单点问题,保证了高可用(Highly Available)。

  通过镜像机制,RabbitMQ将队列放置于集群中的多个节点上,消息的生产和消费都会在节点间同步。镜像队列包含一个master和多个slave,当master退出时,最老的slave被提升为新的master。

  RabbitMQ队列实现可参考:https://my.oschina.net/hackandhike/blog/796063

  镜像队列配置可参考:https://my.oschina.net/hncscwc/blog/186350

  下文主要介绍镜像队列的相关算法和实现(https://github.com/rabbitmq/rabbitmq-server/blob/master/src/gm.erl)。

2. 镜像队列结构

  镜像队列中各节点的进程是独立的,进程间通过消息传递来通信。

图1  镜像队列进程结构

  镜像队列中每个节点都有一个amqqueue_process队列进程,这些进程中有一个master,其余是slave;每个队列进程会启动一个gm(guaranteed multicast)进程,镜像队列中所有gm进程组成一个gm组(gm group)。集群中每个有客户端连接的节点都会启动若干个channel进程,channel进程中记录着镜像队列中master和所有slave进程的Pid,以便直接与队列进程通信。

  消息的生产和消费会在master和slave之间进行同步。同步由master发起,通过GM来确保所有slave完成同步。

  GM保证镜像队列进程组中的进程可以动态添加和删除,发送到进程组的消息在消息的生命周期中,到达进程组中的每个进程。消息的生命周期从消息发送时算起,直到消息发送者了解到消息已经抵达了进程组中的所有进程为止。

  镜像队列结构可参考:https://github.com/rabbitmq/rabbitmq-server/blob/stable/src/rabbit_mirror_queue_coordinator.erl#L50

3. 可靠多播 (Guaranteed Multicast)

  广播通常的实现方法是发送者将消息发给集群中的每个节点,这要求集群节点是全联通的(fully connected)。当发送节点挂掉时,消息可能没有发到集群中的每个节点,这就引入了集群中哪些节点要为已挂掉节点负责、继续发送消息的问题。

  为简化网络拓扑和实现复杂度,GM组将集群中的节点组成一个环。环中节点挂掉时,很容易知道哪些节点已收到消息、哪些节点没有收到:如果一个节点挂掉,其左右邻居节点将获悉其退出信息,然后最近的上游节点(upstream)负责将挂掉节点未转发的消息(in-flight messages)继续发给最近的下游节点(downstream)。

  这就要求GM组中每个节点缓存收到的消息(gm缓存队列),以便当下游节点挂掉时重新转发该消息。消息的最初发送者在收到自身发出的消息时,必须能够再次发出该消息的确认信息(gm ack message)在GM组中同步,这样其他节点才能够将缓存消息删除。当消息最初发送者再次收到确认消息时,说明整个集群已经同步该消息。此时新加入GM组的节点,将不会再收到该消息。

图2  GM消息传递流程

  GM通过监控进程来获悉进程退出:A进程监控B进程,当B不存在时,A直接收到B退出消息(退出原因是进程不存在);当B运行一段时间后退出时,A同样能收到B退出消息(退出原因是进程退出)。GM组中各节点监控其上下游节点进程。

  GM实现中,主要包括以下3方面。

  1. 对于GM组中节点加入和退出的操作,如果这些操作是一个有序的序列,GM就能够判断哪些节点应该继承已挂掉节点的消息。如A->B->C变为A->X->C,如果不确定顺序,将无法判断应该是X继承B的消息,还是C继承B的消息。

  如果顺序确定为A->B->C->D变为A->C->D,B退出时,A和C将获悉退出消息,A将自己的状态(包括缓存的消息、确认消息)发给C,C结合自身状态就能知道哪些消息是B退出时未转发的。

图3  节点退出处理流程

  2. 对于GM组中新加入的节点,必须先与上游节点同步通信,初始化自身状态(与最近上游节点保持状态一致)。这样这个新节点才能够正确处理后面接收到的确认消息;同时当下游节点退出时,也能够把正确的状态发给新的下游节点。

  如A->B->C变为A->D->B->C,D向A发送加入集群请求,A发送自身状态到D,D将自身状态初始化与A一致,并通知B邻居信息更新。

图4  节点加入处理流程

  3. 集群中的节点状态(view)通常是保存在数据库中,为了确保节点变化是一个有序的序列,节点每收到一条消息都需要查询数据库来验证当前view是否失效,是否要更新view。这将急剧降低系统性能。

  GM的实现中,每个节点都缓存一份view,使用缓存失效机制来确保view的正确性。当节点变化时,其他节点能够根据收到的消息获悉该变化。由于GM将节点变化当做有序序列来处理,view的变化就不能通过GM的机制来确保:将view变化的消息传给环中一个节点时,不能保证该节点是否会挂掉。同时,节点的变化可能导致每个节点拥有不同的view。

  所以GM规定当view更新时,会原子性的增加view值(同时用事务更新数据库中节点状态)。节点间传输消息时,会包含节点的view,只有当接收节点持有的view值不小于发送节点的view值时,接收节点才能够正确处理消息;否则,接收节点要先更新其持有的view。

  如A->B->C->D->E变为A->E,假设A、B、C、D、E最初都有view值x。B、C、D同时退出时,A获悉B的退出消息,view值更新为x+1;E获悉D的退出消息,view值更新为x+2;然后A监控C,获悉C退出,view值更新为x+3;A继续获取下游节点E,向E发送自身状态,E的view值小于A,所以E先更新自身view(view值将更新为x+3),然后处理消息。(这是一种可能的顺序,真实的顺序按照节点收到退出消息顺序确定。)

  GM的实现可直接查看源码,有很好的代码注释:
  https://github.com/rabbitmq/rabbitmq-server/blob/stable/src/gm.erl

4. Master提升

  GM保证了集群中slave节点挂掉时,消息不会丢失。当master节点挂掉时,最老的slave节点将被提升为master,此时channel发给原master的消息可能并没有通过原master的GM广播出来,所以该场景下需要额外的机制来确保消息不丢失。

图5  生产消息传递路径

  RabbitMQ通过以下方法解决该问题:channel进程在收到生产者消息后,将消息发送给所有的队列进程,队列进程缓存从channel收到的消息(amqqueue缓存队列)。当队列进程从gm收到该消息后,将消息入BQ队列,并删除缓存消息。当master挂掉时,新的master通过对比gm缓存和amqqueue缓存,获取上述channel已发出、原master未通过gm转发的消息。新master此时会将未确认(consume ack)的消费消息退回BQ队列(requeue),然后将上面获取的原master未转发消息入BQ队列(同时在gm广播进行同步)。

  对于消费消息,channel只用与master通信,master将消费信息在集群中同步。当master挂掉时,新提升的master将未确认的消费消息退回BQ队列(由于新master不知道消费者是否ack,所以只能将消息退回队列);已确认的消费消息由新master继续在集群中同步,不用退回队列。

  该机制确保了RabbitMQ的可靠交付(at-least-once delivery)。

  该部分代码在slave中:https://github.com/rabbitmq/rabbitmq-server/blob/stable/src/rabbit_mirror_queue_slave.erl#L551

5. 镜像队列消息同步

  新节点加入集群时处于未同步状态,默认情况下镜像队列不会自动在集群中同步,所以该情况下可以等待消费者将老的消息取走,直到集群中master队列的内容和新节点一致,新节点才会处于同步状态;或者在节点加入后,手动调用同步命令,对镜像队列强制同步。手动同步时,队列处于阻塞状态,不响应客户端调用。

  处于未同步状态的节点,收到gm同步的消费信息时,直接传递消息,而不从后端BQ队列取消息。处于同步状态的节点,收到gm同步的消费信息时,先从BQ队列取消息,而后传递。

  Master节点重启后将变为slave,如果在重启时,集群仍有消息的生产和消费,原master将处于非同步状态,RabbitMQ实现中将该场景当做新节点加入来处理,直接将原master已保存的数据清空;如果在重启时,集群无消息生产和消费,原master将保持同步状态。

  所以,如果一个集群中节点相继退出,则最后一个退出的节点是master,该节点保存有最新的数据,集群再起启动时,须先启动该节点。

  需要注意的是镜像队列中没有已同步节点时,如果Master正常退出(rabbitmqctl stop),队列将处于Down状态,服务不可用;如果Master异常退出(crash、kill、宕机)时,RabbitMQ提升最老的Slave为Master,此时数据可能有丢失。(https://www.rabbitmq.com/ha.html#unsynchronised-slaves)

6. 总结

  GM的实现能够有效的保证消息在集群内同步,集群节点变化不影响正常的服务,保证了较高的可用性。该实现也能够在集群中做到负载均衡。

  不过正如开发者提到的,GM的实现虽然与LCRRing-Paxos算法类似,但是没有像这些算法以及PaxosRaft等经过严格证明,也没有明确的文档介绍,理解和维护起来有一定的难度。所以用Raft算法重写GM应该会是一个比较好的选择(社区也正在开发中)。

  同时上文中也可以看到RabbitMQ高可靠性的部分实现,当然这里更多的是确保at-least-once delivery可靠性。更多的可靠性要求可以结合其他AMQP特性RabbitMQ扩展来确保,这些都值得深入的了解。

参考

https://github.com/rabbitmq/rabbitmq-server/issues/224

https://my.oschina.net/hncscwc/blog/186350

腾讯云CMQ 与 RabbitMQ的对比

转载于:https://my.oschina.net/hackandhike/blog/800334

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值