消息中间件MetaQ

消息传递服务

点对点模式

生产者发送消息给特定的消费者(其实是该消费者对应的一个消息队列)。一对一发送。

发布-订阅模式

多生产者、多消费者模型,消息队列分不同的类型,用 Topic 标识。消费者只有订阅该消息后才可以收到。

MetaQ使用指南

MetaQ是一款分布式、队列模型的消息中间件。基于发布订阅模式,有Push和Pull两种消费方式,支持严格的消息顺序,亿级别的堆积能力,支持消息回溯和多个维度的消息查询。

  • 推消息模型(push)

    • 消息中间件主动将消息推送给消费者。将消息提前推送给消费者,消费者必须设置一个缓冲区缓存这些消息。好处很明显,消费者总是有一堆在内存中待处理的消息,所以效率高。缺点:是缓冲区可能会溢出。
    • 实时性较好,收到数据后可立即发给客户端(消息消费者)。
  • 拉消息模型(pull)

    • 消费者主动从消息中间件拉取消息。在消费者需要时才去消息中间件拉取消息,这段网络开销会明显增加消息延迟,降低系统吞吐量。
    • 实时性取决于pull的间隔时间

POM依赖

推荐使用3.0.0-SNAPSHOT版本

<dependency>
    <groupId>com.taobao.metaq</groupId>
    <artifactId>metaq-client</artifactId>
    <version>3.0.0-SNAPSHOT</version>
</dependency>

发送消息

  • 日常、预发等非生产环境,发送消息不需要申请,应用可以自由创建Topic。这样做的目的是为了方便应用方更快的了解、测试MetaQ,但是也对运维提出了挑战,我们相信各个应用方都能创建有效Topic。

  • 生产环境必须要申请才能发送,请提前3个工作日申请,对于消息量较大的应用,请尽早与MetaQ运维人员联系,以方便评估线上机器资源。

  • 对于非常重要的消息,例如订单消息,业务方需要有重发补偿的机制,例如MetaQ服务短暂不可用,此时发往MetaQ的消息将失败,等到MetaQ服务恢复后,业务方可以将之前发送失败的消息重新补偿发送

  • 对于Message Size特别大的消息如何处理?例如1M,几百K的消息 不推荐应用发送超过16K的消息,如果消息确实比较大,发送消息客户端有个配置,默认超过4K的消息开始压缩,消息到达订阅方之前会自动解压,压缩过程对用户透明,但是如果压缩过以后消息仍然较大,我们推荐应用对消息进行拆分,这样做的原因如下

    • MetaQ通信层没有对大的请求做优化,采用的是典型的RPC方式,不适合大的请求传递,可能会导致网络层的Buffer异常。
    • MetaQ的服务器存储是一个典型的LRU CACHE系统,过大的消息会占用较多Cache,对于其他应用Cache命中率产生影响
    • MetaQ的磁盘资源通常比较紧张
    • MetaQ暂不解决大消息存储问题
  • 发送消息时,如果将来需要查询消息,或者定位消息是否被接收,需要设置Message Key属性,例如设置为订单Id,商品Id等

  • 发送消息时,如果订阅方有过滤需求,请在消息Tag属性上设置相关值,Tag的名称不需要申请,可自由设置,一条消息只允许设置一个Tag。

  • 发送事务消息,出于运维角度考虑,淘宝用户请使用Notify。

订阅消息

  • 日常、预发等非生产环境,订阅消息不需要申请。订阅生产环境的消息,请提前3个工作日申请,需要人工审批。

  • MetaQ支持服务器消息过滤,如果订阅某个Topic,但只关心其中一部分消息,可以使用表达式方式过滤。这样可以避免无用的消息传输到客户端,而且降低了应用与MetaQ服务器的负载。过滤表达式中的Message Tag是由发送方自由指定,MetaQ不做任何限制,当然不能传入非法字符,例如空白字符、|| 等

  • 非顺序消息消费,耗时时间不做限制,但是应用应该尽可能保证耗时短,这样才能达到高性能,另外消费消息Hang住,会导致消息所在队列的消费动作暂停,直到Hang住的消息消费完。对其他队列不受影响

  • 顺序消息消费,耗时时间有限制,要保证每条消息在30s内消费完,超过30s会有潜在的乱序问题。(原因是分布式锁超时问题,但概率极低)

消息重复性

  • MetaQ不能保证消息不重复,"Exactly Only Once"这个特性不支持,原因如下:

    • 发送消息阶段,会存在分布式环境下典型的超时问题,即发送消息阶段不能保证消息不重复。
    • 订阅消息阶段,由于涉及集群订阅,多个订阅者需要Rebalance方式订阅,在Rebalance短暂不一致情况下,会产生消息重复
    • 订阅者意外宕机,消费进度未及时存储,也会产生消息重复
  • 消息重复性问题如何解决?

    • 应用方收到消息后,可通过Tair、DB等去重
    • 应用方可通过主动拉的方式,可保证拉消息绝对不重复,但是分布式协调分配队列问题需要应用来控制
    • 消息中间件团队也在思考如何有效去重,又对整个消息系统性能影响最低。

广播消息

MetaQ支持广播消息,但是广播消息的代价较高,投递比可能在1:100甚至1:1000,对于生产环境订阅广播消息,人工审核环节可能会拒绝,取决于订阅的消息量及消费者集群规模。

MetaQ的广播消息不支持失败重试,原因如下:

对于集群消费的消息支持失败重试,因为失败的维度是一个订阅组集群,而广播消息失败重试维护的则是订阅组集群中的每个订阅者,代价较高。
MetaQ的广播消息消费进度维护在消费者本地磁盘,每隔5s刷盘一次,如果本地磁盘损坏,消费进度如何恢复?

联系MetaQ运维人员,通过运维工具按照时间维度,例如回退一小时,新创建一份消费进度。(此功能开发中)

消息重试

非顺序消息消费失败重试,消费失败的消息发回服务器,应用可以指定这条失败消息下次到达Consumer的时间。消费失败重试次数有限制,通常线上为每个订阅组每条失败消息重试5次(每次消息都会定时重试,定时时间随着重试次数递增,此过程应用可干预)。超过重试次数,消息进入死信队列,并向用户报警。

消息重试对于服务器代价较高,如果某个应用消息量非常大,且失败率非常高,需要大量重试,则不建议使用MetaQ

顺序消息消费失败重试,某个队列正在消费的消息消费失败,会将当前队列挂起(挂起时间应用可通过API设置),其他队列仍然正常消费。

死信队列

消息一旦进入死信队列,则不再向应用投递,MetaQ监控系统会向应用报警 (报警功能开发中)

由于消息一旦进入死信队列,则不能再被订阅,建议应用在最后一次重试消费时,将失败消息保存到DB

消息堆积

MetaQ每台服务器提供大约亿级的消息堆积能力(多个业务方共用),超过堆积阀值,订阅消息吞吐量会下降。

消息实时性

MetaQ采用了长轮询方式从Broker拉消息,实时性同Push方式一致,消息的延迟时间大约几十毫秒左右。

MetaQ底层原理

MetaQ集群架构

在这里插入图片描述

  • NameServer集群:MetaQ基于NameServer,也是基于阿里内部中间件Config Server。可以把它理解为类似zookeeper的角色
  • Broker:消息中转角色,负责存储消息,转发消息
  • Consumer:消息消费者,负责消费消息,一般是后台系统负责异步消费。MetaQ提供两种消费模型
    • Push Consumer :向Consumer对象注册一个Listener接口,收到消息后回调Listener接口方法,采用长轮询实现push
    • Pull Consumer:主动由Consumer主动拉取信息,同kafka
  • Producer:消息生产者,负责产生消息,一般由业务系统负责产生消息

消息结构模型

  • Message:单位消息
  • Topic:消息主题,软分区,对应相同的topic时,生产者对应消费者的分区标识
  • Tag:消息在topic基础上的二级分类
  • Message Queue:硬分区,物理上区分topic,一个topic对应多个message queue。在 MetaQ 中,所有消息队列都是持久化,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用 Offset 来访问,offset 为 java long 类型,64 位,理论上在 100 年内不会溢出,所以认为是长度无限,另外队列中只保存最近几天的数据,之前的数据会按照过期时间来 删除。
  • Group:
    • Consumer Group,一类 Consumer 的集合名称,这类 Consumer 通常消费一类消息,且消费逻辑一致;
    • Producer Group,一类 Producer 的集合名称,这类 Producer 通常发送一类消息,且发送逻辑一致。
  • Offset:绝对偏移值,message queue中有两类offset(commitOffset和offset),前者存储在OffsetStore中表示消费到的位置,后者是在PullRequest中为拉取消息位置。

Broker

  • Broker以组为单位向Consumer提供消息服务,group中分为master和slave两种角色。然后通过NameServer暴露给Consumer具体通信地址,采用message queue消息队列结构来提供消费接口。针对某一topic情况下,message queue会根据queue id分布在不同的broker上,Consumer的消息消费压力则会分摊在不同的Broker上的message queue,从而达到负载均衡的作用。
  • 虽然每个topic下面有很多message queue,但是message queue本身并不存储消息。真正的消息存储会写在CommitLog的文件,message queue只是存储CommitLog中对应的位置信息,方便通过message queue找到对应存储在CommitLog的消息。不同的topic,message queue都是写到相同的CommitLog 文件,也就是说CommitLog完全的顺序写,而顺序读写是metaq高吞吐量的基础。

Broker存储结构

在这里插入图片描述

  • 重试队列:%RETRY%+consumergroup,push consumer默认订阅用于消费失败后的重试消费
  • 死信队列:多次(默认16次)消费失败后进入DLQ队列,需要人工处理
  • 定时队列:用于定时和延时消息
  • ConsumeQueue: 即message queue,根据topic和queueId区分的消息队列,对MappedFileQueue进行封装
  • CommitLog: Broker中顺序存储的消息结构,管理消息commit和flush,对MappedFileQueue进行封装
  • MappedFileQueue: 对~/store/commitlog/中MappedFile封装成文件队列,进行文件大小格式检查,对mappedFile进行管理。
  • MappedFile: 实际broker数据文件映射成的类,即~/store/commitlog/中00000000000000000000、00000000001073741824等文件,每个文件默认大小上限为1G。

消息写入

CommitLog负责将Producer的消息写入文件中

在这里插入图片描述
核心代码如下

putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
        try {
            long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
            this.beginTimeInLock = beginLockTimestamp;

            // Here settings are stored timestamp, in order to ensure an orderly
            // global
            msg.setStoreTimestamp(beginLockTimestamp);

            if (null == mappedFile || mappedFile.isFull()) {
                mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
            }
            if (null == mappedFile) {
                log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
                beginTimeInLock = 0;
                return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
            }

            result = mappedFile.appendMessage(msg, this.appendMessageCallback);
            switch (result.getStatus()) {
                case PUT_OK:
                    break;
                case END_OF_FILE:
                    unlockMappedFile = mappedFile;
                    // Create a new file, re-write the message
                    mappedFile = this.mappedFileQueue.getLastMappedFile(0);
                    if (null == mappedFile) {
                        // XXX: warn and notify me
                        log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
                        beginTimeInLock = 0;
                        return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
                    }
                    result = mappedFile.appendMessage(msg, this.appendMessageCallback);
                    break;
                case MESSAGE_SIZE_EXCEEDED:
                case PROPERTIES_SIZE_EXCEEDED:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result);
                case UNKNOWN_ERROR:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
                default:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
            }

            eclipseTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
            beginTimeInLock = 0;
        } finally {
            putMessageLock.unlock();
        }

putMessageLock这里提供了两种上锁方式,一种是默认的自旋锁,使用compareAndSet实现(用于low-race condition);一种是可重入锁,使用ReentrantLock实现。

快速使用

发送普通消息

package com.taobao.metaq.example.simple;

import com.alibaba.rocketmq.client.exception.MQClientException;

import com.alibaba.rocketmq.client.producer.SendResult;

import com.alibaba.rocketmq.common.message.Message;

import com.taobao.metaq.client.MetaProducer;

public class Producer {

    public static void main(String[] args) throws MQClientException, InterruptedException {

        /**
         * 一个应用创建一个Producer,由应用来维护此对象,可以设置为全局对象或者单例<br>
         * 注意:ProducerGroupName需要由应用来保证唯一<br>
         * ProducerGroup这个概念发送普通的消息时,作用不大,但是发送分布式事务消息时,比较关键,
         * 因为服务器会回查这个Group下的任意一个Producer
         */

        MetaProducer producer = new MetaProducer("manhongTestPubGroup");

        /**
         * Producer对象在使用之前必须要调用start初始化,初始化一次即可<br>
         * 注意:切记不可以在每次发送消息时,都调用start方法
         */
        producer.start();

        /**
         * 下面这段代码表明一个Producer对象可以发送多个topic,多个tag的消息。
         * 注意:send方法是同步调用,只要不抛异常就标识成功。但是发送成功也可会有多种状态,<br>
         * 例如消息写入Master成功,但是Slave不成功,这种情况消息属于成功,但是对于个别应用如果对消息可靠性要求极高,<br>
         * 需要对这种情况做处理。另外,消息可能会存在发送失败的情况,失败重试由应用来处理。
         */
        try {

            for (int i = 0; i < 20; i++) {
                {
                    Message msg = new Message("Jodie_topic_1023",// topic
                            "TagA",// tag
                            "OrderID001",// key,消息的Key字段是为了唯一标识消息的,方便运维排查问题。如果不设置Key,则无法定位消息丢失原因。
                            ("Hello MetaQ").getBytes());// body
                    SendResult sendResult = producer.send(msg);
                    System.out.println(sendResult);
                }

                {
                    Message msg = new Message("TopicTest2",// topic
                            "TagB",// tag
                            "OrderID0034",// key
                            ("Hello MetaQ").getBytes());// body
                    SendResult sendResult = producer.send(msg);
                    System.out.println(sendResult);
                }

                {
                    Message msg = new Message("TopicTest3",// topic
                            "TagC",// tag
                            "OrderID061",// key
                            ("Hello MetaQ").getBytes());// body
                    SendResult sendResult = producer.send(msg);
                    System.out.println(sendResult);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        /**
         * 应用退出时,要调用shutdown来清理资源,关闭网络连接,从MetaQ服务器上注销自己
         * 注意:我们建议应用在JBOSS、Tomcat等容器的退出钩子里调用shutdown方法
         */
        producer.shutdown();
    }
}

订阅普通消息

package com.taobao.metaq.example.simple;

import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageExt;
import com.taobao.metaq.client.MetaPushConsumer;

import java.util.List;


public class PushConsumer {

    /**
     * 当前例子是PushConsumer用法,使用方式给用户感觉是消息从MetaQ服务器推到了应用客户端。<br>
     * 但是实际PushConsumer内部是使用长轮询Pull方式从MetaQ服务器拉消息,然后再回调用户Listener方法<br>
     */
    public static void main(String[] args) throws InterruptedException, MQClientException {
        /**
         * 一个应用创建一个Consumer,由应用来维护此对象,可以设置为全局对象或者单例<br>
         * 注意:ConsumerGroupName需要由应用来保证唯一<br>
         * ConsumerGroupName在生产环境需要申请,非生产环境不需要
         */
        MetaPushConsumer consumer = new MetaPushConsumer("RebalanceTest_Consumer_Group");

        /**
         * 订阅指定topic下tags分别等于TagA或TagC或TagD
         */
        consumer.subscribe("TopicTest1", "TagA || TagC || TagD");
        consumer.setConsumeMessageBatchMaxSize(3);
        /**
         * 订阅指定topic下所有消息<br>
         * 注意:一个consumer对象可以订阅多个topic
         */
        consumer.subscribe("TopicTest2", "*");

        consumer.registerMessageListener(new MessageListenerConcurrently() {

            /**
             * 1、默认msgs里只有一条消息,可以通过设置consumeMessageBatchMaxSize参数来批量接收消息<br>
             * 2、如果设置为批量消费方式,要么都成功,要么都失败。<br>
             * 3、此方法由MetaQ客户端多个线程回调,需要应用来处理并发安全问题<br>
             * 4、抛异常与返回ConsumeConcurrentlyStatus.RECONSUME_LATER等价<br>
             * 5、每条消息失败后,会尝试重试,重试16次都失败,则丢弃<br>
             */
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs);
                // for (MessageExt msg : msgs) {
                // if (msg.getTags().equals("TagA")) {
                // return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                // }
                // }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        /**
         * Consumer对象在使用之前必须要调用start初始化,初始化一次即可<br>
         */
        consumer.start();

        System.out.println("Consumer Started.");
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冷冰殇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值