RocketMq简介

1.官方定义


  • RocketMq是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式特点。
  • Producer、Consumer、队列都可以分布式。
  • Producer 向一些队列轮流发送消息,队列集合称为 Topic,Consumer 如果做广播消费,则一个 consumer实例消费这个 Topic 对应的所有队列,如果做集群消费,则多个 Consumer 实例平均消费这个 topic 对应的队列集合。
  • 能够保证严格的消息顺序
  • 提供丰富的消息拉取模式
  • 高效的订阅者水平扩展能力
  • 实时的消息订阅机制
  • 亿级消息堆积能力
  • 较少的依赖

2.各种概念

  • Producer消息生产者,负责产生消息,一般由业务系统负责产生消息。
  • Consumer消息消费者,负责消费消息,一般是后台系统负责异步消费。
  • Push Consumer,Consumer的一种,应用通常向Consumer对象注册一个Listener接口,一旦收到消息,Consumer对象立刻回调Listener接口方法。
  • Pull Consumer,Consumer的一种,应用通常主动调用Consumer的拉消息方法从Broker拉消息,主动权由应用控制。
  • Producer Group,一类Producer的集合名称,这类Producer通常发送一类消息,且发送逻辑一致。
  • Consumer Group,一类Consumer的集合名称,这类Consumer通常消费一类消息,且消费逻辑一致。
  • Broker,消息中转角色,负责存储消息,转发消息,一般也称为Server。
  • 广播消费

一条消息被多个Consumer 消费,即使这些Consumer 属于同一个Consumer Group,消息也会被Consumer Group 中的每个Consumer 都消费一次,广播消费中的Consumer Group 概念可以认为在消息划分方面无意义。

  • 集群消费

一个Consumer Group 中的Consumer 实例平均分摊消费消息。例如某个Topic 有 9 条消息,其中一个ConsumerGroup 有 3 个实例(可能是 3 个进程,或者 3 台机器),那么每个实例只消费其中的 3 条消息。(消费者默认为集群消费,可以通过consumer.setMessageModel()来设置消费者的消费模式)

  • 顺序消息

实际上,RocketMQ是支持顺序消费的。

但这个顺序,不是全局顺序,只是分区顺序。要全局顺序只能一个分区。

之所以出现你这个场景看起来不是顺序的,是因为发送消息的时候,消息发送默认是会采用轮询的方式发送到不通的queue(分区)。如图:


而消费端消费的时候,是会分配到多个queue的,多个queue是同时拉取提交消费。

如图:


但是同一条queue里面,RocketMQ的确是能保证FIFO的。那么要做到顺序消息,应该怎么实现呢——把消息确保投递到同一条queue。

rocketmq消息生产端示例代码如下:

/** 
 * Producer,发送顺序消息 
 */  
public class Producer {  
      public SendResult sendOrder(Message message, Long orderId) throws BusinessException {
        SendResult result = null;
        try {
            result = producer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    Long id = (Long) arg;
                    long index = id % mqs.size();
                    return mqs.get((int) index);
                }
            }, orderId);
        } catch (Exception e) {
            e.printStackTrace();
            throw new BusinessException("发送有序消息发生异常");
        }
        return result;
    }
}  

按照这个示例,把订单号取了做了一个取模运算再丢到selector中,selector保证同一个模的都会投递到同一条queue。

即: 相同订单号的--->有相同的模--->有相同的queue。

rocketmq消息消费端监听器示例代码如下:

public class MQOrderListener implements MessageListenerOrderly{
    private MQConsumer consumer;

    public void setConsumer(MQConsumer consumer) {
        this.consumer = consumer;
    }

    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {//有序监听
        context.setAutoCommit(true); 
        for (MessageExt messageExt: msgs) {    
            boolean result = consumer.handleMessage(messageExt);
            if (!result) {
                return ConsumeOrderlyStatus.SUCCESS;
            }
        }   
        return ConsumeOrderlyStatus.SUCCESS;    
    }
} 

最后就会类似这样:


        MessageListenerOrderly能够保证顺序消费,从图中我们也看到了期望的结果。图中的输出是只启动了一个消费者时的输出,看起来订单号还是混在一起,但是每组订单号之间是有序的。因为消息发送时被分配到了三个队列(参见前面生产者输出日志),那么这三个队列的消息被这唯一消费者消费。

        这样同一批你需要做到顺序消费的肯定会投递到同一个queue,同一个queue肯定会投递到同一个消费实例,同一个消费实例肯定是顺序拉取并顺序提交线程池的,只要保证消费端顺序消费,则大功告成!

        按照这个示例,把订单号取了做了一个取模运算再丢到selector中,selector保证同一个模的都会投递到同一条queue。即: 相同订单号的--->有相同的模--->有相同的queue。最后就会类似这样:

总结:

rocketmq的顺序消息需要满足2点:

    1.Producer端保证发送消息有序,且发送到同一个队列。

    2.consumer端保证消费同一个队列。

但的确会有一些异常场景会导致乱序。如master宕机,导致写入队列的数量上出现变化。

如果还是沿用取模的seletor,就会一批订单号的消息前面散列到q0,后面的可能散到q1,这样就不能保证顺序了。除非选择牺牲failover特性,如master挂了无法发通接下来那批消息。

从消费端,如果想保证这批消息是M1消费完成再消费M2的话,可以使用MessageListenerOrderly接口,但是这样的话会有以下问题:

        1. 遇到消息失败的消息,无法跳过,当前队列消费暂停 

        2. 目前版本的RocketMQ的MessageListenerOrderly是不能从slave消费消息的

3. 组件详解


3.1  Namesrv

 ① Name Server 是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。

 ② 启动Namesrv,Namesrv起来后监听端口,等待Broker、Produer、Consumer连上来,相当于一个路由控制中心。

 ③ Namesrv用于存储Topic、Broker关系信息,功能简单,稳定性高。多个Namesrv之间相互没有通信,单台Namesrv宕机不影响其他Namesrv与集群;即使整个Namesrv集群宕机,已经正常工作的Producer,Consumer,Broker仍然能正常工作,但新起的Producer, Consumer,Broker就无法工作。

 ④ Namesrv压力不会太大,平时主要开销是在维持心跳和提供Topic-Broker的关系数据。但有一点需要注意,Broker向Namesr发心跳时,会带上当前自己所负责的所有Topic信息,如果Topic个数太多(万级别),会导致一次心跳中,就Topic的数据就几十M,网络情况差的话,网络传输失败,心跳失败,导致Namesrv误认为Broker心跳失败。

3.2  Broker

    Broker 部署相对复杂,Broker分为 Master 与 Slave,一个 Master 可以对应多个 Slave,但是一个 Slave 只能对应一个Master,Master与 Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为 0 表示 Master,非 0 表示 Slave。Master 也可以部署多个。每个 Broker 与 Name Server 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 Name Server。

    Broker启动,跟所有的Namesrv保持长连接,定时发送心跳包。心跳包中包含当前Broker信息(IP+端口等)以及存储所有topic信息。注册成功后,namesrv集群中就有Topic跟Broker的映射关系。

Broker的高并发读写主要是依靠以下两点:

①  消息顺序写,所有Topic数据同时只会写一个文件,一个文件满1G,再写新文件,真正的顺序写盘,使得发消息TPS大幅提高。

②  消息随机读,RocketMQ尽可能让读命中系统pagecache,因为操作系统访问pagecache时,即使只访问1K的消息,系统也会提前预读出更多的数据,在下次读时就可能命中pagecache,减少IO操作。

负载均衡与动态伸缩

① 负载均衡:Broker上存Topic信息,Topic由多个队列组成,队列会平均分散在多个Broker上,而Producer的发送机制保证消息尽量平均分布到所有队列中,最终效果就是所有消息都平均落在每个Broker上。

②  动态伸缩能力(非顺序消息):Broker的伸缩性体现在两个维度:Topic, Broker。

          1.  Topic维度:假如一个Topic的消息量特别大,但集群水位压力还是很低,就可以扩大该Topic的队列数,Topic的队列数跟发送、消费速度成正比。

         2.  Broker维度:如果集群水位很高了,需要扩容,直接加机器部署Broker就可以。Broker起来后向Namesrv注册,Producer、Consumer通过Namesrv发现新Broker,立即跟该Broker直连,收发消息。

高可用&高可靠

①  高可用:集群部署时一般都为主备,备机实时从主机同步消息,如果其中一个主机宕机,备机提供消费服务,但不提供写服务。

②  高可靠:所有发往broker的消息,有同步刷盘和异步刷盘机制;同步刷盘时,消息写入物理文件才会返回成功,异步刷盘时,只有机器宕机,才会产生消息丢失,broker挂掉可能会发生,但是机器宕机崩溃是很少发生的,除非突然断电。

Broker与namesvr的心跳机制
    单个Broker跟所有Namesrv保持心跳请求,心跳间隔为30秒,心跳请求中包括当前Broker所有的Topic信息。Namesrv会反查Broker的心跳信息,如果某个Broker在2分钟之内都没有心跳,则认为该Broker下线,调整Topic跟Broker的对应关系。但此时Namesrv不会主动通知Producer、Consumer有Broker宕机。

3.3  Producer

    Producer 与 NameServer 集群中的其中一个节点(随机选择)建立长连接,定期从 Name Server 取 Topic 路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master 发送心跳。Producer 完全无状态,可集群部署。

    Producer启动时,也需要指定Namesrv的地址,从Namesrv集群中选一台建立长连接。如果该Namesrv宕机,会自动连其他Namesrv。直到有可用的Namesrv为止。

    生产者每30秒从Namesrv获取Topic跟Broker的映射关系,更新到本地内存中。再跟Topic涉及的所有Broker建立长连接,每隔30秒发一次心跳。在Broker端也会每10秒扫描一次当前注册的Producer,如果发现某个Producer超过2分钟都没有发心跳,则断开连接。

生产者端的负载均衡

    生产者发送时,会自动轮询当前所有可发送的broker,一条消息发送成功,下次换另外一个broker发送,以达到消息平均落到所有的broker上。

    这里需要注意一点:假如某个Broker宕机,意味生产者最长需要30秒才能感知到。在这期间会向宕机的Broker发送消息。当一条消息发送到某个Broker失败后,会往该broker自动再重发2次,假如还是发送失败,则抛出发送失败异常。业务捕获异常,重新发送即可。客户端里会自动轮询另外一个Broker重新发送,这个对于用户是透明的。

3.4  Consumer

    Consumer 与 Name Server 集群中的其中一个节点(随机选择)建立长连接,定期从 NameServer 取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave 发送心跳。Consumer既可以从 Master 订阅消息,也可以从 Slave 订阅消息,订阅规则由 Broker 配置决定。

    消费者启动时需要指定Namesrv地址,与其中一个Namesrv建立长连接。消费者每隔30秒从nameserver获取所有topic的最新队列情况,这意味着某个broker如果宕机,客户端最多要30秒才能感知。连接建立后,从namesrv中获取当前消费Topic所涉及的Broker,直连Broker。

    Consumer跟Broker是长连接,会每隔30秒发心跳信息到Broker。Broker端每10秒检查一次当前存活的Consumer,若发现某个Consumer 2分钟内没有心跳,就断开与该Consumer的连接,并且向该消费组的其他实例发送通知,触发该消费者集群的负载均衡。

消费者端的负载均衡

先讨论消费者的消费模式,消费者有两种模式消费:集群消费,广播消费。

广播消费:每个消费者消费Topic下的所有队列。

集群消费:一个topic可以由同一个ID下所有消费者分担消费。具体例子:假如TopicA有6个队列,某个消费者ID起了2个消费者实例,那么每个消费者负责消费3个队列。如果再增加一个消费者ID相同消费者实例,即当前共有3个消费者同时消费6个队列,那每个消费者负责2个队列的消费。

消费者端的负载均衡,就是集群消费模式下,同一个GROUP下的所有消费者实例平均消费该Topic的所有队列。

4. 关键特性

4.1 刷盘策略

RocketMQ的所有消息都是持久化的,先写入系统 PAGECACHE,然后刷盘,可以保证内存与磁盘都有一份数据,访问时,直接从内存读取。

异步刷盘


    生产者发送的每一条消息并不是立即保存到磁盘,而是暂时缓存起来,然后就返回生产者成功。随后再异步的将缓存数据保存到磁盘,有两种情况:1是定期将缓存中更新的数据进行刷盘,2是当缓存中更新的数据条数达到某一设定值后进行刷盘。这种方式会存在消息丢失(在还未来得及同步到磁盘的时候宕机),但是性能很好。默认是这种模式。

同步刷盘


同步刷盘与异步刷盘的唯一区别是异步刷盘写完 PAGECACHE 直接返回,而同步刷盘需要等待刷盘完成才返回。生产者发送的每一条消息都在保存到磁盘成功后才返回告诉生产者成功。这种方式不会存在消息丢失的问题,但是有很大的磁盘IO开销,性能有一定影响。

4.2 消息存储

 RocketMQ的消息存储是由Consume queue和commit log配合完成的。
 ① Consume Queue
Consume queue是消息的逻辑队列,相当于字典的目录,用来指定消息在物理文件commit log上的位置。
我们可以在配置中指定Consume queue与commit log存储的目录,每个topic下的每个queue都有一个对应的Consume queue文件,比如:

${rocketmq.home}/store/consumequeue/${topicName}/${queueId}/${fileName}

 ② Commit Log

  Commit Log:消息存放的物理文件,每台broker上的commit log被本机所有的queue共享,不做任何区分。

文件的默认位置如下,仍然可通过配置文件修改:

${user.home}/store/${commitlog}/${fileName}

Commit Log的消息存储单元长度不固定,文件顺序写,随机读。消息的存储结构如下表所示,按照编号顺序以及编号对应的内容依次存储。

4.3 服务器消息过滤

RocketMQ 的消息过滤方式有别于其他消息中间件,是在订阅时,再做过滤,先来看下Consume Queue 的存储结构。


①  在 Broker 端进行Message Tag 比对,先遍历 Consume Queue,如果存储的 Message Tag 与订阅的 Message Tag 不符合,则跳过,继续比对下一个,符合则传输给 Consumer。注意:Message Tag 是字符串形式,ConsumeQueue 中存储的是其对应的 hashcode,比对时也是比对 hashcode。

②  Consumer 收到过滤后的消息后,同样也要执行在 Broker 端的操作,但是比对的是真实的 Message Tag 字符串,而不是 Hashcode。

     为什么过滤要这样做?

    a)  Message Tag 存储Hashcode,是为了在 Consume Queue 定长方式存储,节约空间。

    b)  过滤过程中不会访问 Commit Log 数据,可以保证堆积情况下也能高效过滤。

    ③  即使存在 Hash 冲突,也可以在 Consumer 端进行修正,保证万无一失。

4.4 负载均衡与动态伸缩

①  负载均衡:Broker上存Topic信息,Topic由多个队列组成,队列会平均分散在多个Broker上,而Producer的发送机制保证消息尽量平均分布到所有队列中,最终效果就是所有消息都平均落在每个Broker上。

②  动态伸缩能力(非顺序消息):Broker的伸缩性体现在两个维度:Topic, Broker。

    a)  Topic维度:假如一个Topic的消息量特别大,但集群水位压力还是很低,就可以扩大该Topic的队列数,Topic的队列数跟发送、消费速度成正比。

   b)  Broker维度:如果集群水位很高了,需要扩容,直接加机器部署Broker就可以。Broker起来后想Namesrv注册,Producer、Consumer通过Namesrv发现新Broker,立即跟该Broker直连,收发消息。

发送消息负载均衡


如图所示,5 个队列可以部署在一台机器上,也可以分别部署在 5 台不同的机器上,发送消息通过轮询队列的方式

发送,每个队列接收平均的消息量。通过增加机器,可以水平扩展队列容量。

另外也可以自定义方式选择发往哪个队列。

订阅消息负载均衡


如图所示,如果有 5 个队列,2 个 consumer,那么第一个 Consumer 消费 3 个队列,第二 consumer 消费 2 个队列。

这样即可达到平均消费的目的,可以水平扩展 Consumer 来提高消费能力。但是 Consumer 数量要小于等于队列数量,如果 Consumer 超过队列数量,那么多余的 Consumer 将不能消费消息。



猜你喜欢

转载自blog.csdn.net/qq_24313635/article/details/80698836