RocketMQ 消息发送system busy、broker busy、RemotingTooMuchRequestException原因分析与解决方案

1、broker busy

现象描述

1、code=2,remark="[PCBUSY_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: %sms, size of queue: %d

2、org.apache.rocketmq.client.exception.MQBrokerException: CODE: 2  DESC: [TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: 201ms, size of queue: 420 BROKER: XX.XX.XX.XX:10911

根源分析

RockerMQ 默认采用异步刷盘策略,Producer 把消息发送到 Broker 后,Broker 会先把消息写入 Page Cache,刷盘线程定时地把数据从 Page Cache 刷到磁盘上,如下图:

那 broker busy 是怎么导致的呢?

Broker 默认是开启快速失败的,处理逻辑类是 BrokerFastFailure,这个类中有一个定时任务用来清理过期的请求,每 10 ms 执行一次,代码如下:

public void start() {
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            if (brokerController.getBrokerConfig().isBrokerFastFailureEnable()) {
                cleanExpiredRequest();
            }
        }
    }, 1000, 10, TimeUnit.MILLISECONDS);
}

Page Cache 繁忙

清理过期请求之前首先会判断 Page Cache 是否繁忙,如果繁忙,就会给 Producer 返回一个系统繁忙的状态码(code=2,remark="[PCBUSY_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: %sms, size of queue: %d"),也就是本文开头的异常日志。那怎么判断 Page Cache 繁忙呢?Broker 收到一条消息后会追加到 Page Cache 或者内存映射文件,这个过程首先获取一个 CommitLog 写入锁,如果持有锁的时间大于 osPageCacheBusyTimeOutMills(默认 1s,可以配置),就认为 Page Cache 繁忙。具体代码见 DefaultMessageStore 类 isOSPageCacheBusy 方法。

清理过期请求

清理过期请求时,如果请求线程的创建时间到当前系统时间间隔大于 waitTimeMillsInSendQueue(默认 200ms,可以配置)就会清理这个请求,然后给 Producer 返回一个系统繁忙的状态码(code=2,remark="[TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: %sms, size of queue: %d")

解决方案

1、PCBUSY_CLEAN_QUEUE---修改osPageCacheBusyTimeOutMills参数

修改配置文件application.yml:

#系统页面缓存繁忙超时时间(翻译),默认值 1000
osPageCacheBusyTimeOutMills=3000

2、TIMEOUT_CLEAN_QUEUE---修改waitTimeMillsInSendQueue参数

修改配置文件application.yml:

#发送队列等待时间,默认200
waitTimeMillsInSendQueue=2000

时间大小可以通过压测调试,增加并发,增大等待时间,看看是否还有报错。

2、RemotingTooMuchRequestException

现象描述

org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException: invokeAsync call the addr[XX.XX.XX.XX:10911] timeout

根源分析

RemotingTooMuchRequestException的根源是并发过高,服务端负载太大,导致请求处理不过来但是RocketMQ的TPS单机是几万,集群是3主6从,也就是TPS可以达到10w左右,不可能业务团队可以承受的TPS,到RocketMQ端处理不过来,所以问题没那么简单,于是打开源码看一下问题出现的原因。源码中出现问题的代码定位如下:

这段代码的逻辑是选择本次消息发送的队列,并发送消息,异常产生的原因是处理超时了,超时时间默认是3s。

解决方案

修改配置文件application.yml

# 发送消息超时时间,默认3000
sendMessageTimeout: 5000

3、 system busy

现象描述

[REJECTREQUEST]system busy, start flow control for a while

根源分析

这个异常在 NettyRemotingAbstract#processRequestCommand 方法,

  1. 拒绝请求

如果 NettyRequestProcessor 拒绝了请求,就会给 Producer 返回一个系统繁忙的状态码(code=2,remark="[REJECTREQUEST]system busy, start flow control for a while")。那什么情况下请求会被拒绝呢?看下面这段代码:

//SendMessageProcessor类
public boolean rejectRequest() {
    return this.brokerController.getMessageStore().isOSPageCacheBusy() ||
        this.brokerController.getMessageStore().isTransientStorePoolDeficient();
}

从代码中可以看到,请求被拒绝的情况有两种可能,一个是 Page Cache 繁忙,另一个是 TransientStorePoolDeficient。

跟踪 isTransientStorePoolDeficient 方法,发现判断依据是在开启 transientStorePoolEnable 配置的情况下,是否还有可用的 ByteBuffer。

注意:在开启 transientStorePoolEnable 的情况下,写入消息时会先写入堆外内存(DirectByteBuffer),然后刷入 Page Cache,最后刷入磁盘。而读取消息是从 Page Cache,这样可以实现读写分离,避免读写都在 Page Cache 带来的问题如果对消息非常严谨的话,建议扩容集群,或迁移topic到新的集群。如下图:

  1. 线程池拒绝

Broker 收到请求后,会把处理逻辑封装成到 Runnable 中,由线程池来提交执行,如果线程池满了就会拒绝请求(这里线程池中队列的大小默认是 10000,可以通过参数 sendThreadPoolQueueCapacity 进行配置),线程池拒绝后会抛出异常 RejectedExecutionException,程序捕获到异常后,会判断是不是单向请求(OnewayRPC),如果不是,就会给 Producer 返回一个系统繁忙的状态码(code=2,remark="[OVERLOAD]system busy, start flow control for a while")

判断 OnewayRPC 的代码如下,flag = 2 或者 3 时是单向请求:

public boolean isOnewayRPC() {
    int bits = 1 << RPC_ONEWAY;
    return (this.flag & bits) == bits;
}

解决方案

根源分析里提到sendThreadPoolQueueCapacity这个参数,发现配置了不起作用。具体的解决方案还是通过osPageCacheBusyTimeOutMills参数的配置,降低Page Cache 繁忙

修改配置文件application.yml:

#系统页面缓存繁忙超时时间(翻译),默认值 1000
osPageCacheBusyTimeOutMills=3000

4、消息重试

Broker 发生流量控制的情况下,返回给 Producer 系统繁忙的状态码(code=2),Producer 收到这个状态码是不会进行重试的。下面是会进行重试的响应码:

//DefaultMQProducer类
private final Set<Integer> retryResponseCodes = new CopyOnWriteArraySet<Integer>(Arrays.asList(
    ResponseCode.TOPIC_NOT_EXIST,
    ResponseCode.SERVICE_NOT_AVAILABLE,
    ResponseCode.SYSTEM_ERROR,
    ResponseCode.NO_PERMISSION,
    ResponseCode.NO_BUYER_ID,
    ResponseCode.NOT_IN_CURRENT_UNIT
));

所以,出现broker busy和system busy的情况下,消息发送失败是不会重试的。如果需要重试,需要获取异常类型,增加重试机制:

if (e instanceof MQBrokerException) {
      MQBrokerException mqBrokerException = (MQBrokerException) e;
      //出现broker busy或者system busy错误需要重试
      if (ResponseCode.SYSTEM_BUSY == mqBrokerException.getResponseCode()) {
          rocketMQProducer.asyncSend(topic, tag, message);
      }
}

5、总结

system busy、broker busy这个错误,其本质是系统的PageCache繁忙,通俗一点讲就是向PageCache追加消息时,单个消息发送占用的时间超过1s了,如果继续往该Broker服务器发送消息并等待,其TPS根本无法满足,哪还是高性能的消息中间了呀。故才会采用快速失败机制,直接给消息发送者返回错误,消息发送者默认情况会重试2次,将消息发往其他Broker,保证其高可用。

目前通过生产环境、压测5000TPS的情况下,各种参数修改测试得出:

broker busy异常: 可通过增大 waitTimeMillsInSendQueue 解决

system busy异常:可通过增大 osPageCacheBusyTimeOutMills 解决

但是如果并发更大,TPS很大的情况下,还不能从根本上解决问题。最根源的办法还是,broker服务器扩容,增加JVM内存,把机械盘换成SSD。在压测环境下,JVM内存从8G->16G,SSD盘,TPS能达到6000,性能有很大提升。

警惕!这 8 个场景下 RocketMQ 会发生流量控制-腾讯云开发者社区-腾讯云

RocketMQ 消息发送system busy、broker busy原因分析与解决方案_rocketmq dbsc: rejectreqlest,system busy-CSDN博客

RocketMQ发送消息还有这种坑?遇到SYSTEM_BUSY不重试?_阿里云 rocketmq 不重试-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值