RocketMQ - 生產者原理

VipSoft發表於2023-02-16

https://rocketmq.apache.org/

Apache RocketMQ是一款開源的、分散式的訊息投遞與流資料平臺。出生自阿里巴巴,在阿里巴巴內部經歷了3個版本後,作為Apache
頂級開源專案之一直到現在。在GitHub上有10000+star、5000+fork、170+contributors(在GitHub上提交程式碼並被採納的開發者)

RocketMQ的前世

和大部分元件產生的原因類似,阿里巴巴內部為了適應淘寶 B2C 的更快、更復雜的業務,2001年啟動了“五彩石專案”,阿里巴巴的第一代訊息佇列服務Notify就是在這個背景下產生的。
2010 年,阿里巴巴內部的 Apache ActiveMQ 仍然作為核心技術被廣泛用於各個業務線,而順序訊息、海量訊息堆積、完全自主控制訊息佇列服務,也是阿里巴巴同時期急需的。在這種背景下,2011年,MetaQ 誕生

RocketMQ 雲化

2011年,LinkedIn將Kafka開源。2012年,阿里巴巴參考Kafka的設計,基於對MetaQ的理解和實際使用,研發了一套通用訊息佇列引擎,也就是 RocketMQ。自此才有了第一代真正的RocketMQ,2016年阿里雲上線雲RocketMQ訊息佇列服務。

Apache RocketMQ——金融級訊息佇列,一個擁有亞毫秒級延遲、萬億級訊息容量保證、高訊息容錯設計的中介軟體,在阿里巴巴、VIPKID、微眾銀行、民生銀行、螞蟻金服、滴滴等國內知名網際網路公司的實踐中,有著完美的表現。
隨著RocketMQ 5.0的釋出,藉助OpenMessaging提供跨平臺、多語言的能力,將會打通 Prometheus、ELK 等上游元件,透過訊息、Streaming 等形式將資料扭轉到 Flink、Elasticsearch、Hbase、Spark等下游元件。屆時整個生態體系將會更加完美、便捷。

RocketMQ支援3種訊息:普通訊息(併發訊息)、順序訊息、事務訊息
RocketMQ支援3種傳送方式:同步傳送、非同步傳送、單向傳送。

生產者概述

傳送訊息的一方被稱為生產者,它在整個RocketMQ的生產和消費體系中扮演的角色如圖所示。
image

生產者組: 一個邏輯概念,在使用生產者例項的時候需要指定一個組名。一個生產者組可以生產多個Topic的訊息。
生產者例項: 一個生產者組部署了多個程式,每個程式都可以稱為一個生產者例項。
Topic: 主題名字,一個Topic由若干Queue組成。

RocketMQ 客戶端中的生產者有兩個獨立實現類 :org.apache.rocketmq.client.producer.DefaultMQProducer 和org.apache.rocketmq.client.producer.TransactionMQProducer 。 前者用於生產普通訊息、順序訊息、單向訊息、批次訊息、延遲訊息,後
者主要用於生產事務訊息

訊息結構和訊息型別

訊息類的核心欄位

public class Message implements Serializable {
	private static final long serialVersionUID = 8445773977080406428L;
	// 主題名字,可以透過RocketMQ Console建立
	private String topic;
	// 目前沒用
	private int flag;
	// 訊息擴充套件資訊,Tag、keys、延遲級別都儲存在這裡
	private Map<String, String> properties;
	// 訊息體,位元組陣列。需要注意生產者使用什麼編碼,消費者也必須使用相同編碼解碼,否則會產生亂碼
	private bytel[] body;
	// 設定訊息的 key,多個 key 可 以 用MessageConst.KEY_SEPARATOR(空格)分隔或者直接用另一個過載方法。如果 Broker 中 messageIndexEnable=true 則會根據 key建立訊息的Hash索引,幫助使用者進行快速查詢。
	public void setKeys(String keys) {}	
	public void setKeys(Collection<String> keys){}
	// 訊息過濾的標記,使用者可以訂閱某個Topic的某些 Tag,這樣Broker只會把訂閱了topic-tag的訊息傳送給消費者。
	public void setTags(String tags) {}
	// 設定延遲級別,延遲多久消費者可以消費
	public void setDelayTimeLevel(int level) { )
	public void setTopic(String topic) { }
	// 如果還有其他擴充套件資訊,可以存放在這裡。內部是一個Map,重複呼叫會覆蓋舊值。
	public void putUserProperty(final String name, final String value) {...}
}

普通訊息

普通訊息也稱為併發訊息,和傳統的佇列相比,併發訊息沒有順序,但是生產消費都是並行進行的,單機效能可達十萬級別的TPS

分割槽有序訊息

與Kafka中的分割槽類似,把一個Topic訊息分為多個分割槽“儲存”和消費,在一個分割槽內的訊息就是傳統的佇列,遵循FIFO(先進先出)原則。

全域性有序訊息

如果把一個 Topic 的分割槽數設定為 1,那麼該Topic 中的訊息就是單分割槽,所有訊息都遵循FIFO(先進先出)的原則。

延遲訊息

訊息傳送後,消費者要在一定時間後,或者指定某個時間點才可以消費。在沒有延遲訊息時,基本的做法是基於定時計劃任務排程,定時傳送訊息。在 RocketMQ中只需要在傳送訊息時設定延遲級別即可實現

事務訊息

主要涉及分散式事務,即需要保證在多個操作同時成功或者同時失敗時,消費者才能消費訊息。RocketMQ透過傳送Half訊息、處理本地事務、提交(Commit)訊息或者回滾(Rollback)訊息優雅地實現分散式事務。

生產者高可用

客戶端保證

第一種保證機制:重試機制

RocketMQ 支援同步、非同步傳送,不管哪種方式都可以在配置失敗後重試,如果單個 Broker 發生故障,重試會選擇其他 Broker 保證訊息正常傳送。
配置項 retryTimesWhenSendFailed表示同步重試次數,預設為 2次,加上正常傳送 1次,總共3次機會。

同步傳送的重試: 程式碼可以參考org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendDefaultImpl(),每次傳送失敗後,除非傳送被打斷否則都會執行重試程式碼。

非同步傳送重試: 程式碼可以參考org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessageAsync()
重試是在通訊層非同步傳送完成的,當operationComplete()方法返回的response值為null時,會重新執行重試程式碼。返回值 response為 null 通常是因為客戶端收到 TCP請求解包失敗,或者沒有找到匹配的request
生產者配置項 retryTimesWhenSendAsyncFailed 表示非同步重試的次數,預設為 2 次,加上正常傳送的1次,總共有3次傳送機會。

第二種保證機制:客戶端容錯

RocketMQ Client會維護一個“Broker-傳送延遲”關係,根據這個關係選擇一個傳送延遲級別較低的 Broker 來傳送訊息,這樣能最大限度地利用 Broker 的能力,剔除已經當機、不可用或者傳送延遲級別較高的 Broker,儘量保證訊息的正常傳送。

這種機制主要體現在傳送訊息時如何選擇 Queue,原始碼在 MQFaultStrategy.selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName)方法中

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
    if (this.sendLatencyFaultEnable) {
        try {
            //第一步:獲取一個在延遲上可以接受,並且和上次傳送相同的Broker。首先獲取一個自增序號 index,透過取模獲取Queue的位置下標 Pos。如果 Pos對應的 Broker的延遲時間是可以接受的,並且是第一次傳送,或者和上次傳送的Broker相同,則將Queue返回。
            int index = tpInfo.getSendWhichQueue().incrementAndGet();
            for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                if (pos < 0)
                    pos = 0;
                MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                if (latencyFaultTolerance.isAvailable(mq.getBrokerName()))
                    return mq;
            }
            //第二步:如果第一步沒有選中一個Broker,則選擇一個延遲較低的Broker。
            final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
            int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
            if (writeQueueNums > 0) {
                final MessageQueue mq = tpInfo.selectOneMessageQueue();
                if (notBestBroker != null) {
                    mq.setBrokerName(notBestBroker);
                    mq.setQueueId(tpInfo.getSendWhichQueue().incrementAndGet() % writeQueueNums);
                }
                return mq;
            } else {
                latencyFaultTolerance.remove(notBestBroker);
            }
        } catch (Exception e) {
            log.error("Error occurred when selecting message queue", e);
        }

        return tpInfo.selectOneMessageQueue();
    }
    //第三步:如果第一、二步都沒有選中一個Broker,則隨機選擇一個Broker
    return tpInfo.selectOneMessageQueue(lastBrokerName);
}

tpInfo.selectOneMessageQueue(lastBrokerName) 該方法的功能就是隨機選擇一個Broker

public MessageQueue selectOneMessageQueue(final String lastBrokerName)
{
	//第一步 如果沒有上次使用的Broker作為參考,那麼隨機選擇一個Broker。
	if (lastBrokerName == null) {
		return selectOneMessageQueue ();
	} else {//第二步 如果存在上次使用的Broker,就選擇非上次使用的 Broker,目的是均勻地分散Broker的壓力
		int index = this.sendwhichQueue.getAndIncrement();
		for (int i = 0;i < this.messageQueueList.size(); i++){
			int pos = Math.abs(index++)  this.messageQueueList.size();
			if (pos <0)
				pos =0;
			MessageQueue mg = this.messageQueueList.get(pos);
			if (!mq.getBrokerName().equals(lastBrokerName)) {
				return mq;
			}
		}
		//第三步 如果第一、二步都沒有選中一個Broker,則採用兜底方案——隨機選擇一個Broker
		return selectOneMessageQueue();
	}
}

Broker 端保證

資料同步方式保證:在後面 Broker章節中會講到 Broker主從複製分為兩種:同步複製和非同步複製。同步複製是指訊息傳送到MasterBroker後,同步到Slave Broker才算傳送成功;非同步複製是指訊息傳送到Master Broker,即為傳送成功。在生產環境中,建議至少部署2個Master和2個Slave,下面分為幾種情況詳細描述。

  • 1個Slave掉電。Broker同步複製時,生產第一次傳送失敗,重試到另一組Broker後成功;Broker非同步複製時,生產正常不受影響。
  • 2個Slave掉電。Broker同步複製時,生產失敗;Broker非同步複製時,生產正常不受影響。
  • 1個Master掉電。Broker 同步複製時,生產第一次失敗,重試到另一組 Broker後成功;Broker非同步複製時的做法與同步複製相同。
  • 2個Master掉電。全部生產失敗。
  • 同一組Master和Slave掉電。Broker同步複製時,生產第一次傳送失敗,重試到另一組Broker後成功;Broker非同步複製時,生產正常不受影響。
  • 2組機器都掉電:全部生產失敗。

綜上所述,想要做到絕對的高可靠,將 Broker 配置的主從同步進行複製即可,只要生產者收到訊息儲存成功的反饋,訊息就肯定不會丟失。一般適用於金融領域的特殊場景。絕大部分場景都可以配置Broker主從非同步複製,這樣效率極高

相關文章