RabbitMQ——短连接惹的祸

【前言】

最近在生产环境出现了一个奇怪问题,并且该问题多次出现,问题排查过程中对一些线索大胆猜测其问题的原因,最终找了了问题的根因。这里进行总结,方便后续回顾。

【问题现象】

环境背景与具体的现象:队列与路由到队列中的消息均为持久化;队列设置了最大长度为100W;同时队列设置为lazy模式;队列实际只堆积了30-50W的消息;队列里有一个消费者。

但是,消费者几乎无法从队列消费到消息,并且内存在不断的增加,最严重时,内存超过了设置的高水位,最终导致整体不可用

【问题分析】

问题出现时,第一反应怀疑消费者是不是有问题,因为我们的消费者一般都开启了ACK机制,同时设置了一定大小的prefetch_count。如果消费者没有及时进行ACK,导致unack数目等于prefetch_count的值,那么这个时候服务端确实是不会继续给消费者推送消息的。

然而,实际情况是队列的unack持续为0,这就意味着是rabbitmq没有给消费者推送消息。

出于不死心的心态,又把模拟消费的客户端放上去,想尝试到队列消费消息,结果都无法成功进行订阅

既然客户端无法订阅并消费消息,索性从WEB界面上直接GET消息,结果依旧是没有任何响应

这时,自己也产生了疑惑,明明队列里有消息,为什么不给消费者推送,GET请求也没有任何响应

带着疑惑打开了rabbitmq_top插件,发现有问题的这个队列的gen_server2 buffer中竟然300W+的消息,并且还在不断增加。马上又通过process_info查看了该队列进程的信息,发现队列进程字典中有100W+的credit_to记录

队列进程中100W+的credit_to记录就意味着当前或曾经有100W+的生产者(不懂队列进程字典中credit_to记录与生产者的关系的可以看下《RabbitMQ——流控》),然而实际发现很多credit_to记录对应的进程并不存在。于是大胆猜测生产者采用了"短连接"的方式,也就是每次发送消息时都新创建一条TCP连接,或者同一TCP连接上新打开一个通道,发送完消息后,关闭了连接或通道,并不断进行重复。

为了验证猜测,反推找到队列对应生产者的连接,在WEB界面上看到了该生产者连接的通道信息在不断变化,一会有1000多个通道,一会一个也没有了。同样,tcpdump抓包也进一步确认了生产者对应的连接上在不断重复的打开通道,发送消息,关闭通道。

至此,断定就是生产者采用了短连接的方式进行消息的发送导致了本次问题。与对应的开发人员沟通,改成了长连接的方式后,问题得以解决。

【原理分析】

问题虽然是解决了,但仍旧有疑惑:例如队列进程中大量的credit_to记录与buffer的堆积有什么关联?为什么buffer的堆积又会导致无法消费(GET)到消息。

带着疑问在测试环境中进行问题的复现,同时查看相关源码,发现了其中的问题。

首先来说下gen_server2 buffer是什么。

erlang中的每个进程都各自有一个邮箱,进程与进程通信的方式是将消息投递到对方的邮箱中,进程对邮箱中的消息采用模式匹配的方式进行处理(模式匹配涉及erlang的语法知识,这里不展开说明,读者可先简单理解为从邮箱中逐一取出消息并进行处理)

而rabbitmq自己实现了一套处理逻辑,那就是不断从邮箱中接收消息,然后放到一个buffer中,然后再不断从这个buffer中取出消息进行相应的处理。

这里说的进程邮箱也就是rabbitmq_top插件开启后,web界面上显示的Erlang mailbox;而这里说的buffer,就是web界面上显示的gen_server2 buffer。

注意:

1、这个buffer的实现实际上是一个优先级队列,每个投递给队列进程的消息,从邮箱中取出进行处理时会先回调上层模块确认消息的优先级,然后再根据优先级插入到buffer中。高优先级的消息会被放到buffer的头部,低优先级的消息会被放到buffer的尾部,队列进程每次都是从buffer的头部取消息进行处理,这就意味着,高优先级的消息会优先被处理

2、位于这个buffer中的消息都是存放在内存中的,这样就能解释为什么队列和消息都是持久化的,队列也设置了lazy属性,并且队列实际上并没有堆积很多的情况下,buffer中消息的增加会导致整体内存的增加。

既然进入队列的消息都是有优先级的,那么哪些消息是高优先级的,哪些是相对较低的。这里贴出对应的源码片段

这里也有几点要注意

1、"_"表示匹配除上面指定外的其他任何消息,也就包括正常生产的消息,GET请求的消息。

2、消费者订阅请求的消息有两个优先级,在队列生产消费速度都很低的情况下为0,反之为2。

队列进程收到生产者发送的消息后,会对生产者的通道进行monitor,如果此时生产者的通道关闭,队列进程会收到通道DOWN的消息(该消息优先级为8)。因此,就存在这么一种情况,生产者使用"短连接"的方式持续发送大量消息,队列收到这些消息并且在处理的过程中生产者通道关闭了,那么通道DOWN的消息会因为优先级较高而被插入到了buffer的头部。这个时候,队列一段时间内一直都在处理通道DOWN的消息(需要从进程字典中找到对应的credit_to记录并删除,除此之外还有其他的处理动作),对队列而言,此时的进出速率为0,因此消费者订阅的请求消息,GET请求的消息,生产者发送的消息投递到队列进程后,都会被放到buffer的尾部,必须等前面的消息都处理完后才能得到响应。

这样就很好的解释了为什么队列进程字典中有大量的credit_to记录后,一段时间内会导致消费者却无法进行订阅,也无法按从队列GET到消息了。

【总结】

排查问题需要大胆猜测,然后实测并结合源码小心验证。

【彩蛋】

结合前面提到的消息优先级,设想这么一种场景:

假如生产者发送消息后,消息还在队列的buffer中排队等待处理,此时生产者关闭了通道或TCP连接,这种情况下,生产者发送的消息还能被队列正确处理吗?如果能处理的话,是不是意味着队列进程字典中还会记录对应的credit_to信息?如果记录了,那么这个记录还有机会被删除吗?

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值