您的位置:  首页 > 技术 > 中间件 > 正文

【原创】RabbitMQ 之殇:auto-delete 怎么了

2021-11-08 18:00 https://my.oschina.net/moooofly/blog/5301050 摩云飞 次阅读 条评论

背景

测试报 bug 说,使用 auto-delete queue 时出现了奇怪的问题:

  • 从管理界面上看,目标 queue 相应的绑定关系“丢失”
  • 当出现集群网络不通时(脑裂),(重新)声明绑定关系时会阻塞住,直到集群心跳超时(net tick 超时)
  • 集群心跳超时后,(怀疑)RabbitMQ 内部的相关删除逻辑与客户端重新声明的绑定逻辑同时“发生”,(产生竞态,进而导致)界面上看不到绑定关系了
  • 客户端做过改动:把 heartbeat 心跳调到了十几秒
  • c 客户端之前针对类似问题做过一些调整,但 python 的 pika 客户端无调整

由于这个问题出现了较长时间,且一直没有定论,于是我决定出手一探究竟

尝试复现

解决问题的关键,其实是能够复现出问题;

由于脑裂问题涉及到多节点通信,理论上至少需要 2 或 3 个机器才行,此时采用 docker 的来处理该问题,最为合适;

复现步骤简单如下:

  • 基于 docker-compose 构建 rabbit1 和 rabbit2 两节点 rabbitmq cluster
  • 在节点 rabbit1 上基于配置文件直接创建出具有 auto-delete 属性的 queue
  • 基于测试客户端构建针对上述 queue 的消费者,且客户端支持断链后自动切换连接节点的能力
    • 情况一:连接到 rabbit1 节点
    • 情况二:连接到 rabbit2 节点
  • 通过 docker network 命令模拟脑裂行为
  • 观察脑裂发生后
    • 测试客户端的切换行为,协议执行情况
    • 服务器侧 rabbit1 和 rabbit2 两个节点上的日志输出情况
  • 通过 docker network 命令对脑裂情况进行恢复
  • 观察脑裂恢复后
    • 测试客户端的切换行为,协议执行情况
    • 服务器侧 rabbit1 和 rabbit2 两个节点上的日志输出情况

 

基于 C 客户端测试

  1. 基于 docker-compose 构建 2 node rabbitmq cluster

  2. 基于配置,默认在 rabbit1 节点上创建名为 auto-delete.queue 的 AD queue

    {
      "name": "auto-delete.queue",
      "vhost": "/",
      "durable": false,
      "auto_delete": true,
      "arguments": {}
    }
  1. cluster 运行信息如下

    • RabbitMQ 版本为 3.9.8
    • Erlang OTP 版本为 24.1.3
    • 双节点分别为 rabbit1 和 rabbit2

  1. 基于配置创建出来的 AD queue ,初始状态如下

  • 具有 AD 属性
  • 位于 rabbit1 节点上
  • 有 bind 无 consumer

  1. 运行测试客户端,创建一个消费者到 AD queue 上

rabbit1 收到连接信息

  1. 将 rabbit1 从 docker network 中踢出,模拟脑裂情况

从 rabbit1 和 rabbit2 的管理界面上很快会发现“卡住”状态的发生,之后点击页面元素无法正常响应

服务器侧:rabbit1 发现心跳超时(这里我们采用的心跳时间为5秒),故主动关闭了客户端 tcp 连接,之后 rabbit2 上立刻收到了来自客户端的重连

客户端侧:在 net tick 超时前,协议流程阻塞在接收 Queue.Declare-Ok 上

大约 1min 后(net tick 超时),rabbit2 的管理界面恢复可用状态,此时可以看到,rabbit2 判定 rabbit1 已经下线

服务器侧:rabbit1 和 rabbit2 均认为对端已下线,原因为 net_tick_timeout

客户端侧:在 net tick 超时后,在 rabbit2 上的协议流程将会继续完成剩余部分

此时,管理界面可以确认上述消费者的情况

同时,可以测试下,在 rabbit2 上新建的 AD queue 以及消费者能否正常工作

可以看到,工作完全正常

  1. 将 rabbit1 重新加入 docker network 中,模拟脑裂恢复

服务器侧:

客户端侧:保持之前的状态,没有任何变化

基于 Python 客户端测试

由于之前 bug 是打在 python 客户端上,若不基于 pika 也测试一次恐难服众,于是

基于 pika 创建一个消费者

正常完成协议流程

将 rabbit1 从 docker network 中踢掉,管理页面开始进入不可用状态

服务器侧:tcp 连接从 rabbit1 上断开,重新向 rabbit2 建立

客户端侧:在 net tick 超时前,协议流程同样阻塞在接收 Queue.Declare-Ok 上(这里我加的打印不太好)

net tick 超时后,管理页面恢复可用状态

服务器侧:rabbit1 和 rabbit2 相互认为对端已经掉线

客户端侧:继续完成剩下的协议流程

最终效果,pika 客户端成功在 rabbit2 上创建了 AD queue ,完成了 bind 和 consume 操作

完整过程的抓包如下

之后,恢复 rabbit1 的网络,行为和之前的实验一致,不再赘述

其他

  • 由于报 bug 的环境使用的是 RabbitMQ 3.7.17 古董版本,而我用于复现的版本是最新的 3.9.8,害怕被人说结果不符合要求,于是又屁颠屁颠的基于 3.7.17 重新测试了一遍,结果没差别,不再赘述。

  • 上述测试中,建立最初的消费关系时,客户端均是连接的 rabbit1 节点,没有测试创建 AD queue 在 rabbit1 上,但客户端却连到 rabbit2 上完成 bind + comsume 的情况(这种情况,其实并不符合常规逻辑)

  • 测试的 bug 说,之前出现问题时似乎是阻塞在了 queue bind 上,而不是 queue declare 上,但这种情况在我的复现中始终无法出现

结论

  • 无论是基于古董版本 3.7.17 ,还是最新版本 3.9.8 ,针对上述问题,行为是一致的

  • 无论是采用我们自己实现的 C 库客户端,还是基于 pika 的客户端,针对上述问题,行为是一致的

  • 当支持自动切换逻辑的客户端遇到连接节点直接掉线的情况时,会触发连接到新节点的逻辑,会成功建立 TCP 连接,以及 AMQP 层面的 Channel ,之后发出 Queue.Declare 后,会阻塞在接收 Queue.Declare-Ok 上,阻塞时间长度取决于 net_tick_timeout 的值,阻塞期间 AMQP 协议层面的 heartbeat 能够正常交互;当net_tick_timeout 时间到达后,协议流程将自动恢复,之后客户端会在 rabbit2 上创建完成相应的 queue、bind 关系,以及 consume 成功;

  • 由于未能复现出 bug 中所说的“绑定关系丢失”,所以这篇文章并未说解决了问题,而是提供一个基础测试数据,在此基础上,如果再次出现上述问题,应该可以做的更好一点

遗留问题

  • “竞态”问题是否真的存在:rabbitmq 底层对于“脑裂”的处理,与上层业务会重连后的动作,是否真的在“元信息”处理上存在“竞态”关系?

  • auto-delete queue 是否存在“无解”的使用问题,是否可以和其他属性搭配使用会有更好的效果?

(如果有时间,后面我再补一篇基于源码的讨论)

 


PS:本文敏感字如下,呵呵


原创不易,添加关注,点赞,在看,分享是最好的支持, 谢谢~

更多精彩内容,欢迎关注微信公众号 西风冷楼阙

 

  • 0
    感动
  • 0
    路过
  • 0
    高兴
  • 0
    难过
  • 0
    搞笑
  • 0
    无聊
  • 0
    愤怒
  • 0
    同情
热度排行
友情链接