參考資料
-
Rocketmq官網:http://rocketmq.apache.org/
-
Rocketmq的其它專案:https://github.com/apache/rocketmq-externals
-
Rocketmq-console安裝:https://blog.csdn.net/zzzgd_666/article/details/81387237
RocketMQ的引數指南
NameServer配置屬性
#broker名字,注意此處不同的配置檔案填寫的不一樣
brokerClusterName=rocketmqcluster
brokerName=broker-a
#0 表示 Master, >0 表示 Slave
brokerId=0
#nameServer地址,分號分割
namesrvAddr=rocketmq-nameserver1:9876;rocketmq-nameserver2:9876
#這個配置可解決雙網路卡,傳送訊息走外網的問題,這裡配上內網ip就可以了
brokerIP1=10.30.51.149
#在傳送訊息時,自動建立伺服器不存在的topic,預設建立的佇列數
defaultTopicQueueNums=8
#是否允許 Broker 自動建立Topic,建議線下開啟,線上關閉
autoCreateTopicEnable=false
#是否允許 Broker 自動建立訂閱組,建議線下開啟,線上關閉
autoCreateSubscriptionGroup=true
#Broker 對外服務的監聽埠
listenPort=10911
#刪除檔案時間點,預設凌晨 0點
deleteWhen=03
#檔案保留時間,預設 48 小時
fileReservedTime=48
#commitLog每個檔案的大小預設1G
mapedFileSizeCommitLog=1073741824
#ConsumeQueue每個檔案預設存30W條,根據業務情況調整
mapedFileSizeConsumeQueue=1000000
destroyMapedFileIntervalForcibly=120000
redeleteHangedFileInterval=120000
#檢測物理檔案磁碟空間
diskMaxUsedSpaceRatio=88
#儲存路徑
storePathRootDir=/app/data/rocketmq/data
#commitLog 儲存路徑
storePathCommitLog=/app/data/rocketmq/data/commitlog
#消費佇列儲存路徑儲存路徑
storePathConsumeQueue=/app/data/rocketmq/data/consumerqueue
#訊息索引儲存路徑
storePathIndex=/app/data/rocketmq/data/index
#checkpoint 檔案儲存路徑
storeCheckpoint=/app/data/rocketmq/data/checkpoint
#abort 檔案儲存路徑
abortFile=/app/data/rocketmq/data/abort
#限制的訊息大小 修改為16M
maxMessageSize=16777216
#傳送佇列等待時間
waitTimeMillsInSendQueue=3000
osPageCacheBusyTimeOutMills=5000
flushCommitLogLeastPages=12
flushConsumeQueueLeastPages=6
flushCommitLogThoroughInterval=30000
flushConsumeQueueThoroughInterval=180000
#Broker 的角色
#- ASYNC_MASTER 非同步複製Master
#- SYNC_MASTER 同步雙寫Master
#- SLAVE
brokerRole=ASYNC_MASTER
#刷盤方式
#- ASYNC_FLUSH 非同步刷盤
#- SYNC_FLUSH 同步刷盤
flushDiskType=ASYNC_FLUSH
#checkTransactionMessageEnable=false
#發訊息執行緒池數量
sendMessageThreadPoolNums=80
#拉訊息執行緒池數量
pullMessageThreadPoolNums=128
useReentrantLockWhenPutMessage=true
Rocketmq 傳送控制流程
針對前4種 broker busy ,主要是由於 Broker 在追加訊息時持有的鎖時間超過了設定的1s,Broker 為了自我保護,會丟擲錯誤,客戶端會選擇其他 broker 伺服器進行重試。
如果對不是金融級服務,建議將 transientStorePoolEnable = true,可以有效避免前面 4 種 broker ,因為開啟這個引數,訊息首先會儲存在堆外記憶體中,並且 RocketMQ 提供了記憶體鎖定的功能,其追加效能能得到一定的保障,這樣可以做到在記憶體使用層面的讀寫分離,即寫訊息是直接寫入堆外記憶體,消費訊息直接從 pagecache中讀,然後定時將堆外記憶體的訊息寫入 pagecache。
但這種方案隨之帶來的就是可能存在訊息丟失,如果對訊息非常嚴謹的話,建議擴容叢集,或遷移topic到新的叢集。
可以看出來,丟擲這種錯誤,在 broker 還沒有傳送“嚴重”的 pagecache 繁忙,即訊息追加到記憶體中的最大時延沒有超過 1s,通常追加是很快的,絕大部分都會低於1ms,但可能會由於出現一個超過200ms的追加時間,導致排隊中的任務等待時間超過了200ms,則此時會觸發broker 端的快速失敗,讓請求快速失敗,便於客戶端快速重試。但是這種請求並不是實時的,而是每隔10s 檢查一遍。
值得注意的是,一旦出現 TIMEOUT_CLEAN_QUEUE,可能在一個點會有多個這樣的錯誤資訊,具體多少與當前積壓在待傳送佇列中的個數有關。
Rocketmq 傳送時異常
system busy 和 broker busy 解決方案
- [REJECTREQUEST]system busy too many requests and system thread pool busy
- [PC_SYNCHRONIZED]broker busy
- [PCBUSY_CLEAN_QUEUE]broker busy
- [TIMEOUT_CLEAN_QUEUE]broker busy
之前寫的解決方案,都是基於測試環境測試的.到生產環境之後,正常使用沒有問題,生產環境壓測時,又出現了system busy異常(簡直崩潰)
com.alibaba.rocketmq.client.exception.MQBrokerException: CODE: 2 DESC: [TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: 208ms, size of queue: 8
For more information, please visit the url, http://docs.aliyun.com/cn#/pub/ons/faq/exceptions&unexpected_exception
at com.alibaba.rocketmq.client.impl.MQClientAPIImpl.processSendResponse(MQClientAPIImpl.java:455)
at com.alibaba.rocketmq.client.impl.MQClientAPIImpl.sendMessageSync(MQClientAPIImpl.java:272)
at com.alibaba.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:253)
at com.alibaba.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:215)
at com.alibaba.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendKernelImpl(DefaultMQProducerImpl.java:671)
at com.alibaba.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(DefaultMQProducerImpl.java:440)
at com.alibaba.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:1030)
at com.alibaba.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:989)
at com.alibaba.rocketmq.client.producer.DefaultMQProducer.send(DefaultMQProducer.java:90)
at
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:748)
報錯定位
-
cleanExpiredRequestInQueue會處理髮送訊息、拉取訊息、心跳、事務訊息佇列中的資料,此次遇到的問題是傳送Topic訊息報出來的錯誤,所以接下來針對傳送訊息流程進行分析。
-
報出此錯誤的原始碼位置為broker快速失敗機制BrokerFastFailure.java類(該類在Broker啟動時會啟動一個定時任務,每10毫秒執行一次),報錯位置程式碼如下:
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) {
}
}
}
這段程式碼是Broker快速失敗機制的核心程式碼,如果一個等待佇列的頭元素(也就是第一個要處理或者正在處理的元素)等待時間超過該佇列設定的最大等待時間,則丟棄該元素物件的任務,並對這個請求返回[TIMEOUT_CLEAN_QUEUE]broker busy異常資訊。
傳送Topic訊息報該錯誤
sendThreadPoolQueue取出頭元素,轉換成對應的任務,判斷任務在佇列存活時間是否超過了佇列設定的最大等待時間,如果超過了則組裝處理返回物件response,response的code為RemotingSysResponseCode.SYSTEM_BUSY,內容為:
[TIMEOUT_CLEAN_QUEUE]broker busy, start flow control for a while, period in queue: [當前任務在佇列存活時間], size of queue: [當前佇列的長度]
MQClientAPIImpl.processSendResponse處理返回response,根據response.getCode()的處理分支,最終返回MQBrokerException異常,response分支處理程式碼如下:
// 只有ResponseCode.SUCCESS的情況下返回結果,其他情況丟擲MQBrokerException異常
private SendResult processSendResponse(
final String brokerName,
final Message msg,
final RemotingCommand response
) throws MQBrokerException, RemotingCommandException {
switch (response.getCode()) {
case ResponseCode.FLUSH_DISK_TIMEOUT:
case ResponseCode.FLUSH_SLAVE_TIMEOUT:
case ResponseCode.SLAVE_NOT_AVAILABLE: {
}
case ResponseCode.SUCCESS: {
// 省略部分程式碼
return sendResult;
}
default:
break;
}
throw new MQBrokerException(response.getCode(), response.getRemark());
}
訊息傳送客戶端接收到MQBrokerException異常資訊,捕獲異常處理中不符合訊息重試邏輯,直接丟擲該異常,也就是使用者看到的;
// timesTotal為訊息生產者設定的傳送失敗重試次數
for (; times < timesTotal; times++) {
String lastBrokerName = null == mq ? null : mq.getBrokerName();
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
mq = mqSelected;
brokersSent[times] = mq.getBrokerName();
try {
// 省略部分程式碼
} catch (RemotingException e) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
log.warn(msg.toString());
exception = e;
continue;
} catch (MQClientException e) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
log.warn(msg.toString());
exception = e;
continue;
} catch (MQBrokerException e) {
// 此處為MQBrokerException異常處理邏輯,RemotingSysResponseCode.SYSTEM_BUSY不符合分支條件,最終throw e丟擲異常
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
log.warn(msg.toString());
exception = e;
switch (e.getResponseCode()) {
case ResponseCode.TOPIC_NOT_EXIST:
case ResponseCode.SERVICE_NOT_AVAILABLE:
case ResponseCode.SYSTEM_ERROR:
case ResponseCode.NO_PERMISSION:
case ResponseCode.NO_BUYER_ID:
case ResponseCode.NOT_IN_CURRENT_UNIT:
continue;
default:
if (sendResult != null) {
return sendResult;
}
throw e;
}
} catch (InterruptedException e) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
log.warn(String.format("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
log.warn(msg.toString());
log.warn("sendKernelImpl exception", e);
log.warn(msg.toString());
throw e;
}
} else {
break;
}
}
生產環境各種引數:
-
broker busy異常: 可通過增大 waitTimeMillsInSendQueue 解決
-
system busy異常:可通過增大 osPageCacheBusyTimeOutMills 解決
#傳送佇列等待時間
waitTimeMillsInSendQueue=3000
#系統頁面快取繁忙超時時間(翻譯),預設值 1000
osPageCacheBusyTimeOutMills=5000
出現問題分析
出現異常的原因是因為我們同一臺伺服器部署的多個應用造成的。我們一臺伺服器上部署了 三個ES、八個redis、一個rocketmq ,壓力測試時這些都在使用,雖然cpu、記憶體都還有很大剩餘,但是磁碟io和記憶體頻率畢竟只有那麼多可能已經佔滿,或者還有其他都會有影響。
之前測試環境測試其他東西時,發現mq和redis同時大量使用時,redis速度會降低三到四倍,由此可見應用分伺服器部署的重要性。以前知道會有影響,沒想到影響這麼大。
最終結解決方案:應該給rocketmq單獨部署效能較高的伺服器.
記一次 rocketmq 使用時的異常。
問題分析總結
- system busy , start flow control for a while
該異常會造成 訊息丟失。
- broker busy , start flow control for a while
該異常不會造成訊息丟失。
問題解決過程
1、最開始時候 ,測試發現在效能好的伺服器上只會出現system busy,也就是說出現異常就會訊息丟失。
所以:業務程式碼進行處理,出現異常就會重發到當前topic的bak佇列,當時想的是既然這個topic busy了,就換到另外的topic去發,總不能都 busy吧。也算是臨時解決了。
2、發現有訊息重複的現象。不用想肯定是報broker busy異常,重發到topic的 bak佇列了。又因為broker busy可能不會造成訊息丟失,所以訊息重複就出現了。
解決方案:
修改rocketmq配置檔案:
-
方案一:sendMessageThreadPoolNums 改成 1 ,沒有的話新增一行。sendMessageThreadPoolNums=1
-
方案二:useReentrantLockWhenPutMessage改成true,沒有的話新增一行。
sendMessageThreadPoolNums=32
useReentrantLockWhenPutMessage=true
sendMessageThreadPoolNums這個屬性是傳送執行緒池大小, rocketmq4.1版本之後預設為 1,之前版本預設什麼不知道但是肯定大於1。這個屬性改成1的話,就不用管useReentrantLockWhenPutMessage這個屬性了;
如果改成大於1,就需要將useReentrantLockWhenPutMessage這個屬性設定為 true;
目前測試 未發現這兩個方案有什麼區別,sendMessageThreadPoolNums=1 時也支援多執行緒傳送,傳送速度感覺和 sendMessageThreadPoolNums大於1沒有區別,都能跑滿100M的網路卡。
感覺如果useReentrantLockWhenPutMessage=true的時候,就是開啟鎖,然後關鍵程式碼其實還是單執行緒處理;
解決方案
- 業務邏輯處理中進行異常捕獲,如果捕獲到異常為MQBrokerException並且responseCode為2則重發訊息;
- 修改broker的預設傳送訊息任務佇列等待時長waitTimeMillsInSendQueue(單位: 毫秒);
除此之外,還可以觀察報錯時磁碟的IO情況,出現這種錯誤很有可能是當時的磁碟IO很高,導致訊息落盤時間變長。