深入剖析 RocketMQ 原始碼 - 負載均衡機制

vivo網際網路技術發表於2022-04-07

一、引言

RocketMQ是一款優秀的分散式訊息中介軟體,在各方面的效能都比目前已有的訊息佇列要好,RocketMQ預設採用長輪詢的拉模式, 單機支援千萬級別的訊息堆積,可以非常好的應用在海量訊息系統中。

RocketMQ主要由 Producer、Broker、Consumer、Namesvr 等元件組成,其中Producer 負責生產訊息,Consumer 負責消費訊息,Broker 負責儲存訊息,Namesvr負責儲存後設資料,各元件的主要功能如下:

  • 訊息生產者(Producer):負責生產訊息,一般由業務系統負責生產訊息。一個訊息生產者會把業務應用系統裡產生的訊息傳送到Broker伺服器。RocketMQ提供多種傳送方式,同步傳送、非同步傳送、順序傳送、單向傳送。同步和非同步方式均需要Broker返回確認資訊,單向傳送不需要。

  • 訊息消費者(Consumer):負責消費訊息,一般是後臺系統負責非同步消費。一個訊息消費者會從Broker伺服器拉取訊息、並將其提供給應用程式。從使用者應用的角度而言提供了兩種消費形式:拉取式消費、推動式消費。

  • 代理伺服器(Broker Server):訊息中轉角色,負責儲存訊息、轉發訊息。代理伺服器在RocketMQ系統中負責接收從生產者傳送來的訊息並儲存、同時為消費者的拉取請求作準備。代理伺服器也儲存訊息相關的後設資料,包括消費者組、消費進度偏移和主題和佇列訊息等。

  • 名字服務(Name Server):名稱服務充當路由訊息的提供者。生產者或消費者能夠通過名字服務查詢各主題相應的Broker IP列表。多個Namesrv例項組成叢集,但相互獨立,沒有資訊交換。

  • 生產者組(Producer Group):同一類Producer的集合,這類Producer傳送同一類訊息且傳送邏輯一致。如果傳送的是事務訊息且原始生產者在傳送之後崩潰,則Broker伺服器會聯絡同一生產者組的其他生產者例項以提交或回溯消費。

  • 消費者組(Consumer Group):同一類Consumer的集合,這類Consumer通常消費同一類訊息且消費邏輯一致。消費者組使得在訊息消費方面,實現負載均衡和容錯的目標變得非常容易。

RocketMQ整體訊息處理邏輯上以Topic維度進行生產消費、物理上會儲存到具體的Broker上的某個MessageQueue當中,正因為一個Topic會存在多個Broker節點上的多個MessageQueue,所以自然而然就產生了訊息生產消費的負載均衡需求。

本篇文章分析的核心在於介紹RocketMQ的訊息生產者(Producer)和訊息消費者(Consumer)在整個訊息的生產消費過程中如何實現負載均衡以及其中的實現細節。

二、RocketMQ的整體架構

(圖片來自於Apache RocketMQ)

RocketMQ架構上主要分為四部分,如上圖所示:

  • Producer:訊息釋出的角色,支援分散式叢集方式部署。Producer通過MQ的負載均衡模組選擇相應的Broker叢集佇列進行訊息投遞,投遞的過程支援快速失敗並且低延遲。

  • Consumer:訊息消費的角色,支援分散式叢集方式部署。支援以push推,pull拉兩種模式對訊息進行消費。同時也支援叢集方式和廣播方式的消費,它提供實時訊息訂閱機制,可以滿足大多數使用者的需求。

  • NameServer:NameServer是一個非常簡單的Topic路由註冊中心,支援分散式叢集方式部署,其角色類似Dubbo中的zookeeper,支援Broker的動態註冊與發現。

  • BrokerServer:Broker主要負責訊息的儲存、投遞和查詢以及服務高可用保證,支援分散式叢集方式部署。

RocketMQ的Topic的物理分佈如上圖所示:

Topic作為訊息生產和消費的邏輯概念,具體的訊息儲存分佈在不同的Broker當中。

Broker中的Queue是Topic對應訊息的物理儲存單元。

在RocketMQ的整體設計理念當中,訊息的生產消費以Topic維度進行,每個Topic會在RocketMQ的叢集中的Broker節點建立對應的MessageQueue。

producer生產訊息的過程本質上就是選擇Topic在Broker的所有的MessageQueue並按照一定的規則選擇其中一個進行訊息傳送,正常情況的策略是輪詢。

consumer消費訊息的過程本質上就是一個訂閱同一個Topic的consumerGroup下的每個consumer按照一定的規則負責Topic下一部分MessageQueue進行消費。

在RocketMQ整個訊息的生命週期內,不管是生產訊息還是消費訊息都會涉及到負載均衡的概念,訊息的生成過程中主要涉及到Broker選擇的負載均衡,訊息的消費過程主要涉及多consumer和多Broker之間的負責均衡。

三、producer訊息生產過程

producer訊息生產過程:

  • producer首先訪問namesvr獲取路由資訊,namesvr儲存Topic維度的所有路由資訊(包括每個topic在每個Broker的佇列分佈情況)。

  • producer解析路由資訊生成本地的路由資訊,解析Topic在Broker佇列資訊並轉化為本地的訊息生產的路由資訊。

  • producer根據本地路由資訊向Broker傳送訊息,選擇本地路由中具體的Broker進行訊息傳送。

3.1 路由同步過程

public class MQClientInstance {
 
    public boolean updateTopicRouteInfoFromNameServer(final String topic) {
        return updateTopicRouteInfoFromNameServer(topic, false, null);
    }
 
 
    public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
        DefaultMQProducer defaultMQProducer) {
        try {
            if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
                try {
                    TopicRouteData topicRouteData;
                    if (isDefault && defaultMQProducer != null) {
                        // 省略對應的程式碼
                    } else {
                        // 1、負責查詢指定的Topic對應的路由資訊
                        topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
                    }
 
                    if (topicRouteData != null) {
                        // 2、比較路由資料topicRouteData是否發生變更
                        TopicRouteData old = this.topicRouteTable.get(topic);
                        boolean changed = topicRouteDataIsChange(old, topicRouteData);
                        if (!changed) {
                            changed = this.isNeedUpdateTopicRouteInfo(topic);
                        }
                        // 3、解析路由資訊轉化為生產者的路由資訊和消費者的路由資訊
                        if (changed) {
                            TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
 
                            for (BrokerData bd : topicRouteData.getBrokerDatas()) {
                                this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
                            }
 
                            // 生成生產者對應的Topic資訊
                            {
                                TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
                                publishInfo.setHaveTopicRouterInfo(true);
                                Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQProducerInner> entry = it.next();
                                    MQProducerInner impl = entry.getValue();
                                    if (impl != null) {
                                        impl.updateTopicPublishInfo(topic, publishInfo);
                                    }
                                }
                            }
                            // 儲存到本地生產者路由表當中
                            this.topicRouteTable.put(topic, cloneTopicRouteData);
                            return true;
                        }
                    }
                } finally {
                    this.lockNamesrv.unlock();
                }
            } else {
            }
        } catch (InterruptedException e) {
        }
 
        return false;
    }
}

路由同步過程

  • 路由同步過程是訊息生產者傳送訊息的前置條件,沒有路由的同步就無法感知具體發往那個Broker節點。

  • 路由同步主要負責查詢指定的Topic對應的路由資訊,比較路由資料topicRouteData是否發生變更,最終解析路由資訊轉化為生產者的路由資訊和消費者的路由資訊。

public class TopicRouteData extends RemotingSerializable {
    private String orderTopicConf;
    // 按照broker維度儲存的Queue資訊
    private List<QueueData> queueDatas;
    // 按照broker維度儲存的broker資訊
    private List<BrokerData> brokerDatas;
    private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}
 
 
public class QueueData implements Comparable<QueueData> {
    // broker的名稱
    private String brokerName;
    // 讀佇列大小
    private int readQueueNums;
    // 寫佇列大小
    private int writeQueueNums;
    // 讀寫許可權
    private int perm;
    private int topicSynFlag;
}
 
 
public class BrokerData implements Comparable<BrokerData> {
    // broker所屬叢集資訊
    private String cluster;
    // broker的名稱
    private String brokerName;
    // broker對應的ip地址資訊
    private HashMap<Long/* brokerId */, String/* broker address */> brokerAddrs;
    private final Random random = new Random();
}
 
 
--------------------------------------------------------------------------------------------------
 
 
public class TopicPublishInfo {
    private boolean orderTopic = false;
    private boolean haveTopicRouterInfo = false;
    // 最細粒度的佇列資訊
    private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>();
    private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();
    private TopicRouteData topicRouteData;
}
 
public class MessageQueue implements Comparable<MessageQueue>, Serializable {
    private static final long serialVersionUID = 6191200464116433425L;
    // Topic資訊
    private String topic;
    // 所屬的brokerName資訊
    private String brokerName;
    // Topic下的佇列資訊Id
    private int queueId;
}

路由解析過程:

  • TopicRouteData核心變數QueueData儲存每個Broker的佇列資訊,BrokerData儲存Broker的地址資訊。

  • TopicPublishInfo核心變數MessageQueue儲存最細粒度的佇列資訊。

  • producer負責將從namesvr獲取的TopicRouteData轉化為producer本地的TopicPublishInfo。

public class MQClientInstance {
 
    public static TopicPublishInfo topicRouteData2TopicPublishInfo(final String topic, final TopicRouteData route) {
 
        TopicPublishInfo info = new TopicPublishInfo();
 
        info.setTopicRouteData(route);
        if (route.getOrderTopicConf() != null && route.getOrderTopicConf().length() > 0) {
          // 省略相關程式碼
        } else {
 
            List<QueueData> qds = route.getQueueDatas();
 
            // 按照brokerName進行排序
            Collections.sort(qds);
 
            // 遍歷所有broker生成佇列維度資訊
            for (QueueData qd : qds) {
                // 具備寫能力的QueueData能夠用於佇列生成
                if (PermName.isWriteable(qd.getPerm())) {
                    // 遍歷獲得指定brokerData進行異常條件過濾
                    BrokerData brokerData = null;
                    for (BrokerData bd : route.getBrokerDatas()) {
                        if (bd.getBrokerName().equals(qd.getBrokerName())) {
                            brokerData = bd;
                            break;
                        }
                    }
                    if (null == brokerData) {
                        continue;
                    }
                    if (!brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) {
                        continue;
                    }
 
                    // 遍歷QueueData的寫佇列的數量大小,生成MessageQueue儲存指定TopicPublishInfo
                    for (int i = 0; i < qd.getWriteQueueNums(); i++) {
                        MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
                        info.getMessageQueueList().add(mq);
                    }
                }
            }
 
            info.setOrderTopic(false);
        }
 
        return info;
    }
}

路由生成過程:

  • 路由生成過程主要是根據QueueData的BrokerName和writeQueueNums來生成MessageQueue 物件。

  • MessageQueue是訊息傳送過程中選擇的最細粒度的可傳送訊息的佇列。

{
    "TBW102": [{
        "brokerName": "broker-a",
        "perm": 7,
        "readQueueNums": 8,
        "topicSynFlag": 0,
        "writeQueueNums": 8
    }, {
        "brokerName": "broker-b",
        "perm": 7,
        "readQueueNums": 8,
        "topicSynFlag": 0,
        "writeQueueNums": 8
    }]
}

路由解析舉例:

  • topic(TBW102)在broker-a和broker-b上存在佇列資訊,其中讀寫佇列個數都為8。

  • 先按照broker-a、broker-b的名字順序針對broker資訊進行排序。

  • 針對broker-a會生成8個topic為TBW102的MessageQueue物件,queueId分別是0-7。

  • 針對broker-b會生成8個topic為TBW102的MessageQueue物件,queueId分別是0-7。

  • topic(名為TBW102)的TopicPublishInfo整體包含16個MessageQueue物件,其中有8個broker-a的MessageQueue,有8個broker-b的MessageQueue。

  • 訊息傳送過程中的路由選擇就是從這16個MessageQueue物件當中獲取一個進行訊息傳送。

3.2 負載均衡過程

public class DefaultMQProducerImpl implements MQProducerInner {
 
    private SendResult sendDefaultImpl(
        Message msg,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final long timeout
    ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        
        // 1、查詢訊息傳送的TopicPublishInfo資訊
        TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
 
        if (topicPublishInfo != null && topicPublishInfo.ok()) {
             
            String[] brokersSent = new String[timesTotal];
            // 根據重試次數進行訊息傳送
            for (; times < timesTotal; times++) {
                // 記錄上次傳送失敗的brokerName
                String lastBrokerName = null == mq ? null : mq.getBrokerName();
                // 2、從TopicPublishInfo獲取傳送訊息的佇列
                MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
                if (mqSelected != null) {
                    mq = mqSelected;
                    brokersSent[times] = mq.getBrokerName();
                    try {
                        // 3、執行傳送並判斷髮送結果,如果傳送失敗根據重試次數選擇訊息佇列進行重新傳送
                        sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                        switch (communicationMode) {
                            case SYNC:
                                if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
                                    if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
                                        continue;
                                    }
                                }
 
                                return sendResult;
                            default:
                                break;
                        }
                    } catch (MQBrokerException e) {
                        // 省略相關程式碼
                    } catch (InterruptedException e) {
                        // 省略相關程式碼
                    }
                } else {
                    break;
                }
            }
 
            if (sendResult != null) {
                return sendResult;
            }
        }
    }
}

訊息傳送過程:

  • 查詢Topic對應的路由資訊物件TopicPublishInfo。

  • 從TopicPublishInfo中通過selectOneMessageQueue獲取傳送訊息的佇列,該佇列代表具體落到具體的Broker的queue佇列當中。

  • 執行傳送並判斷髮送結果,如果傳送失敗根據重試次數選擇訊息佇列進行重新傳送,重新選擇佇列會避開上一次傳送失敗的Broker的佇列。

public class TopicPublishInfo {
 
    public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
        if (lastBrokerName == null) {
            return selectOneMessageQueue();
        } else {
            // 按照輪詢進行選擇傳送的MessageQueue
            for (int i = 0; i < this.messageQueueList.size(); i++) {
                int index = this.sendWhichQueue.getAndIncrement();
                int pos = Math.abs(index) % this.messageQueueList.size();
                if (pos < 0)
                    pos = 0;
                MessageQueue mq = this.messageQueueList.get(pos);
                // 避開上一次上一次傳送失敗的MessageQueue
                if (!mq.getBrokerName().equals(lastBrokerName)) {
                    return mq;
                }
            }
            return selectOneMessageQueue();
        }
    }
}

路由選擇過程:

  • MessageQueue的選擇按照輪詢進行選擇,通過全域性維護索引進行累加取模選擇傳送佇列。

  • MessageQueue的選擇過程中會避開上一次傳送失敗Broker對應的MessageQueue。

Producer訊息傳送示意圖

  • 某Topic的佇列分佈為Broker_A_Queue1、Broker_A_Queue2、Broker_B_Queue1、Broker_B_Queue2、Broker_C_Queue1、Broker_C_Queue2,根據輪詢策略依次進行選擇。

  • 傳送失敗的場景下如Broker_A_Queue1傳送失敗那麼就會跳過Broker_A選擇Broker_B_Queue1進行傳送。

四、consumer訊息消費過程

consumer訊息消費過程

  • consumer訪問namesvr同步topic對應的路由資訊。

  • consumer在本地解析遠端路由資訊並儲存到本地。

  • consumer在本地進行Reblance負載均衡確定本節點負責消費的MessageQueue。

  • consumer訪問Broker消費指定的MessageQueue的訊息。

4.1 路由同步過程

public class MQClientInstance {
 
    // 1、啟動定時任務從namesvr定時同步路由資訊
    private void startScheduledTask() {
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
 
            @Override
            public void run() {
                try {
                    MQClientInstance.this.updateTopicRouteInfoFromNameServer();
                } catch (Exception e) {
                    log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
                }
            }
        }, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);
    }
 
    public void updateTopicRouteInfoFromNameServer() {
        Set<String> topicList = new HashSet<String>();
 
        // 遍歷所有的consumer訂閱的Topic並從namesvr獲取路由資訊
        {
            Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
            while (it.hasNext()) {
                Entry<String, MQConsumerInner> entry = it.next();
                MQConsumerInner impl = entry.getValue();
                if (impl != null) {
                    Set<SubscriptionData> subList = impl.subscriptions();
                    if (subList != null) {
                        for (SubscriptionData subData : subList) {
                            topicList.add(subData.getTopic());
                        }
                    }
                }
            }
        }
 
        for (String topic : topicList) {
            this.updateTopicRouteInfoFromNameServer(topic);
        }
    }
 
    public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
        DefaultMQProducer defaultMQProducer) {
 
        try {
            if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
                try {
                    TopicRouteData topicRouteData;
                    if (isDefault && defaultMQProducer != null) {
                        // 省略程式碼
                    } else {
                        topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
                    }
 
                    if (topicRouteData != null) {
                        TopicRouteData old = this.topicRouteTable.get(topic);
                        boolean changed = topicRouteDataIsChange(old, topicRouteData);
                        if (!changed) {
                            changed = this.isNeedUpdateTopicRouteInfo(topic);
                        }
 
                        if (changed) {
                            TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
 
                            for (BrokerData bd : topicRouteData.getBrokerDatas()) {
                                this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
                            }
 
                            // 構建consumer側的路由資訊
                            {
                                Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
                                Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
                                while (it.hasNext()) {
                                    Entry<String, MQConsumerInner> entry = it.next();
                                    MQConsumerInner impl = entry.getValue();
                                    if (impl != null) {
                                        impl.updateTopicSubscribeInfo(topic, subscribeInfo);
                                    }
                                }
                            }
     
                            this.topicRouteTable.put(topic, cloneTopicRouteData);
                            return true;
                        }
                    }
                } finally {
                    this.lockNamesrv.unlock();
                }
            }
        } catch (InterruptedException e) {
        }
 
        return false;
    }
}

路由同步過程

  • 路由同步過程是訊息消費者消費訊息的前置條件,沒有路由的同步就無法感知具體待消費的訊息的Broker節點。

  • consumer節點通過定時任務定期從namesvr同步該消費節點訂閱的topic的路由資訊。

  • consumer通過updateTopicSubscribeInfo將同步的路由資訊構建成本地的路由資訊並用以後續的負責均衡。

4.2 負載均衡過程

public class RebalanceService extends ServiceThread {
 
    private static long waitInterval =
        Long.parseLong(System.getProperty(
            "rocketmq.client.rebalance.waitInterval", "20000"));
 
    private final MQClientInstance mqClientFactory;
 
    public RebalanceService(MQClientInstance mqClientFactory) {
        this.mqClientFactory = mqClientFactory;
    }
 
    @Override
    public void run() {
 
        while (!this.isStopped()) {
            this.waitForRunning(waitInterval);
            this.mqClientFactory.doRebalance();
        }
 
    }
}

負載均衡過程

  • consumer通過RebalanceService來定期進行重新負載均衡。

  • RebalanceService的核心在於完成MessageQueue和consumer的分配關係。

public abstract class RebalanceImpl {
 
    private void rebalanceByTopic(final String topic, final boolean isOrder) {
        switch (messageModel) {
            case BROADCASTING: {
                // 省略相關程式碼
                break;
            }
            case CLUSTERING: { // 叢集模式下的負載均衡
                // 1、獲取topic下所有的MessageQueue
                Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
 
                // 2、獲取topic下該consumerGroup下所有的consumer物件
                List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
                 
                // 3、開始重新分配進行rebalance
                if (mqSet != null && cidAll != null) {
                    List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
                    mqAll.addAll(mqSet);
 
                    Collections.sort(mqAll);
                    Collections.sort(cidAll);
 
                    AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
 
                    List<MessageQueue> allocateResult = null;
                    try {
                        // 4、通過分配策略重新進行分配
                        allocateResult = strategy.allocate(
                            this.consumerGroup,
                            this.mQClientFactory.getClientId(),
                            mqAll,
                            cidAll);
                    } catch (Throwable e) {
 
                        return;
                    }
 
                    Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
                    if (allocateResult != null) {
                        allocateResultSet.addAll(allocateResult);
                    }
                    // 5、根據分配結果執行真正的rebalance動作
                    boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
                    if (changed) {
                        this.messageQueueChanged(topic, mqSet, allocateResultSet);
                    }
                }
                break;
            }
            default:
                break;
        }
    }

重新分配流程

  • 獲取topic下所有的MessageQueue。

  • 獲取topic下該consumerGroup下所有的consumer的cid(如192.168.0.8@15958)。

  • 針對mqAll和cidAll進行排序,mqAll排序順序按照先BrokerName後BrokerId,cidAll排序按照字串排序。

  • 通過分配策略

  • AllocateMessageQueueStrategy重新分配。

  • 根據分配結果執行真正的rebalance動作。

public class AllocateMessageQueueAveragely implements AllocateMessageQueueStrategy {
    private final InternalLogger log = ClientLogger.getLog();
 
    @Override
    public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
        List<String> cidAll) {
         
        List<MessageQueue> result = new ArrayList<MessageQueue>();
         
        // 核心邏輯計算開始
 
        // 計算當前cid的下標
        int index = cidAll.indexOf(currentCID);
         
        // 計算多餘的模值
        int mod = mqAll.size() % cidAll.size();
 
        // 計算平均大小
        int averageSize =
            mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
                + 1 : mqAll.size() / cidAll.size());
        // 計算起始下標
        int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
        // 計算範圍大小
        int range = Math.min(averageSize, mqAll.size() - startIndex);
        // 組裝結果
        for (int i = 0; i < range; i++) {
            result.add(mqAll.get((startIndex + i) % mqAll.size()));
        }
        return result;
    }
    // 核心邏輯計算結束
 
    @Override
    public String getName() {
        return "AVG";
    }
}
 
------------------------------------------------------------------------------------
 
rocketMq的叢集存在3個broker,分別是broker_a、broker_b、broker_c。
 
rocketMq上存在名為topic_demo的topic,writeQueue寫佇列數量為3,分佈在3個broker。
排序後的mqAll的大小為9,依次為
[broker_a_0  broker_a_1  broker_a_2  broker_b_0  broker_b_1  broker_b_2  broker_c_0  broker_c_1  broker_c_2]
 
rocketMq存在包含4個consumer的consumer_group,排序後cidAll依次為
[192.168.0.6@15956  192.168.0.7@15957  192.168.0.8@15958  192.168.0.9@15959]
 
192.168.0.6@15956 的分配MessageQueue結算過程
index:0
mod:9%4=1
averageSize:9 / 4 + 1 = 3
startIndex:0
range:3
messageQueue:[broker_a_0、broker_a_1、broker_a_2]
 
 
192.168.0.6@15957 的分配MessageQueue結算過程
index:1
mod:9%4=1
averageSize:9 / 4 = 2
startIndex:3
range:2
messageQueue:[broker_b_0、broker_b_1]
 
 
192.168.0.6@15958 的分配MessageQueue結算過程
index:2
mod:9%4=1
averageSize:9 / 4 = 2
startIndex:5
range:2
messageQueue:[broker_b_2、broker_c_0]
 
 
192.168.0.6@15959 的分配MessageQueue結算過程
index:3
mod:9%4=1
averageSize:9 / 4 = 2
startIndex:7
range:2
messageQueue:[broker_c_1、broker_c_2]

分配策略分析:

  • 整體分配策略可以參考上圖的具體例子,可以更好的理解分配的邏輯。

consumer的分配

  • 同一個consumerGroup下的consumer物件會分配到同一個Topic下不同的MessageQueue。

  • 每個MessageQueue最終會分配到具體的consumer當中。

五、RocketMQ指定機器消費設計思路

日常測試環境當中會存在多臺consumer進行消費,但實際開發當中某臺consumer新上了功能後希望訊息只由該機器進行消費進行邏輯覆蓋,這個時候consumerGroup的叢集模式就會給我們造成困擾,因為消費負載均衡的原因不確定訊息具體由那臺consumer進行消費。當然我們可以通過介入consumer的負載均衡機制來實現指定機器消費。

public class AllocateMessageQueueAveragely implements AllocateMessageQueueStrategy {
    private final InternalLogger log = ClientLogger.getLog();
 
    @Override
    public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
        List<String> cidAll) {
        
 
        List<MessageQueue> result = new ArrayList<MessageQueue>();
        // 通過改寫這部分邏輯,增加判斷是否是指定IP的機器,如果不是直接返回空列表表示該機器不負責消費
        if (!cidAll.contains(currentCID)) {
            return result;
        }
 
        int index = cidAll.indexOf(currentCID);
        int mod = mqAll.size() % cidAll.size();
        int averageSize =
            mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
                + 1 : mqAll.size() / cidAll.size());
        int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
        int range = Math.min(averageSize, mqAll.size() - startIndex);
        for (int i = 0; i < range; i++) {
            result.add(mqAll.get((startIndex + i) % mqAll.size()));
        }
        return result;
    }
}

consumer負載均衡策略改寫

  • 通過改寫負載均衡策略AllocateMessageQueueAveragely的allocate機制保證只有指定IP的機器能夠進行消費。

  • 通過IP進行判斷是基於RocketMQ的cid格式是192.168.0.6@15956,其中前面的IP地址就是對於的消費機器的ip地址,整個方案可行且可以實際落地。

六、小結

本文主要介紹了RocketMQ在生產和消費過程中的負載均衡機制,結合原始碼和實際案例力求給讀者一個易於理解的技術普及,希望能對讀者有參考和借鑑價值。囿於文章篇幅,有些方面未涉及,也有很多技術細節未詳細闡述,如有疑問歡迎繼續交流。

作者:vivo網際網路伺服器團隊-Wang Zhi

相關文章