RocketMQ 異常分析 [TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a whil

現(xiàn)象:

在對 RMQ 做集群壓測時(shí),偶現(xiàn) [TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a whil 異常,對系統(tǒng)正確率有一定影響,所以決定一探究竟。

全局搜索代碼

首先,clone 了一波代碼,全局搜了一下,在 BrokerFastFailure 這個(gè)類里的 cleanExpiredRequestInQueue 方法看到了:

    void cleanExpiredRequestInQueue(final BlockingQueue<Runnable> blockingQueue, final long maxWaitTimeMillsInQueue) {
        while (true) {
            try {
                if (!blockingQueue.isEmpty()) {
                    final Runnable runnable = blockingQueue.peek();
                    if (null == runnable) {
                        break;
                    }
                    final RequestTask rt = castRunnable(runnable);
                    if (rt == null || rt.isStopRun()) {
                        break;
                    }

                    final long behind = System.currentTimeMillis() - rt.getCreateTimestamp();
                    if (behind >= maxWaitTimeMillsInQueue) {//
                        if (blockingQueue.remove(runnable)) {
                            rt.setStopRun(true);
                            rt.returnResponse(RemotingSysResponseCode.SYSTEM_BUSY, String.format("[TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: %sms, size of queue: %d", behind, blockingQueue.size()));
                        }
                    } else {
                        break;
                    }
                } else {
                    break;
                }
            } catch (Throwable ignored) {
            }
        }
    }

這段代碼邏輯:

  1. 如果當(dāng)前時(shí)間減去這個(gè)請求的創(chuàng)建時(shí)間 > 配置的最大等待時(shí)間。就刪除掉這個(gè)請求,并拋出這個(gè)異常。在調(diào)用 returnResponse 方法時(shí),會直接調(diào)用 netty 的 writeAndFlush 方法寫回?cái)?shù)據(jù)。

而這個(gè)方法會被 4 個(gè)地方調(diào)用:

樓主在每個(gè)調(diào)用后面設(shè)置了默認(rèn)值:
發(fā)送時(shí):最大 200ms
pull 消息時(shí): 最大 5 秒
心跳: 31 秒
事務(wù): 3 秒。

而我們的系統(tǒng)都是在 send 消息時(shí),報(bào)錯(cuò)的,看來,是隊(duì)列里請求等待超過 200 毫秒了。

再回頭看看 cleanExpiredRequest 這個(gè)方法。

這個(gè)方法被一個(gè) 10ms 間隔的定時(shí)任務(wù)執(zhí)行的。因此,最多也就超過 210 毫秒,就會拋異常。而這個(gè)方法還有另外一個(gè)邏輯:

 while (this.brokerController.getMessageStore().isOSPageCacheBusy()) {// 如果寫不進(jìn)去.
            try {
                if (!this.brokerController.getSendThreadPoolQueue().isEmpty()) {
                    final Runnable runnable = this.brokerController.getSendThreadPoolQueue().poll(0, TimeUnit.SECONDS);
                    if (null == runnable) {
                        break;
                    }

                    final RequestTask rt = castRunnable(runnable);
                    rt.returnResponse(RemotingSysResponseCode.SYSTEM_BUSY, String.format("[PCBUSY_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: %sms, size of queue: %d", System.currentTimeMillis() - rt.getCreateTimestamp(), this.brokerController.getSendThreadPoolQueue().size()));
                } else {
                    break;
                }
            } catch (Throwable ignored) {
            }
        }

如果操作系統(tǒng) PageCache 很忙,也拋出 PCBUSY 異常,忙的意思是:往 MQ 文件里寫是需要一把鎖的,如果上次上鎖的時(shí)間截止到現(xiàn)在,超過了 1 秒,就表示忙,就不等了,就拋出異常。

明顯,200ms 的隊(duì)列排隊(duì)超時(shí),更容易被觸發(fā)。

那有沒有可能,把這個(gè)隊(duì)列對應(yīng)的線程池搞的大一點(diǎn)呢?

我們看看源碼:
sendThreadPoolQueueCapacity = 10000;
sendMessageThreadPoolNums = 1;

發(fā)現(xiàn)這個(gè)隊(duì)列的大小是 10000。線程池的大小是 1(max 和 core 都是 1).

為什么是1?

注釋是這么說的:

thread numbers for send message thread pool, since spin lock will be used by default since 4.0.x, the default value is 1.

意思是,4.0.x 版本后使用自旋鎖,所以默認(rèn)值是1.

這個(gè)鎖說的是 PutMessageLock,在 RMQ 里,有 2 種實(shí)現(xiàn):

  1. PutMessageSpinLock 自旋鎖。
  2. PutMessageReentrantLock JDK 非公平重入鎖。

默認(rèn)使用自旋鎖,但是,自旋鎖適合在并發(fā)低的時(shí)候使用,說白了就是樂觀鎖,樂觀鎖適合并發(fā)低的時(shí)候使用,悲觀鎖適合在并發(fā)高的時(shí)候使用。

為什么使用一個(gè)線程?因?yàn)槎鄠€(gè)線程沒有效果,往 PageCache 里寫數(shù)據(jù)是上鎖的,最耗時(shí)的就在這部分,開啟多個(gè)線程在這里搶鎖是沒有意義的。而且,在高并發(fā)下,多個(gè)線程自旋搶鎖,CPU 可能會爆炸。

那用重入鎖呢?大部分時(shí)候,MQ 的壓力都沒那么大,使用自旋鎖,能夠減少 CPU 上下文切換,提高性能。這是一個(gè)權(quán)衡。如果你把線程池搞了多個(gè) 線程,那就使用重入鎖吧。但多線程確實(shí)沒意義。

所以,線程池這部分,我們最好不要修改,也就是說,你即使增加線程數(shù),也解決不了這個(gè)問題。另外,如果不是 1,在使用絕對順序消息時(shí),是無法保證消息的順序的。相當(dāng)于多個(gè)線程處理一個(gè)隊(duì)列的消息,順序一定會錯(cuò)亂。這個(gè)千萬要注意。

根本的原因還是 PageCache 刷盤的時(shí)候,會有毛刺,超時(shí)超過 200ms,就會偶發(fā)這種現(xiàn)象。

總結(jié)

總結(jié)一下:
這個(gè)錯(cuò)誤是因?yàn)檎埱笤陉?duì)列里待的太久了,如果是 send 請求,就是 200 多毫秒。因此,會被 Server 端認(rèn)為這是過期的請求。

相關(guān)參數(shù):

  1. waitTimeMillsInSendQueue 這個(gè)最有效,默認(rèn)值是 200,可稍微改的大一點(diǎn)。例如 250,300,切不可過大,過大會導(dǎo)致積壓過多請求,而且大部分都是無效的。更會引起后面的請求都無效。

  2. sendMessageThreadPoolNums 發(fā)送消息處理線程數(shù),可以改的大一點(diǎn),但治標(biāo)不治本。

  3. brokerFastFailureEnable 這個(gè)參數(shù)用于控制是否進(jìn)行掃描,就是每 10ms 執(zhí)行 cleanExpiredRequest 那個(gè)定時(shí)任務(wù),可以修改為 false,但不建議這么做。

最后,這個(gè)錯(cuò)誤的根本原因猜測是因?yàn)椴僮飨到y(tǒng) IO 抖動,因?yàn)榭吹骄W(wǎng)上很多 tps 很低,也會出現(xiàn)這個(gè)情況。具體還需要詳細(xì)的測試和排查。

通常,大家會使用重試策略,解決這個(gè)異常,但是可能會引起消息重復(fù),所以,如果對消息重復(fù)敏感,做冪等是必須的。

如果 CPU 和 磁盤負(fù)載很高,出現(xiàn)這種問題就很正常了,建議增加 Broker 服務(wù)器,分擔(dān)壓力。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • ReentrantLock 介紹 一個(gè)可重入的互斥鎖,它具有與使用{synchronized}方法和語句訪問的隱式...
    tomas家的小撥浪鼓閱讀 4,098評論 1 4
  • 引用自多線程編程指南應(yīng)用程序里面多個(gè)線程的存在引發(fā)了多個(gè)執(zhí)行線程安全訪問資源的潛在問題。兩個(gè)線程同時(shí)修改同一資源有...
    Mitchell閱讀 2,019評論 1 7
  • 1.寫出synchronized的使用方式 synchronized的三種應(yīng)用方式 synchronized關(guān)鍵字...
    wuyuan0127閱讀 307評論 0 1
  • 在一個(gè)方法內(nèi)部定義的變量都存儲在棧中,當(dāng)這個(gè)函數(shù)運(yùn)行結(jié)束后,其對應(yīng)的棧就會被回收,此時(shí),在其方法體中定義的變量將不...
    Y了個(gè)J閱讀 4,436評論 1 14
  • 線程池ThreadPoolExecutor corepoolsize:核心池的大小,默認(rèn)情況下,在創(chuàng)建了線程池之后...
    irckwk1閱讀 756評論 0 0