RocketMQ 源码解析:当 Broker 抛出“Broker Busy”时,它到底在忙什么?

3 阅读6分钟

在日常使用 RocketMQ 的过程中,你可能遇到过这样的报错:CODE: 2 DESC: [TIMEOUT_CLEAN_QUEUE]broker busy。这行日志通常意味着 Broker 端触发了自我保护机制,拒绝了你的消息发送请求。

很多开发者看到“Broker Busy”的第一反应是服务器负载过高,但事实真的如此吗?更有趣的是,明明默认超时阈值是 200ms,为什么日志里记录的却是 401ms、799ms 这种远超阈值的数字?

今天,我们将深入 RocketMQ 的源码,一探究竟,看看 Broker 在什么情况下会拒绝服务,以及它是如何做出决策的。


异常现象分析

首先,让我们看一段典型的异常日志:

[ERROR] ... org.apache.rocketmq.client.exception.MQBrokerException: CODE: 2  DESC: [TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: 401ms, size of queue: 34

日志中的关键信息如下:

  • CODE: 2:这是 RocketMQ 定义的系统繁忙错误码。
  • TIMEOUT_CLEAN_QUEUE:这是触发流控的具体原因,意为“在队列中等待超时”。
  • period in queue: 401ms:请求在 Broker 端的排队耗时。
  • size of queue: 34:当前阻塞队列中的积压请求数量。

结论:并不是 Broker 的 CPU 或内存爆了,而是消息发送请求在 Broker 的处理队列中等待的时间超过了阈值,被 Broker 主动“清理”并拒绝了。


核心解惑:为什么日志时间是 401ms 而不是 200ms?

在深入源码之前,必须先解决一个最反直觉的疑问:既然 RocketMQ 的默认流控阈值(waitTimeMillsInSendQueue)是 200ms,为什么日志里显示的排队时间(period in queue)经常是 400ms、800ms 甚至更多?

这涉及到 RocketMQ 流控机制的核心原理:它是“被动检测”,而非“主动中断”。

1. 200ms 是“门槛”,不是“熔断线”

如果你的理解是:“系统里有一个监控线程,盯着每个请求,一旦它呆了 200ms 就把它踢出去”,那么日志确实应该都在 200ms 左右。

但 RocketMQ 的逻辑是:请求进入队列后,只能乖乖排队。只有当业务线程终于轮到处理这个请求(从队列里把它拿出来)时,才会回头计算它已经等了多久。

2. 积压越严重,超时越离谱

想象一下医院挂号:

  • 规则:排队超过 200ms 的病人直接退号。
  • 现实:你前面排了 1000 个人,医生处理速度很慢。
  • 结果:哪怕你实际上已经排了 799ms,医生(业务线程)才终于叫到你的号。这时医生一看时间:“哎呀,你都排了 799ms 了,超过 200ms 了,退号(抛出异常)。”

结论:日志中的 401ms799ms 代表的是请求实际在队列中“坐牢”的时长。这个数值越大,说明 Broker 端的消息积压越严重,处理速度远远跟不上写入速度,导致请求在队列里“睡”了很久才被处理(或被清理)。


源码深度解析:Broker 端的流控逻辑

RocketMQ 的通信层基于 Netty 实现。当 Producer 发送消息时,请求会被封装成 RemotingCommand,通过 Netty 传输到 Broker。Broker 端接收到请求后,会将其放入一个业务线程池的阻塞队列中等待处理。

这个“排队超时”的检测逻辑,就发生在请求被线程池取出并执行的瞬间。

关键类:NettyRemotingAbstract

在 Broker 端的 NettyRemotingAbstract 类中,有一个核心方法 processRequestCommand。当请求被 Netty 线程接收后,会被包装成一个任务提交给业务线程池。

真正执行请求的逻辑在 RequestTaskrun 方法中(这是 NettyRemotingAbstract 的内部类):

// NettyRemotingAbstract.java

class RequestTask implements Runnable {
    // ... 省略其他字段
    private final long createTimestamp = System.currentTimeMillis(); // 1. 记录请求进入队列的时间
    private final long beginTimestamp = System.currentTimeMillis();
    
    @Override
    public void run() {
        try {
            // 2. 如果是发送消息的请求,且开启了流控检查
            if (requestHeader != null && requestHeader.isOneway() == false) {
                // 计算排队耗时:当前时间 - 请求创建时间
                long waitTime = System.currentTimeMillis() - createTimestamp;
                
                // 3. 核心判断:如果排队时间 > 阈值 (默认200ms)
                if (waitTime > brokerController.getMessageStoreConfig().getWaitTimeMillsInSendQueue()) {
                    // 4. 拒绝请求,返回 SYSTEM_BUSY (CODE: 2)
                    log.warn("process request over long time, {} ms, release", waitTime);
                    RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY,
                        String.format("[TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: %sms, size of queue: %d", 
                        waitTime, 
                        brokerController.getSendThreadPoolQueue().size()));
                    
                    // 写回响应给客户端
                    ctx.writeAndFlush(response);
                    return;
                }
            }
            // ... 正常处理请求
        } catch (Exception e) {
            // ...
        }
    }
}

源码逻辑拆解

  1. 记录时间戳:当请求刚进入 Netty 层,准备提交给线程池时,createTimestamp 被记录为当前时间。
  2. 入队等待:请求任务进入 BlockingQueue。此时,如果 Broker 负载高,线程池繁忙,这个任务就会在队列里积压。
  3. 取出执行:假设过了 401ms,线程池终于有空闲线程了,把这个任务从队列里拿出来,开始执行 run() 方法。
  4. 事后诸葛亮:代码执行到 long waitTime = System.currentTimeMillis() - createTimestamp;,此时算出来的 waitTime 就是 401ms。
  5. 判定流控401ms > 200ms(默认阈值 waitTimeMillsInSendQueue),条件成立。
  6. 抛出异常:Broker 构造一个 SYSTEM_BUSY 的响应返回给客户端,客户端收到后抛出 MQBrokerException

解决方案与调优建议

既然知道了原理,解决 broker busy 的思路就清晰了:要么让请求别排队(扩容),要么让队列别太挤(限流/调参)。

1. 临时方案:调大阈值

如果你确认 Broker 的机器负载(CPU、IO)并不高,只是因为偶尔的毛刺导致排队,可以尝试调大 Broker 的配置参数:

  • 参数名waitTimeMillsInSendQueue
  • 默认值:200
  • 建议:可以调整为 300 或 400,但这只是治标不治本。

2. 根本方案:提升处理能力

日志里的 period in queue 很大,说明处理速度跟不上。

  • 增加 Broker 节点:分摊流量。
  • 检查磁盘 IO:RocketMQ 是写磁盘的,如果磁盘 IO 达到瓶颈(如 %util 接近 100%),处理速度会急剧下降,导致队列积压。
  • 检查 PageCache:确保操作系统的 PageCache 充足,避免频繁的磁盘换页。

3. 客户端优化

  • 重试机制:Producer 端遇到 broker busy 时,RocketMQ 客户端默认会自动重试。确保你的重试配置合理。
  • 降级处理:在业务层捕获该异常,进行适当的降级或异步缓冲。

总结

RocketMQ 的 broker busy 异常是 Broker 端的一种自我保护机制

  • 触发原因:请求在 Broker 的发送线程池队列中等待时间过长。
  • 时间悖论:日志中的时间(如 401ms)是请求实际排队时长,它远超阈值(200ms)恰恰证明了队列积压的严重性。
  • 核心逻辑:Broker 不会在请求等待时主动杀掉它,而是在请求终于轮到被处理时,发现它已经“过期”了,于是拒绝服务。

理解了这一点,下次再看到 period in queue 很大时,你就知道该去检查 Broker 的处理性能瓶颈了。