分佈式消息中間件RocketMQ原理解析與集羣模式實戰

最近的項目開發中涉及到支付業務的模塊需要用到MQ進行業務解耦以及把用戶請求量削峯填谷,提高系統的可用性和可靠性,我們選擇了RocketMQ來部署消息中間件集羣,我也在此回顧和歸納下RocketMQ的相關知識。

什麼是RocketMQ

阿里開源的分佈式消息中間件,單機就能支持千萬級的消息堆積,集羣模式能滿足海量消息堆積的場景

RocketMQ的特點

支持發佈/訂閱(Pub/Sub)和點對點(P2P)消息模型

在一個隊列中可靠的先進先出(FIFO)和嚴格的順序傳遞

支持拉(pull)和推(push)兩種消息模式

單一隊列百萬消息的堆積能力

支持多種消息協議,如 JMS、MQTT 等

分佈式高可用的部署架構,滿足至少一次消息傳遞語義

提供 docker 鏡像用於隔離測試和雲集羣部署

提供配置、指標和監控等功能豐富的 Dashboard

核心機制與原理

RocketMQ底層基於隊列模型來實現消息收發功能。RocketMQ集羣中包含4個模塊:Namesrv, Broker, Producer, Consumer。

Namesrv: 存儲當前集羣所有Brokers信息、Topic跟Broker的對應關係。

Broker: 集羣最核心模塊,主要負責Topic消息存儲、消費者的消費位點管理(消費進度)。

Producer: 消息生產者,每個生產者都有一個ID(編號),多個生產者實例可以共用同一個ID。同一個ID下所有實例組成一個生產者集羣。

Consumer: 消息消費者,每個訂閱者也有一個ID(編號),多個消費者實例可以共用同一個ID。同一個ID下所有實例組成一個消費者集羣。

每30秒心跳機制

單個Broker跟所有Namesrv保持心跳請求,心跳間隔爲30秒,心跳請求中包括當前Broker所有的Topic信息。Namesrv會反查Broer的心跳信息, 如果某個Broker在2分鐘之內都沒有心跳,則認爲該Broker下線,調整Topic跟Broker的對應關係。但此時Namesrv不會主動通知Producer、Consumer有Broker宕機。

Consumer跟Broker是長連接,會每隔30秒發心跳信息到Broker。Broker端每10秒檢查一次當前存活的Consumer,若發現某個Consumer 2分鐘內沒有心跳, 就斷開與該Consumer的連接,並且向該消費組的其他實例發送通知,觸發該消費者集羣的負載均衡(rebalance)。

生產者每30秒從Namesrv獲取Topic跟Broker的映射關係,更新到本地內存中。再跟Topic涉及的所有Broker建立長連接,每隔30秒發一次心跳。 在Broker端也會每10秒掃描一次當前註冊的Producer,如果發現某個Producer超過2分鐘都沒有發心跳,則斷開連接。

集羣模式部署

單 master 模式 也就是隻有一個 master 節點,稱不上是集羣,一旦這個 master 節點宕機,那麼整個服務就不可用,適合個人學習使用。

多 master 模式 多個 master 節點組成集羣,單個 master 節點宕機或者重啓對應用沒有影響。

優點:所有模式中性能最高

缺點:單個master節點宕機期間,未被消費的消息在節點恢復之前不可用,消息的實時性就受到影響。注意:使用同步刷盤可以保證消息不丟失,同時 Topic 相對應的 queue 應該分佈在集羣中各個節點,而不是隻在某各節點上,否則,該節點宕機會對訂閱該 topic 的應用造成影響。

多 master 多 slave 異步複製模式 在多 master 模式的基礎上,每個 master 節點都有至少一個對應的 slave。master 節點可讀可寫,但是 slave 只能讀不能寫,類似於 mysql 的主備模式。 優點: 在 master 宕機時,消費者可以從 slave 讀取消息,消息的實時性不會受影響,性能幾乎和多 master 一樣。 缺點:使用異步複製的同步方式有可能會有消息丟失的問題。

多 master 多 slave 同步雙寫模式 同多 master 多 slave 異步複製模式類似,區別在於 master 和 slave 之間的數據同步方式。 優點:同步雙寫的同步模式能保證數據不丟失。 缺點:發送單個消息 RT 會略長,性能相比異步複製低10%左右。 刷盤策略:同步刷盤和異步刷盤(指的是節點自身數據是同步還是異步存儲) 同步方式:同步雙寫和異步複製(指的一組 master 和 slave 之間數據的同步) 注意:要保證數據可靠,需採用同步刷盤和同步雙寫的方式,但性能會較其他方式低。

在此舉個可靠異步消息發佈和消息訂閱的例子  , 更多的實戰經歷後續文章繼續發佈

可靠異步消息

發送可靠異步消息
       
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // MQ服務器地址端口
        producer.setNamesrvAddr("localhost:9876");
        //初始化對象實例
        producer.start();
        producer.setRetryTimesWhenSendAsyncFailed(0);
        for (int i = 0; i < 100; i++) {
                final int index = i;
                //創建消息實體 包括 topic, tag and message body.
                Message msg = new Message("TopicTest",
                    "TagA",
                    "OrderID188",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));

                producer.send(msg, new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        System.out.printf("%-10d OK %s %n", index,
                            sendResult.getMsgId());
                    }
                    @Override
                    public void onException(Throwable e) {
                        System.out.printf("%-10d Exception %s %n", index, e);
                        e.printStackTrace();
                    }
                });
        }
        //關閉不再使用的連接,釋放資源
        producer.shutdown();

消費消息

        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name");

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

        consumer.subscribe("TopicTest", "TagA || TagC || TagD");

        consumer.registerMessageListener(new MessageListenerOrderly() {

            AtomicLong consumeTimes = new AtomicLong(0);
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
                                                       ConsumeOrderlyContext context) {
                context.setAutoCommit(false);
                System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
                this.consumeTimes.incrementAndGet();
                if ((this.consumeTimes.get() % 2) == 0) {
                    return ConsumeOrderlyStatus.SUCCESS;
                } else if ((this.consumeTimes.get() % 3) == 0) {
                    return ConsumeOrderlyStatus.ROLLBACK;
                } else if ((this.consumeTimes.get() % 4) == 0) {
                    return ConsumeOrderlyStatus.COMMIT;
                } else if ((this.consumeTimes.get() % 5) == 0) {
                    context.setSuspendCurrentQueueTimeMillis(3000);
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
                return ConsumeOrderlyStatus.SUCCESS;

            }
        });

        consumer.start();

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章