MVCCRocketMQ的前世今生
RocketMQ的前世今生
rocketMq在阿里内容叫做Metaq(最早名为Metamorphosis,中文意思“变形记”,是作家卡夫卡的中篇小说代表作,可见是为了致敬Kafka)。
RocketMQ是Metaq3.0之后的开源版本。
Metaq在阿里巴巴集团内部、蚂蚁金服、菜鸟等各个业务中被广泛使用,介入了上万个应用系统中,并平稳支撑了历年的双十一大促(万亿级的消息),在性能、稳定性、可靠性等方面表现出色,在整个
阿里技术体系和大中台战略中发挥着举足轻重的作用。
Metaq最早源于Kafka,早期借鉴了Kafka很多优秀的设计。但是由于Kafka是Scale语言编写而阿里系主要使用Java,且无法满足阿里的电商、金融业务场景,所以誓嘉(花名)团队用Java重新造轮子,
并做了大量的改造和优化。
在此之前,淘宝有一款消息中间件名为Notify,目前已经逐步被Metaq所取代。
第一代的Notify主要使用了推模型,解决了事务消息;第二代的MetaQ主要使用了拉模型,解决了顺序消息和海量堆积的问题。相比起Kafka使用的Scale语言编写,RabbitMQ 使用Erlang语言编写,基
于Java的RocketMQ开源后更容易被广泛的研究,以及其他大厂定制开发。
RocketMQ的使用场景
应用解耦
系统的耦合性越高,容错性就越低。以用户下单为例,创建订单后,如果耦合调用了库存、物流、支付等,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。
流量削峰
应用系统如果遇到系统请求量的瞬间猛增,有可能会将系统压垮。有了消息队列可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提高系统的稳定性和用户体验
数据分发
通过消息队列可以让数据在多个系统之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可。
RocketMQ 部署架构
RocketMQ的角色介绍
producer:消息的发送者 发信人
consumer:消息消费者 收件者
Broker:暂存和传输消息 邮局
NameServer:管理broker 各个邮局管理机构
Topic: 区分消息的种类;一个发送者可以发送消息给一个或多个Topic,一个消息的接收者可以订阅一个或者多个topic消息
Message Queue:相当于是topic的分区;用于并行发送和接收消息
NameServer是一个几乎无状态节点,可集群部署,节点之间无任何信息同步
Broker部署相对复杂,broker分为master和slave,一个master可以对应多个slave,但是一个slave只能对应一个master,master与slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义0表示master。Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer。 注意:当前RocketMQ版本在部署架构上支持一Master多Slave,但只有BrokerId=1的从服务器才会参与消息的读负载。
Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。
Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取。
执行流程:
- 启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上
来,相当于一个路由控制中心。 - Broker启动,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含当前
Broker信息(IP+端口等)以及存储所有Topic信息。注册成功后,NameServer集群中就有Topic
跟Broker的映射关系。 - 收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在
发送消息时自动创建Topic。 - Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从
NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,
然后与队列所在的Broker建立长连接从而向Broker发消息。 - Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在
哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息
RocketMQ特性
1,订阅与发布
消息的发布是指某个生产者向某个topic发送消息;消息的订阅是指某个消费者关注了某个topic中带有tag的消息
2,消息顺序
消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了三条消息分别时创建订单、订单付款、订单完成。消费时要按照这个顺序才能有意义,但是同时订单之间是可以并行消费的。rocketmq可以严格的保证消息有序。
3,消息过滤
RocketMQ的消费者可以根据tag进行消息过滤,也支持自定义属性过滤。消息过滤目前是在broker端实现的,优点是减少了Consumer无用消息的网络传输,缺点是增加了Broker的负担,而且实现复杂。
4,消息可靠性
RocketMQ支持消息的高可靠,影响消息可靠性的集中情况:1)Broker非正常关闭 2)Broker异常Crash 3)OS Crash 4)机器掉电,但是能立即恢复供电情况 5)机器无法开机(可能是cpu、主板、内存等关键设备损坏) 6)磁盘设备损坏1)、2)、3)、4) 四种情况都属于硬件资源可立即恢复情况,RocketMQ在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式是同步还是异步)。5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ在这两种情况下,通过异步复制,可保证99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与Money相关的应用。注:RocketMQ从3.0版本开始支持同步双写。
5,至少一次
至少一次(At least Once)指每个消息必须投递一次。Consumer先Pull消息到本地,消费完成后,才向服务器返回ack,如果没有消费一定不会ack消息,所以RocketMQ可以很好的支持此特性。
6,回溯消费
回溯消费是指Consumer已经消费成功的消息,由于业务上需求需要重新消费,要支持此功能,Broker在向Consumer投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于Consumer系统故障,恢复后需要重新消费1小时前的数据,那么Broker要提供一种机制,可以按照时间维度来回退消费进度。RocketMQ支持按照时间回溯消费,时间维度精确到毫秒。
7,事务消息
RocketMQ事务消息(Transactional Message)是指本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。
RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,通过事务消息能达到分布式事务的最终一致性。
8,定时消息
定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。
broker有配置项messageDelayLevel,默认值为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,18个level
messageDelayLevel是broker的属性,不属于某个topic。发消息时,设置delayLevel等级即可:
msg.setDelayLevel(level)。level有以下三种情况:
level == 0,消息为非延迟消息1<=level<=maxLevel,消息延迟特定时间,例如level1,延迟1s level > maxLevel,则level maxLevel,例如level==20,延迟2h定时消息会暂存在SCHEDULE_TOPIC_XXXX的topic中,并根据delayTimeLevel存入特定的queue,queueId = delayTimeLevel – 1,即一个queue只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。broker会调度地消息SCHEDULE_TOPIC_XXXX,将消息写入真实的topic。需要注意的是,定时消息会在第一次写入和调度写入真实topic时都会计数,因此发送数量、tps都会变高。
9,消息重试
Consumer消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer消费消息失败通常可以人为有以下几种情况。
1)由于消息本身的原因,例如反序话失败,消息数据本身无法处理
2)由于依赖的下游应用服务不可用,例如db连接不可用,外系统不可达等。遇到这种错误,即使跳过当前失败的消息,消费其他消息也会报错。这种情况建议应用sleep30s,消费下一条消息,这样可以减轻broker重试消息的压力。
10,消息重投
生产者在发送消息时:
- 同步消息失败会重投
- 异步消息有重试
- oneway没有任何保证
消息重投保证消息尽可能发送成功、不丢失,但可能会造成假消息,消息重复在RocketMQ中时无法避免的问题。消息 重复在一般情况下不会发生,当出现消息量大,网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、consumer负载变化也会导致重复消息。
如下方法可以设置消息重试策略:
1)retryTimesWhenSendFailed:同步发送失败重投次数,默认为2,因此生产者会最多尝试发送retryTimesWhenSendFailed+1次。不会选择上次失败的broker,尝试向其他broker发送,最大程度保证消息不丢失。超过重试次数,抛异常,由客户端保证消息不丢失。当出现remotingException、MQClientException和部分MQBrokerException会重投。
3)retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false。十分重要消息可以开启。
11 流量控制
生产者流控,因为broker处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈。
- 生产者流控:
commitLog文件被锁时间超过osPageCacheBusyTimeOutMills时,参数默认为1000ms,发生流控。
如果开启transientStorePoolEnable = true,且broker为异步刷盘的主机,且transientStorePool中资源不足,拒绝当前send请求,发生流控。broker每隔10ms检查send请求队列头部请求的等待时间,如果超过waitTimeMillsInSendQueue,默认200ms,拒绝当前send请求,发生流控。broker通过拒绝send 请求方式实现流量控制。注意,生产者流控,不会尝试消息重投。 - 消费者流控:
消费者本地缓存消息数超过pullThresholdForQueue时,默认1000。
消费者本地缓存消息大小超过pullThresholdSizeForQueue时,默认100MB
消费者本地缓存消息跨度超过consumeConcurrentlyMaxSpan时,默认2000。
消费者流控的结果是降低拉取频率。
12死信队列
死信队列用于处理无法被正常消费的消息。
当一条消息初次消费失败,消息队列会自动进行消息重试;
达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到消费者对应的特殊队列中。
RocketMQ将这种正常情况下无法被消费的消息称为死信消息,将存储死信消息的特殊队列称为死信队列,在rocketMQ中,可以通过使用console控制台对死信队列中的消息进行重发来使得消费者实例再次消费。
消费模式push or pull
rocketmq消息订阅由两种模式,一种是push模式(MQPushConsumer),即MQServer主动向消费端推送;另外一种是pull模式(MQPullConsumer),即消费端在需要时,主动到MQ Server拉取。但在具体实现时,push和pull模式本质都是采用消费端主动拉取的方式,即consumer轮询从broker拉取消息。
push模式特点:
好处就是实时性高。不好处就是消费端处理能力有限,当瞬间推送很多消息给消费端时,容易造成消费端的消息堆压,严重时压垮客户端。
pull模式:
好处就是主动权掌握在消费端自己手中,根据自己的处理能力量力而行。缺点就是如何控制pull的频率。定时间太久担心影响时效性,间隔时间太短做太多无用功浪费资源。比较折中的办法就是长轮询。
push模式和pull模式的区别:
push方式,consumer把长轮询的动作封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息时被推送过来的。
pull方式里,取消息的过程需要用户自己主动调用,首先通过打算消费的topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量拉取消息,一次取完后,记录该队列下一次要取得开始offset,直到取完了,再换另一个messageQueue。
rocketMq使用长轮询机制来模拟push效果,算是兼顾了二者得优点。
RocketMQ中角色及相关术语
1)消费模型(Message Model)
由producer、Broker、Consumer三部分组成,其中Producer生产消息,Consumer消费消息。Broker存储消息。Broker在实际部署过程中对应一台服务器,每个Broker可以存储多个topic的消息,每个topic的消息也可以分片存储于不同的Broker。Message Queue用于存储消息的物理地址,每个topic中的消息地址存储在多个message queue中。consumerGroup由多个consumer实例构成。
14)Message queue
在rocketMQ中,所有消息队列都是持久化的,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用Offset来访问,offset为java long类型,64位理论上在 100 年内不会溢出,所以认为为是长度无限,另外队列中只保存最近几天的数据,之前的数据
会按照过期时间来删除。也可以认为Message Queue是一个长度无限的数组,offset 就是下标。
15)标签
为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
RocketMQ事务消息
依赖于TransactionListener接口
- executeLocalTransaction方法会在发送消息后调用,用于执行本地事务,如果本地事务执行成功,rocketmq再提交消息。
- checkLocalTransaction用于对本地事务做检查,rocketmq依赖此方法做补偿。
通过两个内部的topic来实现对消息的两阶段支持。
prepare:将消息(消息上带有事务标识)投递到一个名为RMS_SYS_TRANS_HALF_TOPIC的topic中,而不是投递到真正的topic中。
commit/rollback:producer再通过TransactionListener的executeLocalTransaction方法执行本地事务,当producer的localTransaction处理成功或者失败后,producer会向broker发送commit或rollback命令 ,如果是commit,则broker会将投递到RMQ_SYS_TRANS_HALF_TOPIC中消息投递到真实的topic中,然后再投递一个表示删除的消息RMQ_SYS_TRANS_OP_HALF_TOPIC中,表示当前事务已完成。
如果是rollback,则没有投递到真实topic的过程,只需要投递表示删除的消息到RMQ_SYS_TRANS_OP_HALF_TOPIC中。最后,消费者和消费普通额消息一样消费事务消息。
- 第一阶段(prepare)失败:给应用返回发送消息失败
- 事务失败:发送回滚命令给broker,由broker执行消息的回滚
- commit或rollback失败:由broker定时向producer发起事务检查,如果本地事务成功,则提交消息事务,否则回滚消息事务。
事务状态的检查有两种情况:
commit/rollback:broker会执行相应的commit/rollback操作
如果是TRANSACTION_NOT_TYPE,则一段时间后会再次检查,当检查的次数超过上限(默认15)则丢弃消息。
RocketMQ顺序消息
默认是不能保证的,需要程序保证消息和消费的是同一个queue,多线程也无法保证
发送顺序:发送端自己业务逻辑保证先后,发往一个固定的queue,生产者可以在消息体上设置消息的顺序。
简述RocketMQ持久化机制
- commitLig:日志数据文件,被所有的queue共享,大小为1G,写满之后重新生成,顺序写。
- consumeQueue:逻辑queue,消息先到达commitLog、然后异步转发consumeQueue,包含queue在CommitLog中的物理位置偏移量Offset,消息实体内容的大小和MessageTag的hash值。大小大约600W个字节,写满之后重新生成,顺序写
- indexFile:通过key或者时间区间来查找CommitLog中的消息,文件名以创建的时间戳命名,固定的单个IndexFile大小为400M,可以保存2000W个索引。
所有队列共用一个日志数据文件,避免了kafka的分区数过多、日志文件过多导致磁盘IO读写压力较大造成性能瓶颈,rocketmq的queue只存储少量数据、更加轻量化,对于磁盘的访问是串行化避免磁盘竞争,缺点在于:写入是顺序写,但读是随机的,先读ConsumeQueue,在读Commitlog,会降低消费读的效率
消息发送到broker后,会被写入commitlog,写之前加锁,保证顺序写入。然后转发到ConsumeQueue
消息消费时先从consumeQueue读取消息在CommitLog的起始物理偏移量Offset,消息大小、和消息Tag的HashCode值。在从CommitLog读取消息内容
- 同步刷盘:消息持久化到磁盘才会给生产者返回ack,可以保证消息可靠,但是会影响性能。
- 异步刷盘:消息写入pageCache就返回ack给生产者,刷盘采用异步线程,降低读写延迟提供性能和吞吐
RocketMQ如何保证不丢消息
生产者:
- 同步阻塞的方式发送消息,加上失败重试机制,可能broker存储失败,可以通过查询确认。
- 异步发送需要重写回调方法,检查发送结果。
- ack机制,可能存储CommitLog,存储ConsumerQueue失败,此时对消费者不可见
broker:
同步刷盘,集群模式下采用同步复制,会等待slave复制完成才会返回确认。
消费者
offset手动提交,消息 消费保证幂等性。
提升写入的性能
发送一条消息出去要经过三步
1,客户端发送请求到服务器
2,服务器处理该请求
3,服务器向客户端返回应答
一次消息的发送耗时时是上述三个步骤的总和。
在一些对速度要求高,但是可靠性要求不高的场景下,比如日志收集类应用,可以采用Oneway方式发送。
Oneway方式发送请求不等待应答,即将数据写入客户端的socket缓存区就返回,不等待对方返回结果。
用这种方式发送消息的耗时可以缩短到微秒级。
另一种提高发送速度的方法是增加Producer的并发量,使用多个producer同时发送。
我们不用担心多Producer同时写会降低消息写磁盘的效率,RocketMQ引入了一个并发窗口,在窗口内消息可以并发地写入DireactMem中,然后异步地将连续一段无空洞的数据刷入文件系统当中。
顺序写CommitLog可以让RocketMQ无论在HDD还是SSD磁盘情况下都能保持较高的写入性能。
目前在阿里内部经过调优的服务器上,写入性能达到90万+的TPS,我们可以参考这个数据进行系统优化。
消息消费
简单总结消费的几个要点:
1,消息消费方式(Pull和Push)
2,消息消费的模式(广播模式和集群模式)
3,流量控制(可以结合sentinel来实现)
4,并发线程数设置
5,消息的过滤(Tag、Key)TagA||TagB||TagC*null
当Consumer的处理速度跟不上消息的产生速度,会造成越来越多的消息积压,这个时候首先查看消费逻辑本身有没有优化空间,除此之外还有三种方法可以提高Consumer的处理能力。
1,提供消费并行度
再同一个ConsumerGroup下(Clustering方式),可以通过增加Consumer实例的数量来提高并行度。
通过加机器,或者在已有的机器中启动多个Consumer进程都可以增加Consumer实例数。
注意:总的Consumer数量不要超过Topic 下Read Queue数量,超过的Consumer实例接收不到消息。
此外,通过提高单个Consumer实例中的并行处理的线程数,可以在同一个Consumer内增加并行度来提高吞吐量(设置方法是修改consumeThreadLMin和consumeThreadMax)
2,以批量方式进行消费
某些业务场景下,多条消息同时处理的时间会大大小于逐个处理的时间总和,比如消费消息中涉及update某个数据库,一次update10条的时间会大大小于10次update1条数据的时间。
可以通过批量方式消费来提高消费的吞吐量。实现方法是设置Consumer的consumeMessageBatchMaxSize这个参数,默认是1,如果设置为N,在消息多的时候每次收到的是个长度为N的消息链表。
3,检测延时情况,跳过非重要消息
Consumer在消费的过程中,如果发现由于某种原因发生严重的消息堆积,短时间无法消除堆积,这个时候可以选择丢弃不重要的消息,使Consumer尽快追上Producer的进度。
消息存储
关系型数据库DB
Apache下开源的另外一款MQ-ActiveMQ
文件系统
目前业界较为常用的几款产品(RocketMQ/Kafka/rabbitMQ)均采用的使消息刷盘至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式。除非部署MQ机器本身或是本地磁盘挂了,否则一般使不会出现无法持久化的故障问题。
RocketMq为什么高性能
1,消息存储
目前的高性能磁盘,顺序写速度可以达到600MB/s,超过了一般网卡的传输速度。
但是磁盘随机写的速度只有大概100KB/s,和顺序写的性能相差6000倍。
RocketMQ的消息用顺序写,保证了消息存储的速度。
2,存储结构
RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个MessageQueue都有一个对应的ConsumeQueue文件。
消息存储架构图中主要有下面三个跟消息存储相关的文件构成。
1,CommitLog:消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容是不定长的。单个文件默认1G,文件名长度为20位,左边补零,剩余起始偏移量.比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件;
2,ConsumeQueue:
消息的偏移量写入磁盘文件,
消息消费队列,引入的目的主要是提高消息消费的性能,rocketMQ是基于主体topic的订阅模式,消息消费是针对主题进行,如果要遍历commitLog文件根据topic检索消息是非常低效的。
Consumer即可根据ConsumeQueue(逻辑消费队列)作为消费信息的索引:
1,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset
2,消息大小size
3,消息Tag的HashCode值
consumequeue文件可以看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织方式如下:
topic/queue/file三层组织结构
具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。