1

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

 2 years ago
source link: https://my.oschina.net/moooofly/blog/5301050
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

背景

测试报 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

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

up-8e32e939d6732c9ea8fb591f2ca0e43e58b.png此时,管理界面可以确认上述消费者的情况

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

可以看到,工作完全正常

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

up-e7fc9174f8a7ab53a4e2c7149119dbbc500.png服务器侧:

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

基于 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:本文敏感字如下,呵呵

up-08e1d4716f5a12a410602f81fefb5c50fbc.png


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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK