问题
最近线上出现了一起消费重试导致集群节点内存飙高,生产者消息发送失败。
现象:集群中一个队列的slave
进程疯狂占用内存,master
进程内存正常
RMQ如何实现消费重试
实现重试、延时消费的关键
Dead-Lettered Messages
(死信消息)主要来源于:
rejected - the message was rejected with requeue=false
expired - the TTL of the message expired
Dead Letter Exchanges
(死信转发的Exchange)
Dead Letter Routing Key
(死信转发的Routing Key)
消息被发到一个中转队列(#retry 或者 #delay),然后经过TTL
后, 消息被死信Exchange路由到目标队列中,从而达到重试或延时消费的效果。
分析
- 服务消费异常,大量消息进入重试队列(retry#1 retry#2 retry#3)
- 由于重试利用上述的消息 TTL 机制,然后通过死信转发到原队列。三个Level的重试消息 TTL 后打入原队列,由于 QPS 较高,大量消息消费失败进行重试的极端情况下,原队列需要多承受上面
3
倍的流量。而 RMQ 队列实现本身是一个 Erlang 进程,QPS 过高,队列进程可能来不及处理。
为什么队列slave
进程内存飙高,master
进程正常
初步猜测:
retry队列中的消息经过TTL后,被内部死信转发到原始队列(队列master、slaves进程都会收到)。slaves 进程收到消息后,会缓存在内存中。正常情况下:
master 进程通过 gm 把消息通过环状链表广播到 slaves 进程。当 master 对应的 gm 进程再次收到其广播的消息时,会再次广播 ack 消息,其作用是通知 slaves 进程可以清除缓存的消息。
由于线上采用惰性队列,队列进程收到消息会尽可能早地进行持久化到磁盘(通过 rabbit_msg_store 进程),而当内部重试流量叠加导致大量消息打到队列,rabbit_msg_store 持久化进程可能处理不过来,造成队列进程阻塞。因此 master 进程内存没有出现飙高现象,而 slaves 进程收到消息先进行缓存,等待 master 进程广播 ack 消息后才能进行清除。但由于 master 进程可能被阻塞,而内部重试过来的流量(没有credit_flow
)连续发往 slaves 进程,导致 slaves 进程一直在缓存发往它的消息,造成其进程内存继续飙高。
为什么会影响生产者发送消息
channel 进程向队列进程发消息一般采用了credit_flow
机制
case Flow of
%% Here we are tracking messages sent by the rabbit_channel
%% process. We are accessing the rabbit_channel process
%% dictionary.
flow -> [credit_flow:send(QPid) || QPid <- QPids],
[credit_flow:send(QPid) || QPid <- SPids];
noflow -> ok
end,
由于内部重试流量叠加,队列进程来不及处理,会很快耗尽 rabbit_channel
进程的 credit
值,造成 rabbit_channel
进程被限流阻塞;同理限流过程会往rabbit_channel
进程上游传播,进而阻塞rabbit_reader
进程,从而导致服务端读取 socket 消息变慢,导致接收缓冲区不足,与线上监控的TcpExt.pruneCalled
指标告警一致。这样导致了生产者发送消息异常。
解决方法
- 启动远程Shell
erl -name sj@test -setcookie $COOKIE
- 打印节点 N 占用内存 Top 5 进程列表
rpc:call(N, erlang, apply, [fun()->lists:sublist(lists:reverse(lists:keysort(2, [{P, process_info(P, memory)} || P <- processes()])), 5) end, []]).
结合 rabbitmq_top 插件 Top Processes 进程列表,确认问题进程后,接下来进行 Kill 操作。
- Kill异常slave进程
exit(pid(x1,x2,x3),kill).