在使用 RocketMQ 的过程中,有时候我们会看到下面的日志:
[TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: 206ms, size of queue: 5
这是因为 RocketMQ 触发了流量控制。生产者把消息写入 Broker,Consumer 从 Broker 拉取消息。Broker 是 RocketMQ 的核心 ,触发流量控制主要就是为了防止 Broker 压力过大而宕机。
Broker流控
RockerMQ 默认采用异步刷盘策略,Producer 把消息发送到 Broker 后,Broker 会先把消息写入 Page Cache,刷盘线程定时地把数据从 Page Cache 刷到磁盘上。
那Broker Busy是如何产生的呢?
Broker 默认是开启快速失败的,处理逻辑类是 BrokerFastFailure,这个类中有一个定时任务用来清理过期的请求,每 10 ms 执行一次。
- Page Cache 繁忙 清理过期请求之前首先会判断 Page Cache 是否繁忙,如果繁忙,就会给 Producer 返回一个系统繁忙的状态码, 也就是开头的日志。那怎么判断 Page Cache 繁忙呢?Broker 收到一条消息后会追加到 Page Cache 或者内存映射文件,这个过程首先获取一个 CommitLog 写入锁,如果持有锁的时间大于 osPageCacheBusyTimeOutMills(默认 1s,可以配置),就认为 Page Cache 繁忙。具体代码见 DefaultMessageStore 类 isOSPageCacheBusy 方法。
- 清理过期请求 清理过期请求时,如果请求线程的创建时间到当前系统时间间隔大于 waitTimeMillsInSendQueue(默认 200ms,可以配置)就会清理这个请求,然后给 Producer 返回一个系统繁忙的状态码。
System Busy
在开启 transientStorePoolEnable 的情况下,写入消息时会先写入堆外内存(DirectByteBuffer),然后刷入 Page Cache,最后刷入磁盘。而读取消息是从 Page Cache,这样可以实现读写分离,避免读写都在 Page Cache 带来的问题
-
当发生拒绝请求时,就会返回系统繁忙的响应。判断依据是在开启 transientStorePoolEnable 配置的情况下,是否还有可用的 ByteBuffe。
-
线程池拒绝
Broker 收到请求后,会把处理逻辑封装成到 Runnable 中,由线程池来提交执行,如果线程池满了就会拒绝请求(这里线程池中队列的大小默认是 10000,可以通过参数 sendThreadPoolQueueCapacity 进行配置),线程池拒绝后会抛出异常 RejectedExecutionException,程序捕获到异常后,会判断是不是单向请求(OnewayRPC),如果不是,就会给 Producer 返回一个系统繁忙的状态码。
消息重试
Broker 发生流量控制的情况下,返回给 Producer 系统繁忙的状态码(code=2),Producer 收到这个状态码是不会进行重试的。
消费流控
DefaultMQPushConsumerImpl 类中有 Consumer 流控的逻辑 。
缓存消息数量超过阈值
ProcessQueue 保存的消息数量超过阈值(默认 1000,可以配置)
缓存消息大小超过阈值
ProcessQueue 保存的消息大小超过阈值(默认 100M,可以配置)
缓存消息跨度超过阈值
对于非顺序消费的场景,ProcessQueue 中保存的最后一条和第一条消息偏移量之差超过阈值(默认 2000,可以配置)
获取锁失败
对于顺序消费的情况,ProcessQueue 加锁失败,也会延迟拉取,这个延迟时间默认是 3s,可以配置。