参考了不少文章资料,沉淀一份个人学习总结,主要包括了RocketMQ和Kafka的相关特性。
RocketMQ
组件介绍
NameServer
服务注册中心,用于保存Broker的信息,所有Broker要在每个NameServer上注册信息,并主动发送心跳包(30s)。NameServer可以多机部署变成一个集群保证高可用,但这些机器间彼此并不通信,也就是说三者的元数据舍弃了强一致性(NameServer没有状态,可以横向扩展)。Producer在发送消息前会根据Topic到NameServer获取路由(到Broker)信息;Consumer也会定时获取Topic路由信息。NameServer主要功能有:
- 接收
Broker的请求,注册Broker的路由信息 - 接收
Producer和Consumer的请求,提供对应的Topic的Broker的路由信息
Broker
可以暂时理解为一台虚拟机/物理机,存放实际的消息。Broker可以有master和slave,二者进行同步确保高可用,如果master宕机,通过raft协议进行选举。
每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个Queue中。
Producer
消息生产者,负责发送消息。发送同一类消息的Producer是一个生产者组。
生产者会随机找一个NameServer获取全部的Broker和Topic信息,进行消息的投送。如果某个Broker挂掉了,那么生产者会收不到Broker的投送成功的确认消息,这时会将Broker加入到下次不能投送的名单之中,避免多次投送到不可用的Broker。
每个
Producer可以对应多个Topic。
Consumer
消息消费者,接受消息,Push/Pull。消费同一类消息(同一个Topic)的消费者是一个消费者组。
- Push
Consumer:一般是从客户端新建一个Consumer,在Consumer上注册一个监听器,每次来消息,由Broker推送。 - Pull
Consumer:一般是由客户端创建一个Consumer,调用Consumer的Pull方法进行消息的拉取消费,消费完客户端就继续拉取消息进行消费,注意,一般都是批量拉取消息,在Consumer本地一般都有个WorkBuffer,WorkBuffer没满就会持续拉取消息。 - 集群消费:消息会被均摊到每个
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是一类信息的集合,一般是以某个功能或者服务为单位。
在业务增长,消息量增大时,可以增加Topic的Queue,这样可以将压力分摊到更多的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内,Queue和Consumer之间的对应关系是多对一的关系:一个Queue最多只能分配给一个Consumer,一个Cosumer可以分配得到多个Queue。这样的分配规则,每个Queue只有一个消费者,可以避免消费过程中的多线程处理和资源锁定,有效提高各Consumer消费的并行度和处理效率。
资料
类似kafka的
Partition;负载均衡;提升性能;可以防止多个Consumer重复消费消息;有序消费需要注意。(待补充)
消息生产与消费
生产
Producer端在发送消息的时候,会先根据Topic找到相关的Broker和Queue信息,在获取了路由信息后,RocketMQ的客户端在默认方式下从Queue列表中按照一定策略选择一个Queue进行发送消息。支持轮询、哈希以及自定义。Producer支持同步、异步发送,且都可以在发送失败后重试,如果单个Broker发生故障,重试会选择其他Broker保证消息正常发送。默认会尝试发送三次。Producer客户端会维护一个Broker-发送延迟信息列表。每次发送信息时选择一个发送延迟级别较低的Broker,以最大限度地利用整个Broker集群的性能,尽量避开那些有潜在风险的Broker。 消费Consumer有两种消费模式,广播消费和集群消费,一般使用集群消费,广播消费不支持顺序消息,集群模式消费可以通过设置queue数量为以支持顺序消息。RocketMQ消息消费Consumer客户端支持延缓重试16次(重试时间间隔依次拉长),如果全部重试失败,则会进入死信队列,不能再进行消费。Consumer在以下场景会进行Rebalance(只有集群模式有Rebalance):Consumer启动/停止运行,通知对应的Broker,并让Broker通知其余的Consumer进行RebalanceTopic扩/缩容、Broker掉线等场景下,由Broker通知进行RebalanceConsumer自身的定时任务会去拉取信息是否进行Rebalance
Rebalance的决定性因素在于当前的订阅信息以及分配算法,只要同一个
ConsumerGroup中的Consumer各自获取的信息是一致的,最终的分配结果也就是一致的。
Rebalance具体内容可以查阅相关资料:深入理解RocketMQ Rebalance机制
Broker的主从机制
消息拉取
- Broker 接收到消息消费者拉取请求,在获取本地堆积的消息量后,会计算服务器的消息堆积量是否大于物理内存的一定值(默认40%),如果是,则标记下次从Slave服务器拉取,返回Slave服务器的brokerID
- 当消费者收到拉取响应回来的数据后,会将下次建议拉取的 brokerID 缓存起来。以便下次拉取消息时,确定向哪个节点发送请求 消息同步 RocketMQ有三种同步机制:
- ASYNC_MASTER:主节点异步复制
- SYNC_MASTER:主节点同步复制
- SLAVE:主节点异步复制,从节点定时复制
最新的RocketMQ是支持主从切换的:wanchuan.top/62a8da94a80…
高可用
- RocketMQ天然支持高可用,支持多主多从的
Broker部署架构。 - RocketMQ中没有master选举功能,
NameServer是无状态的,不负责整体的故障处理。 - 如果master挂了,如果当前架构是一主多从,就意味该
Broker无法接收Producer的消息了,但是消费者仍然能够继续消息(从节点拉消息)。如果当前架构是多主多从,就可以保障当其中一个节点挂了,另外一个master节点仍然能够对外提供消息发送服务。 Broker是数据存储节点,不是消息发送的最小单位,消息发送是以Topic为单位,而一个Topic的Queue不会都在一个Broker上,这就保证了,即使没有多主多从,只要不要全部Broker都挂掉,就能保证该Topic的消息写入。唯一的问题就是Broker挂掉,少了Queue,会触发rebalance,消费速度变慢。
数据存储
RokectMQ的几个特点:
- 磁盘存储:吞吐量大,性能相对差点
pagecache和mmap:提高磁盘读写性能commitlog数据文件:顺序写,提高写性能consumerqueue和index索引文件:快速搜索,提高读性能
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个字节,这就知道了消息体整体长度,再读一次数据即可拿到一个完整消息。
消息消费进度保存
如果是广播模式消费,消息的消费进度是保存到本地,如果是集群消费模式,消息的消费进度则是保存到 Broker(新加入的消费者只会从最新的消费进度进行消费),但无论是保存到本地,还是保存到 Broker,消费者都会在本地留一份缓存。
在集群模式下:
- 从节点会定时从主节点拉取消费进度
- 消费者消费完消息后,会告诉主节点或者从节点自己的消费进度
- 消费者自己存有消费进度,每次请求会带上这个消费进度,所以每次拉取的都是没消费的消息
消息的可靠投递
一个消息的生命周期,从生产者起,至消费者止。如何确保生产和消费是十分重要的一环,从生产者来看,就是如何确保消息给到了消费者;从消费者来看,就是怎么保证不重复消费消息:
RocketMQ凭什么这么快
- 零拷贝
- 顺序读写:写的时候顺序写,机械操作很少;读的时候预读,有缓存,对于MQ这种整体来看顺序消费的应用,缓存非常有用
Kafka
KafKa的设计跟RocketMQ很像,可以先看这篇文章:zhuanlan.zhihu.com/p/68052232
要注意Zookeeper管理
待更新
RocketMQ与Kafka的异同
共同点
-
一条消息会被订阅了同一个
Topic的ConsumerGroup都消费一次,但是只会由同一个ConsumerGroup钟的一个Consumer消费;一个Consumer可以消费多个Queue或者Partition;一个Queue或者Partition只能被一个Consumer消费。 -
Queue和Partition在不同Broker上都有副本,并且都有master/leader节点,从节点的数据都由主节点同步。
不同点
- 对于
commitlog/log文件,在一个Broker上RocketMQ的全部Topic都存到同一个文件中,而kafka不同Topic则分文件存储。
- RocketMQ这么设计主要是为了能够进行顺序写,提高IO效率,时延更低。但是
consumequeue文件还是分开存储的,以consumequeue文件的顺序和定长保证消息读取的效率,以及允许commitlog可无序且不定长存储。 - Kafka的分文件存储后,局部是顺序写,全局是随机写,时延会比RocketMQ要高,但是易扩展,整体的吞吐量会更大。 总结就是,RocketMQ更适合在线场景(订单系统),Kafka更适合离线场景(log收集)
- kafka为什么具备更高的吞吐量?
- 生产端会对消息进行Batch以及压缩
commitlog/log文件的设计区别- 多
Partition并发读写的模型
-
kafka的集群由Zookeeper管理,RocketMQ由NameServer管理。
-
RocketMQ支持tag过滤。
问题
- 如何提升消息对咧
Consumer消费能力,消费能力的提升比较复杂,需要具体情况具体分析
- 如果判断瓶颈是SDK导致的,那可以看看SDK是否有消费的优化
- 如果判断瓶颈是消费逻辑的耗时,那可以看看消费逻辑是否有优化的空间
- 如果确定两者都已经是没有优化了,那就需要看看
Queue的数量和消费实例的关系;如果消费实例过少,小于Queue的数量,可以扩容消费实例,或者增加消费端的worker数量。或者增加maxInflight的数量,如果是Queue数量过少导致的,可以进行扩容