MQ理论知识归纳

323 阅读15分钟

参考了不少文章资料,沉淀一份个人学习总结,主要包括了RocketMQ和Kafka的相关特性。

RocketMQ

组件介绍

image.png NameServer
服务注册中心,用于保存Broker的信息,所有Broker要在每个NameServer上注册信息,并主动发送心跳包(30s)。NameServer可以多机部署变成一个集群保证高可用,但这些机器间彼此并不通信,也就是说三者的元数据舍弃了强一致性(NameServer没有状态,可以横向扩展)。Producer在发送消息前会根据TopicNameServer获取路由(到Broker)信息;Consumer也会定时获取Topic路由信息。NameServer主要功能有:

  • 接收Broker的请求,注册Broker的路由信息
  • 接收ProducerConsumer的请求,提供对应的TopicBroker的路由信息

Broker
可以暂时理解为一台虚拟机/物理机,存放实际的消息。Broker可以有master和slave,二者进行同步确保高可用,如果master宕机,通过raft协议进行选举。
每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 BrokerQueue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个Queue中。

Producer
消息生产者,负责发送消息。发送同一类消息的Producer是一个生产者组。
生产者会随机找一个NameServer获取全部的BrokerTopic信息,进行消息的投送。如果某个Broker挂掉了,那么生产者会收不到Broker的投送成功的确认消息,这时会将Broker加入到下次不能投送的名单之中,避免多次投送到不可用的Broker

每个Producer可以对应多个Topic

Consumer
消息消费者,接受消息,Push/Pull。消费同一类消息(同一个Topic)的消费者是一个消费者组。

  • Push Consumer:一般是从客户端新建一个Consumer,在Consumer上注册一个监听器,每次来消息,由Broker推送。
  • Pull Consumer:一般是由客户端创建一个Consumer,调用Consumer的Pull方法进行消息的拉取消费,消费完客户端就继续拉取消息进行消费,注意,一般都是批量拉取消息,在Consumer本地一般都有个WorkBufferWorkBuffer没满就会持续拉取消息。
  • 集群消费:消息会被均摊到每个Consumer上,消息只会被消费一次。
  • 广播消费:消息会被重复消费(每个Consumer都会消费一次,不是一个Consumer消费多次)。
    Consumer并不能指定从master/slave读。当master不可用或者繁忙的时候会自动切换到从slave读。

一个Consumer也能对应多个Topic

消息载体

Topic
消息的逻辑分类,同一个Topic可以包含多个Queue,比如Topic1-Queue1,Topic1-Queue2,Topic2-Queue1,Topic2-Queue2。一个Topic的不同Queue可以分布在不同的Borker上。每个Topic是一类信息的集合,一般是以某个功能或者服务为单位。
在业务增长,消息量增大时,可以增加TopicQueue,这样可以将压力分摊到更多的Broker上。

Tag
在我理解来就是用来初步过滤信息用的,使用起来可比较灵活。

Queue
一个Topic可以为更多的Consumer服务,假设在一个Broker上的Topic分片有5个Queue,一个Consumer Group内有2个Consumer按照集群消费的方式消费消息,按照平均分配策略进行负载均衡得到的结果是:第一个 Consumer 消费3个Queue,第二个Consumer 消费2个Queue。如果增加Consumer,每个Consumer分配到的Queue会相应减少。Rocket MQ的负载均衡策略规定:Consumer数量应该小于等于Queue数量,如果Consumer超过Queue数量,那么多余的Consumer 将不能消费消息。

在一个Consumer Group内,QueueConsumer之间的对应关系是多对一的关系:一个Queue最多只能分配给一个Consumer,一个Cosumer可以分配得到多个Queue。这样的分配规则,每个Queue只有一个消费者,可以避免消费过程中的多线程处理和资源锁定,有效提高各Consumer消费的并行度和处理效率。 资料

类似kafka的Partition;负载均衡;提升性能;可以防止多个Consumer重复消费消息;有序消费需要注意。(待补充)

消息生产与消费

生产

  • Producer端在发送消息的时候,会先根据Topic找到相关的BrokerQueue信息,在获取了路由信息后,RocketMQ的客户端在默认方式下从Queue列表中按照一定策略选择一个Queue进行发送消息。支持轮询、哈希以及自定义。
  • Producer支持同步、异步发送,且都可以在发送失败后重试,如果单个Broker发生故障,重试会选择其他 Broker保证消息正常发送。默认会尝试发送三次。
  • Producer客户端会维护一个Broker-发送延迟信息列表。每次发送信息时选择一个发送延迟级别较低的Broker,以最大限度地利用整个Broker集群的性能,尽量避开那些有潜在风险的Broker消费
  • Consumer有两种消费模式,广播消费和集群消费,一般使用集群消费,广播消费不支持顺序消息,集群模式消费可以通过设置queue数量为以支持顺序消息。RocketMQ消息消费
  • Consumer客户端支持延缓重试16次(重试时间间隔依次拉长),如果全部重试失败,则会进入死信队列,不能再进行消费。
  • Consumer在以下场景会进行Rebalance(只有集群模式有Rebalance):
    • Consumer启动/停止运行,通知对应的Broker,并让Broker通知其余的Consumer进行Rebalance
    • Topic扩/缩容、Broker掉线等场景下,由Broker通知进行Rebalance
    • Consumer自身的定时任务会去拉取信息是否进行Rebalance

Rebalance的决定性因素在于当前的订阅信息以及分配算法,只要同一个ConsumerGroup中的Consumer各自获取的信息是一致的,最终的分配结果也就是一致的。
Rebalance具体内容可以查阅相关资料:深入理解RocketMQ Rebalance机制

Broker的主从机制

消息拉取

  • Broker 接收到消息消费者拉取请求,在获取本地堆积的消息量后,会计算服务器的消息堆积量是否大于物理内存的一定值(默认40%),如果是,则标记下次从Slave服务器拉取,返回Slave服务器的brokerID
  • 当消费者收到拉取响应回来的数据后,会将下次建议拉取的 brokerID 缓存起来。以便下次拉取消息时,确定向哪个节点发送请求 消息同步 RocketMQ有三种同步机制:
  • ASYNC_MASTER:主节点异步复制
  • SYNC_MASTER:主节点同步复制
  • SLAVE:主节点异步复制,从节点定时复制

资料:juejin.cn/post/696346…

最新的RocketMQ是支持主从切换的wanchuan.top/62a8da94a80…

高可用

  • RocketMQ天然支持高可用,支持多主多从的Broker部署架构。
  • RocketMQ中没有master选举功能,NameServer是无状态的,不负责整体的故障处理。
  • 如果master挂了,如果当前架构是一主多从,就意味该Broker无法接收Producer的消息了,但是消费者仍然能够继续消息(从节点拉消息)。如果当前架构是多主多从,就可以保障当其中一个节点挂了,另外一个master节点仍然能够对外提供消息发送服务。
  • Broker是数据存储节点,不是消息发送的最小单位,消息发送是以Topic为单位,而一个TopicQueue不会都在一个Broker上,这就保证了,即使没有多主多从,只要不要全部Broker都挂掉,就能保证该Topic的消息写入。唯一的问题就是Broker挂掉,少了Queue,会触发rebalance,消费速度变慢。

数据存储

RokectMQ的几个特点:

  • 磁盘存储:吞吐量大,性能相对差点
  • pagecachemmap:提高磁盘读写性能
  • commitlog数据文件:顺序写,提高写性能
  • consumerqueueindex索引文件:快速搜索,提高读性能

commitlog
commitlog的几个重点:

  • commitlog存储的是具体的消息,顺序追加,消息结构体的第一个32位数据是消息的大小,拿到第一个数据就能知道消息的整体
  • commitlog每个文件默认1G,大了就切分,消息定位过程就是二分查找过程 官方描述如下:

CommitLog:消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件。 commitlog的存储格式如下,需要注意的是,整个消息的大小位于消息头前4个字节(int),这样拿到每次根据索引定位到消息数据时,就知道整个消息数据的范围了。

    // org.apache.rocketmq.store.CommitLog.DefaultAppendMessageCallback#doAppend
    ...
    // Initialization of storage space
    this.resetByteBuffer(msgStoreItemMemory, msgLen);
    // 1 TOTALSIZE, 首先将消息大小写入
    this.msgStoreItemMemory.putInt(msgLen);
    // 2 MAGICCODE
    this.msgStoreItemMemory.putInt(CommitLog.MESSAGE_MAGIC_CODE);
    // 3 BODYCRC
    this.msgStoreItemMemory.putInt(msgInner.getBodyCRC());
    // 4 QUEUEID
    this.msgStoreItemMemory.putInt(msgInner.getQueueId());
    // 5 FLAG
    this.msgStoreItemMemory.putInt(msgInner.getFlag());
    // 6 QUEUEOFFSET
    this.msgStoreItemMemory.putLong(queueOffset);
    // 7 PHYSICALOFFSET
    this.msgStoreItemMemory.putLong(fileFromOffset + byteBuffer.position());
    // 8 SYSFLAG
    this.msgStoreItemMemory.putInt(msgInner.getSysFlag());
    // 9 BORNTIMESTAMP
    this.msgStoreItemMemory.putLong(msgInner.getBornTimestamp());
    // 10 BORNHOST
    this.resetByteBuffer(bornHostHolder, bornHostLength);
    this.msgStoreItemMemory.put(msgInner.getBornHostBytes(bornHostHolder));
    // 11 STORETIMESTAMP
    this.msgStoreItemMemory.putLong(msgInner.getStoreTimestamp());
    // 12 STOREHOSTADDRESS
    this.resetByteBuffer(storeHostHolder, storeHostLength);
    this.msgStoreItemMemory.put(msgInner.getStoreHostBytes(storeHostHolder));
    // 13 RECONSUMETIMES
    this.msgStoreItemMemory.putInt(msgInner.getReconsumeTimes());
    // 14 Prepared Transaction Offset
    this.msgStoreItemMemory.putLong(msgInner.getPreparedTransactionOffset());
    // 15 BODY
    this.msgStoreItemMemory.putInt(bodyLength);
    if (bodyLength > 0)
        this.msgStoreItemMemory.put(msgInner.getBody());
    // 16 TOPIC
    this.msgStoreItemMemory.put((byte) topicLength);
    this.msgStoreItemMemory.put(topicData);
    // 17 PROPERTIES
    this.msgStoreItemMemory.putShort((short) propertiesLength);
    if (propertiesLength > 0)
        this.msgStoreItemMemory.put(propertiesData);

    final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
    // Write messages to the queue buffer
    byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);

consumerqueue
consumerqueue本质就是存储Topic中的queue的消息的地址。消费者会根据偏移量,而这些偏移量,实际上就是指的consumequeue的偏移量 (注意不是commitlog的偏移量)。这样做有什么好处呢?
如果想一次性定位数据,那么唯一的办法是直接使用commitlog的offset。但这会带来一个最大的问题,Consumer如何判断自己消费的Topic的下一条消息?如果单靠commitlog文件,那么,它必须将下一条消息读入,然后再根据topic信息判定是不是需要的数据。如此一来,就必然存在大量的commitlog文件的io问题了。如果有10个Consumer,那么IO负载将是使用consumerqueue配合commitlog模式的十倍。官方描述如下:

ConsumeQueue:消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。Consumer即可根据consumequeue来查找待消费的消息。其中,consumequeue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在commitlog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。consumequeue文件可以看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样consumequeue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M;

index
index文件是为搜索场景而生的,如果没有搜索业务需求,则这个实现是意义不大的。它更多的需要借助于时间限定,以key或者id进行查询。不属于实时场景应用。官方描述如下:

IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。Index文件的存储位置是:HOME \store\index\{fileName},文件名fileName是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引,IndexFile的底层存储设计为在文件系统中实现HashMap结构,故rocketmq的索引文件其底层实现为hash索引。
IndexFile索引文件为用户提供通过“按照Message Key查询消息”的消息索引查询服务,IndexFile文件的存储位置是:HOME\store\index\{fileName},文件名fileName是以创建时的时间戳命名的,文件大小是固定的,等于40+500W*4+2000W*20= 420000040个字节大小。如果消息的properties中设置了UNIQ_KEY这个属性,就用 topic + “#” + UNIQ_KEY的value作为 key 来做写入操作。如果消息设置了KEYS属性(多个KEY以空格分隔),也会用 topic + “#” + KEY 来做索引。
其中的索引数据包含了Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共20 Byte。NextIndex offset 即前面读出来的 slotValue,如果有 hash冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来了。Timestamp记录的是消息storeTimestamp之间的差,并不是一个绝对的时间。整个Index File的结构如图,40 Byte 的Header用于保存一些总的统计信息,4*500W的 Slot Table并不保存真正的索引数据,而是保存每个槽位对应的单向链表的头。20*2000W 是真正的索引数据,即一个 Index File 可以保存 2000W个索引。

消息定位

consumerqueue定位
根据consumerqueue的offset,可以拿到commitlog的offset以及对应消息的长度,根据offset以及配置中定义的每个commitlog文件长度,定位到commitlog文件和文件中的偏移量,再配合消息长度,即可拿到一个完整消息。 index定位
根据key的哈希值从index文件(实际上就是一个哈希表)中拿到的链表头,根据next index进行遍历,匹配key拿到符合要求的commitlog的offset以及对应消息的长度,根据offset以及配置中定义的每个commitlog文件长度,定位到commitlog文件和文件中的偏移量,读取commitlog文件的偏移量的前4个字节,这就知道了消息体整体长度,再读一次数据即可拿到一个完整消息。

资料:# RocketMQ消息存储和查询原理RocketMQ之六:RocketMQ消息存储

消息消费进度保存

如果是广播模式消费,消息的消费进度是保存到本地,如果是集群消费模式,消息的消费进度则是保存到 Broker(新加入的消费者只会从最新的消费进度进行消费),但无论是保存到本地,还是保存到 Broker,消费者都会在本地留一份缓存。
在集群模式下:

  • 从节点会定时从主节点拉取消费进度
  • 消费者消费完消息后,会告诉主节点或者从节点自己的消费进度
  • 消费者自己存有消费进度,每次请求会带上这个消费进度,所以每次拉取的都是没消费的消息

消息的可靠投递

一个消息的生命周期,从生产者起,至消费者止。如何确保生产和消费是十分重要的一环,从生产者来看,就是如何确保消息给到了消费者;从消费者来看,就是怎么保证不重复消费消息:

RocketMQ凭什么这么快

  • 零拷贝
  • 顺序读写:写的时候顺序写,机械操作很少;读的时候预读,有缓存,对于MQ这种整体来看顺序消费的应用,缓存非常有用


Kafka

KafKa的设计跟RocketMQ很像,可以先看这篇文章:zhuanlan.zhihu.com/p/68052232

技术细节:gitbook.cn/books/5ae1e…

要注意Zookeeper管理

待更新



RocketMQ与Kafka的异同

共同点

  1. 一条消息会被订阅了同一个TopicConsumerGroup都消费一次,但是只会由同一个ConsumerGroup钟的一个Consumer消费;一个Consumer可以消费多个Queue或者Partition;一个Queue或者Partition只能被一个Consumer消费。

  2. QueuePartition在不同Broker上都有副本,并且都有master/leader节点,从节点的数据都由主节点同步。

不同点

  1. 对于commitlog/log文件,在一个Broker上RocketMQ的全部Topic都存到同一个文件中,而kafka不同Topic则分文件存储。
  • RocketMQ这么设计主要是为了能够进行顺序写,提高IO效率,时延更低。但是consumequeue文件还是分开存储的,以consumequeue文件的顺序和定长保证消息读取的效率,以及允许commitlog可无序且不定长存储。
  • Kafka的分文件存储后,局部是顺序写,全局是随机写,时延会比RocketMQ要高,但是易扩展,整体的吞吐量会更大。 总结就是,RocketMQ更适合在线场景(订单系统),Kafka更适合离线场景(log收集)
  1. kafka为什么具备更高的吞吐量?
  • 生产端会对消息进行Batch以及压缩
  • commitlog/log文件的设计区别
  • Partition并发读写的模型
  1. kafka的集群由Zookeeper管理,RocketMQ由NameServer管理。

  2. RocketMQ支持tag过滤。

问题

  1. 如何提升消息对咧Consumer消费能力,消费能力的提升比较复杂,需要具体情况具体分析
  • 如果判断瓶颈是SDK导致的,那可以看看SDK是否有消费的优化
  • 如果判断瓶颈是消费逻辑的耗时,那可以看看消费逻辑是否有优化的空间
  • 如果确定两者都已经是没有优化了,那就需要看看Queue的数量和消费实例的关系;如果消费实例过少,小于Queue的数量,可以扩容消费实例,或者增加消费端的worker数量。或者增加maxInflight的数量,如果是Queue数量过少导致的,可以进行扩容