RocketMQ组件及原理深度剖析详解

 

 RocketMQ于2017年9月成为Apache基金会的顶级项目。有着支撑亿级消息量的能力,可以为复杂的业务场景提供系统解耦、削峰填谷、以及低延迟、高吞吐的能力,下面将详细介绍RockeMQ的核心组件和功能,以及细节。

 

RocketMQ应用场景及作用? 

    应用解耦:用户调用订单系统创建订单后,分别调用库存系统、支付系统、物流系统,使用解耦之后,支付系统、库存系统、物流系统分别从消息队列中去消费。

    流量消峰:平时的qps为1千左右,在某个秒杀时刻达到了1万,其实没必要花大资金升级系统,只需要将消息缓存在消息队列中即可,起到缓冲左右,避免请求全部一次性到订单系统。

    消息分发:团队之间数据的推送。

 

RocketMQ中的角色及作用?

    Producer:发送消息,Comsumer:消费消息,Broker:存储、传输消息,NameServer:协调各个地方

    为了消除单点故障,增加可靠性和吞吐量,可以在多台机器上部署多个NameServer和Broker,为每一个Broker部署一个或多个Slave。

    启动的顺序:先启动NameServer,再启动Broker,这时候消息队列就可以提供服务了。

    Broker:负责接收Producer发过来的消息、处理Consumer的消费消息请求、消息的持久化存储、消息的HA机制以及服务端过滤功能等。

    NameServer功能:

    1)集群的各个组件通过它了解全局信息,各个角色机器定期向NameServer上报自己的状态,超时不上报的话,NameServer会认为某个机器出故障不可用了,其他的组件会把这个机器从可用列表中移除。

    2)NameServer可以部署多个,相互之间独立,其他角色同时向多个NameServer机器上报状态信息,从而达到热备份的目的。NameServer本身是无状态的,也就是NameServer中的Broker、Topic等状态信息不会持久存储,都是各个角色定时上报并存储到内存中的。

    集群状态的存储结构:主要有下面5个变量,源码如下

public class RouteInfoManager {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    // key为Topic名称,存储了所有Topic的属性信息
    // value是QueueData队列,队列长度为这个Topic数据存储的Master Broker的个数
    // QueueData里存储着Broker的名称、读写queue的数量、同步标识等
    private final HashMap> topicQueueTable;

    // 以BrokerName为索引,相同的名称的Broker可能存在多台机器,一个Master和多个Slave
    // BrokerName对应的属性信息包括Cluster名称,一个Msater Broker和多个Slave Broker的地址信息
    private final HashMap brokerAddrTable;

    // 存储集群中Cluster的信息,就是一个Cluster名称对应一个BrokerName组成的集合
    private final HashMap> clusterAddrTable;

    // 这个结构和brokerAddrTable有关,key是BrokerAddr,也就是对应着一台机器
    // value是这台Broker机器的实时状态,包括上次更新状态的时间戳,NameServer会定期检查这个时间戳
    // 超时没有更新就认为这个Broker无效了,并将其从Broker列表移除
    // 每10秒检查一次、时间戳超过2分钟则认为Broker已失效
    private final HashMap brokerLiveTable;

    // Filter Server是过滤服务器,是一种服务端过滤方式,一个Broker可以有一个或多个Filter Server
    // key为Broker的地址,value是和这个Broker关联的多个Filter Server的地址
    private final HashMap/* Filter Server */> filterServerTable;

    // 通过构造函数初始化
    public RouteInfoManager() {
        this.topicQueueTable = new HashMap>(1024);
        this.brokerAddrTable = new HashMap(128);
        this.clusterAddrTable = new HashMap>(32);
        this.brokerLiveTable = new HashMap(256);
        this.filterServerTable = new HashMap>(256);
    }
    ...
}

 

创建Topic的流程是怎样?    

    1)通过创建Topic的命令,指定在哪个Broker上创建Topic的Message Queue

    2)创建Topic命令被发往对应的Broker,Broker接到创建Topic的请求后,执行具体的创建逻辑

    3)创建的最后一步会向NameServer发送注册信息,NameServer完成创建Topic的逻辑后,其他客户端才能发现新增的Topic,逻辑在RouteInfoManager.registerBroker()函数里。

    registerBroker()主要逻辑:首先更新Broker信息,然后对每个Master角色的Broker,创建一个QueueData对象,如果是新建Topic,就是添加QueueData对象;如果是修改Topic,就是把旧的QueueData删除,加入新的QueueData。

 

为什么不用Zookeeper做集群的管理?

    因为Zookerper太重,包括master选举,而RocketMQ不需要这些复杂的功能,而且也可以避免引入另一个中间件,增加维护成本。

 

RocketMQ的消费者类型?

    1)DefaultMQPushConsumer:系统收到消息后自动调用处理函数处理消息,自动保存Offset,而且加入新的DefaultMQPushConsumer后自动做负载均衡。默认采用循环的方式逐个读取一个Topic的所有MessageQueue。

    DefaultMQPushConsumer需要设置三个参数:Consumer的GroupName、NameServer的地址和端口号、Topic的名称(不需要topic下的所有消息时,可通过指定消息的tag标签用于过滤)

    GroupName:用于把多个Consumer组织到一起,提高并发处理能力,它需要和消息模式配合使用

    Rocket支持2种消息模式:Clustering和Broadcasting

    Clustering模式下:同一个ConsumerGroup里的每个Consumer只消费所订阅消息的一部分内容,同一个ConsumerGroup里所有的Consumer消费的内容合起来才是所订阅Topic内容的整体,从而达到负载均衡的目的。

    Broadcasting模式下:同一个ConsumerGroup里的每个Consumer都能消费所订阅Topic的全部消息,也就是一个消息会被多个消费者消费。使用存储Offset的类是LocalFileOffsetStrore。

    特点:以长轮询的方式通过Client端和Server端的配合,达到既有pull的优点又有push的实时性。

    通过设置Broker的最长阻塞时间(默认15秒),当服务端收到新消息请求后,如果队列没有新消息,则进入循环不断查看状态,每次waitForRunning一段时间(默认5秒),然后再check,超时后就返回空结果。在这个期间如果收到了新的消息,会直接调用notifyMessageArriving函数返回请求结果。

    缺点:这种长轮询的方式会占用资源,适合用在消息队列这种客户端连接数可控的场景。

    流量控制:PushConsumer有个线程池,消息处理逻辑在各个线程里同时执行。

    那么客户端如何得知当前消息堆积数量?如何重复处理某些消息?如何延迟处理某些消息呢?

    通过定义了一个快照类ProcessQueue,在运行的时候每个Message Queue都会有个对应的ProcessQueue对象,保存了这个Message Queue消息处理状态的快照。

    ProcessQueue对象里主要是一个TreeMap和一个读写锁。TreeMap中以Message Queue的Offset作为key,消息内容的引用为value,保存了所有从Message Queue获取到,但还未被处理的消息。读写锁控制着每个线程对TreeMap对象的并发访问。

    PushConsumer在每次pull请求前会做三个判断来控制流量:未处理的消息数、消息总大小、Offset速度,任何一个超过都会隔一段时间再拉去消息。

    2)DefaultMQPullConsumer:由使用者自主控制读取操作

    主要处理以下三件事情:

    获取某个Message Queue并遍历下面每条消息:一个Topic包括多个Message Queue,则需要遍历多个Message Queue。

    维护Offsetstore:从一个message Queue里拉取消息的时候,要传入Offset参数(long类型),随着不断的读取,Offset会不断增长。这个时候由用户负责把Offset存储下来,内存或者磁盘。

    根据不同的消息状态做不同处理:如FOUNT、NO_NEW_MSG,分别表示获取到消息和没有新消息。

 

Consumer的启动、关闭流程?

    PullConsumer关闭:主动权更高,可以根据实际情况,暂停、停止、启动消费过程,需要注意的是Offset的保存,要在程序的异常处理部分增加把Offset写入磁盘方面的处理。

    PushConsumer关闭:要调用shutdown()函数、以便释放资源、保存Offset等。这个调用要加到Consumer所在应用的退出逻辑中。

    PushConsumer启动:会在启动时做各种配置检查,然后连接NameServer获取Topic信息,如果遇到无法连接NameServer的异常,依然会正常启动,但是不会收到消息。 这样是为了保证集群在多个NameServer时,某一个连接异常时不立刻退出,而是不断重新连接,保证整体服务依然可用。

 

生产者类型以及发送消息过程?

    DefaultMQProducer:RocketMQ默认使用的类,在发送消息时,需要经历以下五个步骤:

    1)设置Producer的GroupName

    2)设置InstanceName,不设置的话默认为"Default",当一个JVM启动多个Producer的时候通过InstanceName来区分。

    3)设置发送失败重试次数,保证消息不丢

    4)设置NameServer地址

    5)组装消息并发送

消息的发送方式:

    通过Broker配置文件里的flushDiskType参数设置:SYNC_FLUSH、ASYNC_FLUSH

    同步刷盘:在返回写状态成功时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。

    异步刷盘:在返回写状态成功时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大,当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。

消息发送的返回状态、发送的方式:

    FLUSH_DISK_TIMEOUT:没有在规定时间内完成刷盘(需要Broker的刷盘策略为SYNC_FLUSH才会报这个错误)

    FLUSH_SLAVE_TIMEOUT:表示在主备方式下,并且Broker被设置为SYNC_MASTER方式,没有在设定时间内完成主从同步

    SLAVE_NOT_AVAILABLE:产生的场景和FLUSH_SLAVE_TIMEOUT类似,没有找到被配置成Slave的Broker

    SEND_OK:发送成功,上面三种失败都没发生就是成功

    发送延迟消息:通过调用setDelayTimeLevel(int level)

    自定义消息发送规则:默认会轮流向各个Message Queue发送消息。Consumer消费的时候回根据负载均衡策略,消费被分配到的Message Queue。如果不经过特定设置,消息发往哪个队列,被谁消费都是未知的。通过MessageQueueSelector对象作为参数可以指定发到哪个Message Queue。

 

RocketMQ对事务的支持?

    客户端有三个类来支持用户实现事务消息:

    1)LocalTransactionExecutor:根据情况返回LocalTransactionState.ROLLBACK_MESSAGE或者LocalTransactionState.COMMIT_MESSAGE状态

    2)TransactionMQProducer:用法和DefaultMQProducer类似,要通过它启动一个Producer,但是比DefaultMQProducer多设置本地事务处理函数和回查请求状态函数

    3)TransactionCheckListener:实现MQ服务器的回查请求,返回LocalTransactionState.ROLLBACK_MESSAGE或LocalTransactionState.COMMIT_MESSAGE

 

如何存储Offset和调整Offset?

    Offset是指一条消息在某个消息队列中的位置。

    Offset的类结构:主要分为本地文件类型(LocalFileOffsetStore)和Broker代存类型(RemoteBrokerOffsetStore)两种。

    OffsetStore使用Json格式存储。

RocketMQ底层通信机制?

    相关的代码在Remoting模块里,最上层是RemotingServer接口,定义了三个方法:

void start();
void shutdown();
void registerRPCHook(RPCHook rpcHook);

    RemotingClient接口和RemotingServer接口继承RemotingService接口,并增加了自己特有的方法。

    NettyRemotingClient和NettyRemotingServer分别实现了RemotingClient和RemotingServer,而且都继承了NettyRemotingAbstract类,这两个类是基于netty实现的。

    RocketMQ各个模块间的通信,通过发送同一格式的自定义消息(RemotingCommand)来完成,大部分逻辑都是通过发送、接受并处理Command来完成的。

 

消息的存储和发送机制?

    一台服务器把本地磁盘文件的内容发送到客户端,一般分为两个步骤:

    1)read(file, tmp_buf, len):读取本地文件内容

    2)write(socket, tmp_buf, len):将读取的内容通过网络发送出去

    tmp_buf是预先申请的内存。这两个操作实际上进行了4次数据复制:从磁盘复制到内核态内存、从内核态内存复制到用户态内存(完成read)、从用户态内存复制到网络驱动的内核态内存、从网络驱动的内核态内存复制到网卡中进行传输(完成write)    

    通过mmap的方式,省去向用户态的内存复制,提高速度。在Java中是通过MappedByteBuffer实现的,RocketMQ充分利用上述特性,也就是所谓"零拷贝"技术,提高消息存盘和网络发送的速度。

 

消息的存储结构?

    RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的。

    CommitLog:消息真正的物理存储文件,以物理文件的方式存放,每台Broker上的CommitLog被本机器所有ConsumerQueue共享。在CommitLog中,一个消息的存储长度是不固定的,RocketMQ采取一些机制,尽量向CommitLog中顺序写,但是随机读。ConsumerQueue的内容也会被写到磁盘里作持久存储。

    ConsumeQueue:消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址,每个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。文件地址是:

${${storeRoot}\consumequeue\${topicName}\${queueId}\${fileName}}

    存储机制这样设计的好处:

    1)CommitLog顺序写,可以大大提高写入效率。(顺序写比随机写性能高6000倍) 

    2)虽然是随机读,但是利用操作系统的pagecache机制,可以批量地从磁盘读取,作为cache存到内存中,加速后续的读取速度。

    3)为了保证CommitLog和ConsumeQueue的一致性,CommitLog里存储了Consume Queues、Message Key、Tag等所有信息,即使ConsumeQueue丢失也可以通过CommitLog恢复。

 

高可用机制?

    通过Master和Slave的配合达到高可用性的。

    Master和Slave区别:

    1)在Broker的配置文件中,参数brokerId的值为0表示Master,大于0表示Slave。

    2)Master支持读和写,Slave仅支持读,也就是Producer只能和Master角色的Broker连接写入消息

    在Consumer的配置文件中,不需要设置从Master读还是Slave读,当Master不可用或者繁忙时,Consumer会被自动切换到从Slave读,这样就达到了消费端的高可用性。

    在创建Topic的时候,把Topic的多个Message Queue创建在多个Broker组上,这样当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息。RocketMQ目前还不支持Slave自动转成Master,如果需要,则要手动停止Slave角色的Broker,更改配置文件,用新的配置文件启动Broker。

 

RocketMQ的顺序消息实现?

    RocketMQ在默认情况下不保证顺序,比如创建一个Topic,默认八个写队列,八个读队列。这时候一条消息可能被写入任意一个队列里;在数据的读取过程中,可能有多个Consumer,每个Consumer也可能启动多个线程并行处理,所以消息被哪个Consumer消费,被消费的顺序和写入的顺序是否一致是不确定的。

    保证全局顺序消息:需要先把Topic的读写队列数设置为一,然后Producer和Consumer的并发设置也要是一。

    部分顺序消息:在发送端,把同一业务ID的消息发送到同一个Message Queue;在消费过程中,要做到从同一个Message Queue读取的消息不被并发处理。

    发送端:通过MessageQueueSelector类来控制把消息发往哪个Message Queue。

    消费端:通过MessageListenerOrderly类来解决但Message Queue的消息被并发处理的问题。

    MessageListenerOrderly实现原理:不仅通过参数限制一次从Broker的一个消息队列获取消息的最大数量,而且在具体实现中,为每个Consumer Queue加个锁,消费每个消息前,必须先获取对应Consumer Queue的锁,保证同一时间,同一个Consumer Queue的消息不被并发消费,但不同的Consumer Queue的消息可以并发处理。

 

 如何动态增减机器?

   动态增减NameServer,优先级从高到底依次如下:

    1)代码设置setNamesrvAddr()

    2)Java启动参数设置,rocketmq.namesrv.addr

    3)Linux的环节变量设置

    4)HTTP服务来设置,请求指定的URL地址(唯一支持动态增减NameServer的方式,无需重启),使用后其他组件会每隔2分钟请求一次该URL,获取最新的NameServer地址

    动态增减Broker:

    增减Broker后,一是可以把新建的Topic指定到新的broker机器上,均衡利用资源。另一种是通过updateTopic命令更改现有的Topic配置,在新加的Broker上创建新的队列。

    当Topic只有一个Master Broker时:停掉Broker后,发送消息会受到影响

    当Topic有多个Master Broker时:如果使用同步方式发送,则会重试,自动向另一个Broker发消息,不会受影响。如果使用异步方式发送,则会丢失切换过程中的消息,因为异步方式下,发送失败不会重试。

 

PS:很久没来发博客了,由于工作问题,没时间整理,所以都整理在自己的云笔记中,现在准备重新捡来,把之前的总结,学习,分享出来,然后上周建立了自己的微信公众号,每周更新技术分享,感兴趣的小伙伴可以关注一下,另外可以加我讨论学习,进入我的微信技术群讨论,分享或者内推。。。

微信公众号:RocketMQ组件及原理深度剖析详解_第1张图片

个人微信:T_Stone11

 

你可能感兴趣的:(java)