【RocketMq-Producer】訊息傳送者引數詳解

Xander發表於2022-11-22

rocketmq 【RocketMq-生產者】訊息傳送者引數詳解

引言

首先注意本次討論的RokcetMq原始碼版本為 4.9.4,距離5.0釋出 的沒有多久。

這一節針對RocketMq的生產者請求傳送的部分細節進行闡述,主要包含了下面的內容:DefaultMQProducer 為生產者預設物件,這個物件繼承自 ClientConfig,裡面包含了請求者的通用配置,所以可以拆分為兩個部分進行理解,第一部分為ClientConfig,第二部分為DefaultMQProducer。

ClientConfig 部分

ClientConfig 定義了一些配置的獲取方法,定義了名稱空間等引數。無論是訊息的傳送者還是消費者都是通用的。

下面根據本次的版本的原始碼介紹相關引數。

名稱描述引數型別預設值有效值重要性
namesrvAddrNameServer的地址列表String從-D系統引數rocketmq.namesrv.addr或環境變數。NAMESRV_ADDR
instanceName客戶端例項名稱String從-D系統引數rocketmq.client.name獲取,否則就是DEFAULT
clientIP客戶端IPStringRemotingUtil.getLocalAddress()
namespace客戶端名稱空間String
accessChannel設定訪問通道AccessChannelLOCAL
clientCallbackExecutorThreads客戶端通訊層接收到網路請求的時候,處理器的核數intRuntime.getRuntime().availableProcessors()
pollNameServerInterval輪詢從NameServer獲取路由資訊的時間間隔int30000,單位毫秒
heartbeatBrokerInterval定期傳送註冊心跳到broker的間隔int30000,單位毫秒
persistConsumerOffsetInterval作用於Consumer,持久化消費進度的間隔int預設值5000,單位毫秒
pullTimeDelayMillsWhenException拉取訊息出現異常的延遲時間設定long1000,單位毫秒
unitName單位名稱String
unitMode單位模式booleanfalse
vipChannelEnabled是否啟用vip netty通道以傳送訊息boolean從-D com.rocketmq.sendMessageWithVIPChannel引數的值,若無則是true
useTLS是否使用安全傳輸。boolean從-D系統引數tls.enable獲取,否則就是false
mqClientApiTimeoutmq客戶端api超時設定int3000,單位毫秒
language客戶端實現語言LanguageCodeLanguageCode.JAVA

namesrvAddr

NameServer 的地址列表。

clientIp

private String clientIP = RemotingUtil.getLocalAddress();

從程式碼中可以看到,使用RemotingUtil#getLocalAddress 獲取IP資訊,在當前版本中預設返回不是127.0或者192.168開頭的 IPV4地址,否則嘗試獲取IPV6的地址,如果都找不到就用LocalHost地址。

instanceName

private String instanceName = System.getProperty("rocketmq.client.name", "DEFAULT");

instanceName主要獲取當前預設的系統引數客戶端例項名稱,它是客戶端標識 CID 的組成部分

unitName 單元名稱

也是CID的組成部分之一,如果獲取 NameServer 的地址是透過 URL 進行動態更新的話,會透過這個單元名稱進行附加,用來區分不同的NameServer地址服務。

clientCallbackExecutorThreads 回撥執行緒池數量

表示public回撥執行緒池的數量,預設為CPU的核數,通常這個值直接根據JVM獲取的結果為基準即可。

private int clientCallbackExecutorThreads = Runtime.getRuntime().availableProcessors();

namespace 名稱空間

4.5.1 之後才加入的新機制。主要適用場景為全鏈路壓測的時候可以利用不同的名稱空間劃分出真實訊息和壓測訊息,使得線上業務正常執行的情況下同步處理測試流程。

pollNameServerInterval NameServer同步間隔

生產者客戶端預設每隔出30S向NameServer 更新Topic的相關資訊,注意這個引數在消費端同樣存在相同的配置,這個配置通常不建議修改。

/**  
 * Pulling topic information interval from the named server */
   private int pollNameServerInterval = 1000 * 30;

heartbeatBrokerInterval Broker心跳間隔

客戶端向 Broker 傳送心跳包的時間間隔,預設為 30s,不建議修改該值。

/**  
 * Heartbeat interval in microseconds with message broker */
   private int heartbeatBrokerInterval = 1000 * 30;

persistConsumerOffsetInterval

客戶端持久化訊息消費進度的間隔,預設為 5s,該值不建議修改。

/**  
 * Offset persistent interval for consumer */
   private int persistConsumerOffsetInterval = 1000 * 5;

DefaultMQProducer 部分

這部分定義了日誌和常見的使用訊息佇列方法,注意在類的開頭定義了一個 transient 變數執行內部的保護方法。

官方文件中極少DefaultMQProducer配置如下:

名稱描述引數型別預設值有效值重要性
producerGroup生產組的名稱,一類Producer的標識StringDEFAULT_PRODUCER
createTopicKey傳送訊息的時候,如果沒有找到topic,若想自動建立該topic,需要一個key topic,這個值即是key topic的值StringTopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC
defaultTopicQueueNums自動建立topic的話,預設queue數量是多少int4
sendMsgTimeout預設的傳送超時時間int3000,單位毫秒
compressMsgBodyOverHowmuc訊息body需要壓縮的閾值int1024 * 4,4K
retryTimesWhenSendFailed同步傳送失敗的話,rocketmq內部重試多少次int2
retryTimesWhenSendAsyncFailed非同步傳送失敗的話,rocketmq內部重試多少次int2
retryAnotherBrokerWhenNotStoreOK傳送的結果如果不是SEND_OK狀態,是否當作失敗處理而嘗試重發booleanfalse
maxMessageSize客戶端驗證,允許傳送的最大訊息體大小int1024 1024 4,4M
traceDispatcher非同步傳輸資料介面TraceDispatchernull

DefaultMQProducerImpl 內部物件

defaultMQProducerImpl 比較意思,因為此物件是 DefaultMQProducerImpl 整個實現類的實際呼叫者,這裡用了受保護的內部物件完成所有方法呼叫,用final是規避舊版本多個執行緒初始化物件非原子性的問題,同時保證持有的內部物件不可變。

/**  
 * Wrapping internal implementations for virtually all methods presented in this class. */
protected final transient DefaultMQProducerImpl defaultMQProducerImpl;
為什麼這裡要用 transient?
transient 關鍵字確保物件被序列化之後不會洩漏 DefaultMQProducerImpl 物件。

InternalLogger 日誌物件

接著是日誌物件,日誌物件 InternalLogger 如下定義,內部實現比較簡單,基本是一些info和debug日誌列印。

InternalLogger log = ClientLogger.getLog()

客戶端日誌的實現類儲存路徑時是:${user.home}/logs/rocketmqlogs/rocketmq_client.log,這個路徑的獲取細節在org.apache.rocketmq.client.log.ClientLogger#createClientAppender可以看到有關細節。使用System.getProperty("user.home")獲取的路徑在Unix系統中相當於使用者的主目錄。

user.home 如果是 xxx 則是 /usr/home/xxx 為開始,比如個人的Mac電腦最終的存放地址為:/Users/zxd/logs/rocketmqlogs/rocketmq_client.log

producerGroup 訊息組

表示傳送者所屬組定義如下,根據註釋可以得知,gropu 可以實現生產者例項的聚合,主要用在事務的的時候需要使用到,而如果是非事務的訊息,每一個程式都是唯一的,彼此沒有關聯。

有關事務的內容涉及需要用到Broker反查機制,這裡不做過多牽扯,繼續介紹。

/**  
 * Producer group conceptually aggregates all producer instances of exactly same role, which is particularly * important when transactional messages are involved. </p>  
 *  
 * For non-transactional messages, it does not matter as long as it's unique per process. </p>  
 *  
 * See <a href="http://rocketmq.apache.org/docs/core-concept/">core concepts</a> for more discussion.  
 */
 private String producerGroup;

我們可以透過相關命令或者視覺化工具檢視傳送者所屬組的狀態。注意預設的主題佇列數量,RocketMq預設設定為4。

這裡用了volatile保證多執行緒對於主題佇列的數量時可見的,多個生產者例項觀察的數量是一致的。

/**  
 * Number of queues to create per default topic. */private volatile int defaultTopicQueueNums = 4;

sendMsgTimeout 訊息傳送預設超時時間

訊息預設傳送的超時時間為3秒,

注意的是在 RocketMQ 4.3.0 版本之前由於存在重試機制,程式設定的設計為單次重試的超時時間,即如果設定重試次數為 3 次,則 DefaultMQProducer#send 方法可能會超過 9s 才返回。

/**  
 * Timeout for sending messages. */
   private int sendMsgTimeout = 3000;

主要的改動點在org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl 這個物件裡面

修復的方式比較簡單粗暴,是增加一個納秒值進行計算 ,如果請求時間超過傳送請求的時間太久就丟擲異常。下一次請求對應的扣除掉本次耗費的時間再進行重試,如果重試超過的總時間超過超時時間也同樣丟擲異常。

這就意味著如果超時次數設定10次,可能不到10次就會因為超時時間的判斷丟擲異常資訊。

long costTimeAsync = System.currentTimeMillis() - beginStartTime;  
if (timeout < costTimeAsync) {  
    throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");  
}

compressMsgBodyOverHowmuch 壓縮閾值

預設情況下,如果訊息的長度超過4K,那麼RocketMq預設會對於訊息開啟壓縮,雖然會增加CPU的效能損耗,但是可以有效減少網路方便的開銷。

/**  
 * Compress message body threshold, namely, message body larger than 4k will be compressed on default. */
 // 壓縮訊息體閾值,即預設壓縮大於4k的訊息體。
   private int compressMsgBodyOverHowmuch = 1024 * 4;
private boolean tryToCompressMessage(final Message msg) {  
    // 批次資料目前不支援壓縮
    if (msg instanceof MessageBatch) {  
        //batch does not support compressing right now  
        return false;  
    }  
    byte[] body = msg.getBody();  
    if (body != null) {  
        if (body.length >= this.defaultMQProducer.getCompressMsgBodyOverHowmuch()) {  
            try {  
                // 壓縮之後的資料
                byte[] data = compressor.compress(body, compressLevel);  
                if (data != null) {  
                    msg.setBody(data);  
                    return true;                }  
            } catch (IOException e) {  
                log.error("tryToCompressMessage exception", e);  
                log.warn(msg.toString());  
            }  
        }  
    }  
  
    return false;  
}

retryTimesWhenSendFailed 失敗重試

同步訊息傳送重試次數。RocketMQ 客戶端內部在訊息傳送失敗時預設會重試 2 次。該引數與 sendMsgTimeout 聯合生效,但是需要注意這個引數在SYNC模式下才會重試2次,如果是其他模式則預設是一次失敗不再進行重試。

在SYNC模式只重試一次可以看下面程式碼:

int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;

retryTimesWhenSendAsyncFailed 非同步訊息重試

見名知義,非同步訊息傳送重試次數,預設為 2,即重試 2 次,一共有 3 次機會。關鍵的程式碼在org.apache.rocketmq.client.impl.MQClientAPIImpl#onExceptionImpl 這個引數巨多的方法當中,簡單判斷當前的非同步訊息總的重試次數,如果重試多次超過次數則透過sendCallback回撥傳送異常。

/**  
 * Maximum number of retry to perform internally before claiming sending failure in synchronous mode. </p>  
 *  
 * This may potentially cause message duplication which is up to application developers to resolve. */
   private int retryTimesWhenSendFailed = 2;

retryAnotherBrokerWhenNotStoreOK 失敗向其他Broker重試

根據方法的本意按照道理來說如果客戶端收到的結果不是 SEND_OK,應該直接向另外一個 Broker 重試,但根據程式碼分析目前這個引數並不能按預期運作,官方一致也沒有關注過這個問題。

  
/**  
 * Maximum number of retry to perform internally before claiming sending failure in asynchronous mode. </p>  
 *  
 * This may potentially cause message duplication which is up to application developers to resolve. */
   private int retryTimesWhenSendAsyncFailed = 2;

maxMessageSize 最大訊息體

允許傳送的最大訊息體,預設為 4M,具體可以看下面的判斷,注意Broker也有 maxMessageSize 這個引數的設定,故客戶端的設定不能超過服務端的配置:

客戶端的傳送限制如下:

/**  
 * Maximum allowed message body size in bytes. */
   private int maxMessageSize = 1024 * 1024 * 4; // 4M

...

if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {  
    throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL,  
        "the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize());  
}

maxMessageSize 另一個使用地點是在RocketMq的軌跡訊息長度判斷中,不過這一塊的程式碼在2022年的上半年被某位大神大改最佳化過,裡面的最佳化程式碼比較值得學習,但是因為這一塊牽扯的內容比較大部頭需要先放放,我們看其他引數內容。

// 軌跡訊息中累計到3/4左右的時候就進行合併提交
if (currentMsgSize >= traceProducer.getMaxMessageSize() - 10 * 1000) {  
    List<TraceTransferBean> dataToSend = new ArrayList(traceTransferBeanList);  
    AsyncDataSendTask asyncDataSendTask = new AsyncDataSendTask(traceTopicName, regionId, dataToSend);  
    traceExecutor.submit(asyncDataSendTask);  
  
    this.clear();  
  
}

sendLatencyFaultEnable 失敗延遲規避

失敗規避機制預設為false,它的含義是當Product向Broker傳送訊息失敗之後,客戶端的在內部重試的時候會規避掉上一次傳送失敗的Broker,並且一段時間內不會再向該Broker進行傳送。

notAvailableDuration 不可用延遲陣列

不可用延遲陣列,利用等比數列的時間傳送訊息,根據陣列的設定在多少時間內不向Broker傳送訊息。從預設值可以看到這裡是按照階層的方式進行增長的。

private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};

latencyMax 延遲最大值

設定訊息傳送的最大延遲級別,同樣涉及了延遲推送機制。這裡暫時略過。

private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};

MqAdmin

定義了一些基礎的規範介面,由於和我們平時寫業務程式碼的Service Interface類似,這裡不在過多展開介紹,而是簡單羅列一些比較常用的介面:


/**
-   String key:根據 key 查詢 Broker,即新主題建立在哪些 Broker 上
-   String newTopic:主題名稱
-   int queueNum:主題佇列個數
-   int topicSysFlag:主題的系統引數
*/
void createTopic(String key, String newTopic, int queueNum, int topicSysFlag)
 
/**
    根據佇列與時間戳,從訊息消費佇列中查詢訊息,返回訊息的物理偏移量(在 commitlog 檔案中的偏移量)。
    MessageQueue mq:訊息消費佇列
    long timestamp:時間戳
*/
long searchOffset(MessageQueue mq, long timestamp)

 /**
 查詢訊息消費佇列當前最大的邏輯偏移量,在 consumequeue 檔案中的偏移量。
 */
long maxOffset(final MessageQueue mq)
    
 /**
 查詢訊息消費佇列當前最小的邏輯偏移量。
 */
long minOffset(final MessageQueue mq) 
    
/**
返回訊息消費佇列中第一條訊息的儲存時間戳。
*/
long earliestMsgStoreTime(MessageQueue mq)
    
/**
根據訊息的物理偏移量查詢訊息
*/
MessageExt viewMessage(String offsetMsgId)
    
/**
根據主題與訊息的全域性唯一 ID 查詢訊息。
*/    
MessageExt viewMessage(String topic, String msgId)
    
/**
批次查詢訊息,其引數列表如下:

String topic:主題名稱
String key:訊息索引 Key
int maxNum:本次查詢最大返回訊息條數
long begin:開始時間戳
long end:結束時間戳
*/
QueryResult queryMessage(String topic, String key, int maxNum, long begin,long end)

寫在最後

簡單的進行一些API講解,我們可以下具體使用到之後再來本文查閱會更有實際意義。

相關文章