RocketMQ
中间件
消息中间件(Message Queue,MQ)基于队列与消息传递技术,在网络环境中为应用系统提供同步或异步、可靠的消息传输的支撑性软件系统。
消息中间件:分布式系统中完成消息的发送和接收的基础工具。消息中间件也可以称消息队列,是指用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息队列模型,可以在分布式环境下扩展进程的通信。
RPC是远程过程调用(Remote Procedure Call)的缩写形式。SAP系统RPC调用的原理其实很简单,有一些类似于三层构架的C/S系统,第三方的客户程序通过接口调用SAP内部的标准或自定义函数,获得函数返回的数据进行处理后显示或打印。
消息中间件使用场景
应用解耦
通常写法是左图、用户下单后、订单系统需要分别调用别的系统、支付系统、物流系统和库存系统、假如物流系统无法访问,则订单出库将失败,从而导致订单失败。
处理方式:
引入消息队列机制、在用户下单后、订单系统完成持久化处理、将消息写入消息队列、直接返回用户下单成功、其他三系统分别从消息队列获取任务并执行。
流量削峰
在秒杀情况下可能会出现、每秒5k请求、但是mysql处理量有限、导致请求阻塞。
引如消息队列后、用户请求被服务器接收后存入消息队列、用户订单请求就结束了、A系统在每秒从队列中拉取200个请求进行持久化操作。就是一种以空间换时间的操作、也就是说如果我们的高峰时间为10S,那么我们对应的处理时间就是250S。
异步处理
用户注册后,需要发注册邮件和注册短信。
传统的做法有两种 :
- 串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。
- 并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间。
假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是100毫秒。因为CPU在单位时间内处理的请求数是一定的,假设CPU1秒内吞吐量是100次。则串行方式1秒内CPU可处理的请求量是7次(1000/150)。并行方式处理的请求量是10次(1000/100)。
如果使用消息队列,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因为写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍。
中间件各个角色
NameServer 角色
注册服务中心 维护Producer 和 Consumer 的配置信息、状态信息
Broker Cluster 角色
主要负责消息的存储、查询消费,支持主从部署
Producer 角色
消息的发送者,它负责产生消息,可以集群部署。它会先和 NameServer 集群中的随机一台建立长连接,得知当前要发送的 Topic 存在哪台 Broker Master上,然后再与其建立长连接,支持多种负载平衡模式发送消息。
Consumer 角色
消息的消费者,也可以集群部署。它也会先和 NameServer 集群中的随机一台建立长连接,得知当前要消费的 Topic 存在哪台 Broker Master、Slave上,然后它们建立长连接,支持集群消费和广播消费消息。
中间件中的基本概念
主题(Topic)
同一类消息的标识,例如某宝电商,衣服、手机、咖啡等都是一个主题,一般我们在生产或者消费时都会指定一个主题,要么生产这个主题的消息,要么订阅这个主题的消息。
分组(Group)
可以对生产者或者消费者分组,一般都针对于消费者的分组,分组是一个很有意义的事情,因为消费者都是对某一类消息的消费,消费的逻辑都是一致的,比如是一个订单主题的消息,我们可以有一个物流组来消费它,同时也可以定位一个通知组来消费这个消息。相互之间是隔离的,有可能会消费重复的消息。
消息队列(Message Queue)
是一个容器的概念,同一个主题有可能根据分组的不同,会产生不同的队列以供不同的消费组别进行消费。
标签(Tag)
更加细分的划分,在消费的时候我们可以根据 Tag 的不同来订阅不同标签消息,类似于 MySQL 查询中的条件。
偏移量(Offset)
message queue 是无限长的数组,一条消息进来下标就会涨1,下标就是 offset,消息在某个 MessageQueue 里的位置,通过 offset 的值可以定位到这条消息,或者指示 Consumer 从这条消息开始向后处理。
普通消息
同步发送
每一次发送消息 MQ都会进行响应。响应后再发送
异步发送
同样能得到响应,但是却是通过异步监听方式来获取的。所以在异步发送时,并不会阻塞当前线程。消息发送者不需要长时间等待消息的响应,是通过异步监听获取到响应结果。
单向发送
只管发送不管有没响应结果。
三种发送方式区别
发送方式 | 发送速度 | 发送结果反馈 | 可靠性 | 适用场景 |
---|---|---|---|---|
同步发送 | 快 | 有 | 不丢失 | 重要通知邮件、报名短信通知、营销短信系统等 |
异步发送 | 快 | 有 | 不丢失 | 异步发送一般用于链路耗时较长,对响应时间较为敏感的业务场景,例如用户视频上传后通知启动转码服务,转码完成后通知推送转码结果等。 |
单向发送 | 最快 | 无 | 可能丢失 | 适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。 |
消息生产
- 创建生产者对象producer、组名
- 指定 NameSrv 地址
- 开启生产者
- 创建消息对象指定Topic(主题)、标签(tag)、主体
- 发送消息
- 关闭生产者 producer
消息消费
- 创建消费者对象、指定组名(与生产者组名无关)
- 指定NameSrv地址
- 设置订阅主体
- 设置消费者方式(默认未集群消费)
- 设置回调函数处理消息
- 开启消费者
集群消费与广播消费
集群消费
每条消息只处理一次、多个消费者将消息分摊消费。
注意事项
- 集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。
- 集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上。
广播消费
消息被集群下所有消费者都处理一次
注意事项
-
广播消费模式下不支持顺序消息。
-
广播消费模式下不支持重置消费位点。
-
广播模式下不支持线下联调分组消息。
-
每条消息都需要被相同订阅逻辑的多台机器处理。
-
消费进度在客户端维护,出现重复消费的概率稍大于集群模式。
-
广播模式下,消息队列保证每条消息至少被每台客户端消费一次,但是并不会重投消费失败的消息,因此业务方需要关注消费失败的情况。
-
广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。
-
广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。
-
广播模式下服务端不维护消费进度,所以消息队列控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。
顺序消息
顺序消息(FIFO 消息)是消息队列提供的一种严格按照顺序来发布和消费的消息类型。可以被分为:
- 全局顺序消息
- 部分顺序消息
顺序消费的原理解析
- 默认情况下、消息发送会采取轮询方式发送到不同的queue (分区队列)。而消费消息的时候从多个不同的queue上获取消息、就会导致消费不是有序的。
- 如果控制发送的顺序消息只发送在一个queue上、消费时只从这个queue上依次拉取消息进行消费、就保证了有序。
- 当发送和消费参与的queue只有一个、则是全局有序;如果有多个quque参与则是局部有序。即分区有序、每个队列消息有序。
RocketMQ 的 ACK(Acknowledgement) 机制
ACK,即一种消息确认字符,在数据通信中,消息接受站给消息发送站的一种传输类控制符,表示传输过来的数据已经接受无误。
PushConsumer 为了保证消息肯定消费成功,只有使用方明确表示消费成功,RocketMQ 才会认为消息消费成功。中途断电,抛出异常等都不会认为成功——即都会重新投递。
业务实现消费回调的时候,当且仅当此回调函数返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS,RocketMQ 才会认为这批消息(默认是1条)是消费完成的。
如果这时候消息消费失败,例如数据库异常,余额不足扣款失败等一切业务认为消息需要重试的场景,只要返回 ConsumeConcurrentlyStatus.RECONSUME_LATER ,RocketMQ 就会认为这批消息消费失败了。
为了保证消息是肯定被至少消费成功一次,RocketMQ 会把这批消息重发回 Broker,在延迟的某个时间点(默认是10秒,业务可设置)后,再次投递到这个 ConsumerGroup。而如果一直这样重复消费都持续失败到一定次数(默认16次),就会投递到DLQ死信队列。
urrentlyStatus.RECONSUME_LATER ,RocketMQ 就会认为这批消息消费失败了。
为了保证消息是肯定被至少消费成功一次,RocketMQ 会把这批消息重发回 Broker,在延迟的某个时间点(默认是10秒,业务可设置)后,再次投递到这个 ConsumerGroup。而如果一直这样重复消费都持续失败到一定次数(默认16次),就会投递到DLQ死信队列。
事务消息
在一些对数据一致性有强需求的场景、使用事务消息来解决、从而保证上下游数据一致性。
以电商交易场景为例,用户支付订单这一核心操作的同时会涉及到下游物流发货、积分变更、购物车状态清空等多个子系统的变更。当前业务的处理分支包括:
- 主分支订单系统状态更新:由未支付变更为支付成功。
- 物流系统状态新增:新增待发货物流记录,创建订单物流记录。
- 积分系统状态变更:变更用户积分,更新用户积分表。
- 购物车系统状态变更:清空购物车,更新用户购物车记录。
使用普通消息和订单事务无法保证一致的原因,本质上是由于普通消息无法像单机数据库事务一样,具备提交、回滚和统一协调的能力。 而基于 RocketMQ 的分布式事务消息功能,在普通消息基础上,支持二阶段的提交能力。将二阶段提交和本地事务绑定,实现全局提交结果的一致性。
事务消息的发送分为两个阶段、第一阶段发送一个半事务消息、半事务消息指的是暂不能投递的消息。生产者已经成功将消息发送到了broker、但是Broker 未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,如果发送成功则执行本地事务,并根据本地事务执行成功与否,向 Broker 半事务消息状态(commit或者rollback),半事务消息只有 commit 状态才会真正向下游投递。如果由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,Broker 端会通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback)。这样最终保证了本地事务执行成功,下游就能收到消息,本地事务执行失败,下游就收不到消息。总而保证了上下游数据的一致性。
RocketMQ 的高可用机制
集群部署模式
- 单 master 模式
- 多 master 模式
- 多 master 多 slave 模式(同步)
- 多 master 多 slave 模式(异步)
RocketMQ分布式集群是通过Master、和Slave的配合达到高可用的。
Master 和 Slave 的区别:
在 Broker 的配置文件中,参数 brokerId 的值为0表明这个 Broker 是 Master,大于0表明这个 Broker 是 Slave。
Master 角色的 Broker 支持读和写,Slave 角色的Broker仅支持读,也就是 Producer 只能和 Master 角色的 Broker 连接写入消息;Consumer 可以连接 Master 角色的 Broker,也可以连接 Slave 角色的 Broker 来读取消息。
消费消息高可用
在 Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave 读,当 Master 不可用或者繁忙的时候,Consumer 会被自动切换到从 Slave 读。有了自动切换 Consumer 这种机制,当一个 Master 角色的机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序。这就达到了消费端的高可用性。
消息发送高可用
在创建Topic的时候、把Topic的多个Message Queue创建在多个Broker组上、这样当一个Broker组的Master不可用之后、其他组的Master仍然可用、Producer 仍然可以发送消息。
刷盘与主从同步
- 同步刷盘与异步刷盘
- 同步复制与异步复制
同步刷盘与异步刷盘
RocketMQ 的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。RocketMQ 为了提高性能,会尽可能地保证磁盘的顺序写。
异步刷盘方式:
在返回写成功状态时,消息可能只是被写入了内存的 PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入。
即消息只是写在了缓冲区、然后给生产者返回成功信息、当缓冲区消息达到一定量时再写入磁盘;
优点:性能高
缺点:Master宕机,磁盘损坏的情况下,会丢失少量的消息, 导致MQ的消息状态和生产者/消费者的消息状态不一致
同步刷盘方式
在返回应用写成功状态前,消息已经被写入磁盘。具体流程是,消息写入内存的 PAGECACHE 后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,给应用返回消息写成功的状态。
即消息写入缓冲区后、立即执行刷盘、等待消息写入磁盘。刷盘成功后才向生产者返回成功消息。
优点:可以保持MQ的消息状态和生产者/消费者的消息状态一致
缺点:性能比异步的低
同步复制和异步复制
同步复制
Master和Slave都写成功后返回给客户端写成功状态。
同步复制方式下、如果Master发生故障、 Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量。
异步复制
只要 Master 写成功 即可反馈给客户端写成功状态,在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果 Master 出了故障,有些数据因为没有被写入 Slave,有可能会丢失。
RocketMQ 存储结构
CommitLog 文件
commitLog 是存储消息的主体。Producer 发送的消息都会顺序写入 commitLog 文件,所以随着写入的消息增多,文件也会随之变大。单个文件大小默认1G, 文件名长度为20位,左边补零,剩余为起始偏移量,比如 00000000000000000000 代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。
commitLog 以物理文件的方式存放,每台 Broker 上的 commitLog 被本机器所有 consumeQueue 共享。在 commitLog,一个消息的存储长度是不固定的,RocketMQ 采用了一些机制,尽量向 commitLog 中顺序写,但是随即读。
ConsumeQueue 文件
ConsumeQueue (逻辑消费队列)可以看成基于 topic 的 commitLog 的索引文件。因为 CommitLog 是按照顺序写入的,不同的 topic 消息都会混淆在一起,而 Consumer 又是按照 topic 来消费消息的,这样的话势必会去遍历 commitLog 文件来过滤 topic,这样性能肯定会非常差,所以 RocketMQ 采用 ConsumeQueue 来提高消费性能。即每个 Topic 下的每个 queueId 对应一个 ConsumeQueue,其中存储了单条消息对应在 commitLog 文件中的物理偏移量 offset,消息大小 size,消息 Tag 的 hash 值。
IndexFile 文件
因为所有的消息都存在 CommitLog 中,如果要实现根据 key 查询 消息的方法,就会变得非常困难,所以为了解决这种业务需求,有了 IndexFile 的存在。用于为生成的索引文件提供访问服务,通过消息 Key 值查询消息真正的实体内容。在实际的物理存储上,文件名则是以创建时的时间戳命名的,固定的单个 IndexFile 文件大小约为400M,一个 IndexFile 可以保存 2000W个索引。
为什么要顺序写,随机读?
[ 磁盘存储的“快”——顺序写 ]
磁盘存储,使用得当,磁盘的速度完全可以匹配上网络的数据传输速度,目前的高性能磁盘,顺序写速度可以达到600MB/s,超过了一般网卡的传输速度。
[ 磁盘存储的“慢”——随机写 ]
磁盘的随机写的速度只有100KB/s,和顺序写的性能差了好几个数量级。
[ 存储机制这样设计的好处——顺序写,随机读 ]
CommitLog 顺序写,可以大大提高写入的效率;虽然是随机读,但是利用 package 机制,可以批量地从磁盘读取,作为 cache 存到内存中,加速后续的读取速度。
为了保证完全的顺序写,需要 ConsumeQueue 这个中间结构,因为 ConsumeQueue 里只存储偏移量信息,所以尺寸是有限的。
在实际情况中,大部分 ConsumeQueue 能够被全部读入内存,所以这个中间结构的操作速度很快,可以认为是内存读取的速度。