現(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) {
}
}
}
這段代碼邏輯:
- 如果當(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):
- PutMessageSpinLock 自旋鎖。
- 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ù):
waitTimeMillsInSendQueue 這個(gè)最有效,默認(rèn)值是 200,可稍微改的大一點(diǎn)。例如 250,300,切不可過大,過大會導(dǎo)致積壓過多請求,而且大部分都是無效的。更會引起后面的請求都無效。
sendMessageThreadPoolNums 發(fā)送消息處理線程數(shù),可以改的大一點(diǎn),但治標(biāo)不治本。
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)壓力。