消息队列重点问题

参考资料:

从 0 开始带你成为消息中间件实战高手

中华石杉互联网 Java 工程师面试突击(第一季)

1627701119157-5747a2a2-bb69-4b40-ac32-9929ea463dcb.png

重点

  • 一定要在自己的核心链路里做文章,有没有可能一个关键的步骤会失败?如果这个关键步骤失败了,这个时候会怎么样?如果某个步骤没有成功,是不是需要启动后台线程定时扫描进行补偿?
  • 所谓的核心链路,不是说查询链路,即并不是一次请求全部是查询。而是说的是数据更新链路,即一次请求过后会对你的各种核心数据进行更新,同时还会调用其他服务或者系统进行数据更新或者查询,这样的一个链路叫做系统的核心链路。针对这样的系统核心数据链路,你考虑一下有没有哪些环节拖累了性能?你能否通过在系统里打印日志的方式,排查出来核心数据链路中的每个环节的耗时是多长?哪些环节是最耗时的?有没有可能引入MQ技术把一些耗时的步骤做成异步化的方式,来优化核心数据链路的性能?如果可以的话,你应该如何设计这个技术方案?哪些环节同步执行?哪些环节要异步执行?
  • 主动思考能力,随机应变的本事。

如果让你写一个消息队列,该如何进行架构设计?说一下你的思路。

从整体了解把握住一个消息队列的架构原理,给出一些关键点出来。技术的基本原理、核心组成部分、基本架构构成,然后参照一些开源的技术把一个系统设计出来的思路说一下就好。

  • 首先是可伸缩性,负载均衡算法:参考rocketMQ而言就是设计一个逻辑上的topic,之后让物理上的多台broker上的message queue都属于这个topic,broker 扩容怎么办?给topic增加message queue的个数,这样不会导致有的机器分不到queue
  • 其次是网络通信,以及分布式注册中心。心跳机制,通信框架。协议封装。
  • 其次是数据持久化,防止消息丢失,副本机制以及落盘机制,可以不落盘,但是要有副本机制。采用顺序写commitlog,还要建立索引indexfile方便查找。
  • 中间件的可用性,参考rocket的Delger,采用主从节点,raft协议选举一下master节点,
  • 数据的丢失方案,事务消息,同步落盘才可以返回,消费方不会自动提交需要手动提交。以及故障自动转移机制,
  • 附加一些高级功能特性,定时任务对应的延时队列,暂时消费失败返回CONSUME_Later的重试队列,重试一直失败之后加入到的死信队列。

熟悉的一个mq一直问到源码级别非常底层。结合项目来仔细问,详细说说你的业务细节,然后将你的业务跟这些mq的问题场景结合起来,看看你每个细节是怎么处理的。

分布式理论

  • 分片:每一个topic都会有多个message queue ,这个queue分布在不同的broker,分管不同的数据,实现了数据的分片。(使用负载均衡实现分片的访问)
  • 副本:每一个broker自己又有master 和slave实现了副本机制。

在项目里是怎么用消息队列的?

部分同学在这里会进入一个误区,就是你仅仅就是知道以及回答你们是怎么用这个消息队列的,用这个消息队列来干了个什么事情?

系统问题

发布的时候要通知自己的粉丝。

如果是很多用户都在线对话,直接使用netty同步处理会耗费大量资源以及阻塞

评论点赞关注都需要通知被操作者,这些都可以异步处理,同步操作只需要保证操作者数据落库就返回

删除帖子之后ES也需要更新,这时候同步删除耗时较长,

解决方式

用户每次发布新的帖子,都会使用异步的方式通知关注他的人有新的帖子,并且将这个帖子的对应的维度通知持久化到数据库,消息通知使用异步的方式实现防止用户发布帖子时候等待时间过长的问题。

以及点赞的通知,评论的通知都是通过消息队列异步实现的,只要保证消息安全到达消息队列就可以返回了。

删除的时候数据库直接删除,ES的在mq里面消费删除

为什么使用消息队列啊?系统不发送消息到 MQ,直接就调用不就行了

以发布文章为例,不用MQ的话不仅仅需要新增文章的数据,还要将通知消息发给关注自己的人,多个与数据库交互的逻辑使得用户得到响应的时间间隔增加。

使用消息队列可以使得用户快速得到响应,不需要把所有业务流程执行完成才返回,而是只把核心数据保存之后就可以返回,这也是异步带来的好处。

服务器资源预估方法

系统的整体架构以及访问压力:服务器4核8G的机器一般每秒钟抗几百请求都没问题,现在才每秒两三百请求,CPU资源使用率都不超过50%。数据库服务器因为用的是16核32G的配置,每秒四五千的请求。在看系统的访问压力的时候,是不能直接按平均值来计算的。

MQ技术适用的一些业务场景

消息中间件,其实就是一种系统,他自己也是独立部署的,然后让我们的两个系统之间通过发 消息和收消息,来进行异步的调用,而不是仅仅局限于同步调用。

常见应用

  • 数据库访问压力剧增,读写性能会进一步下降,经常出现请求过慢,请求超时等问题。
  • 下单和发券推送同步导致性能差
  • 退款是下单的逆序--第三方退款失败
  • 用户自己下单不支付
  • 第三方物流系统的耦合性
  • 自己系统的数据其他团队要获取,会执行大SQL
  • 秒杀活动时数据库压力太大,该怎么缓解?

用消息队列都有什么优点和缺点?

优点:

  • 首先是解耦,可以把原来同步调用的接口变为异步调用,不需要考虑是否调用成功以及失败超时等问题。自己的系统只要保证消息安全到消息队列即可,也增加了系统的可维护性,方便降级。
  • 其次是异步,用户得到响应的时间大大减少,直接省略了其他系统执行所需要的时间,只需要计算消息发给MQ耗费的时间,也提高了用户的使用体验
  • xuefeng削峰填谷,短暂的高峰期积压可以使用MQ来解决防止系统无法处理突然的高并发请求量,也提高了系统的稳定性。使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。

异步:用户友好性

解耦:可用性,可维护性

削峰填谷:稳定性

缺点:

  • 需要维护的组件增加,可用性降低,MQ挂掉系统就会崩溃(补救就是发现挂了写到本地,恢复之后再将数据写回到数据库里面)
  • 系统复杂度升高
  • 以及互相调用的系统之间的消息一致性问题,如何保证消费端一定会消费成功。引入一系列和消息相关的问题,重复,丢失,乱序、积压

做过调研?KafkaActiveMQRabbitMQRocketMQ 都有什么区别?

吞吐量方面,active和rabbit是万级别的,而kafka和rocket是十万级别的;topic多的情况下,kafka吞吐量收到的影响会比较大,需要增加机器,而rocketMQ则不只会对吞吐量有较小的影响;延迟方面,rabbit 是微秒级别的,其他的都是毫秒级别的;消息丢失方面,kafka和rocketMQ结果参数调整可以实现消息不丢失,但是性能会下降较多。语言方面,rabbitMQ是erlang实现的,可维护性比较低;功能上,Kafka只是简单的收发消息,重点在于大吞吐量集群高可用,所以适合大数据日志收集等不需要太多高级功能的场景,而rocketMQ在吞吐量大的同时还是提供了很多的高级功能,比如事务消息延迟消息等等。

为了使用高级功能,也为了可以学习源码二次开发,所以使用了rocketMQ。

如何保证消息队列的高可用啊?

RocketMQ的高可用

topic是逻辑概念,topic下会有多个message queue,会实际分配到不同的master broker,这些master broker都有自己对应的slave broker,也就是Broker主从架构以及多副本策略。Master Broker收到消息之后会同步给Slave Broker,如果任何一个Master Broker出现故障,还有一个Slave Broker上有一份数据副本,可以保证数据不丢失,还能继续对外提供 服务,保证了MQ的可靠性和高可用性

dledger集群模式使用 raft 机制选举master节点继续提供服务。

RabbitMQ 的高可用性

RabbmitMQ 之类的,并不是分布式消息队列,它就是传统的消息队列,只不过提供了一些集群、HA(High Availability, 高可用性) 的机制而已,因为无论怎么玩儿,RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。

  • 单机模式,没人生产用单机模式。
  • 普通集群模式(无高可用性),只复制队列的元数据到其他机器,相当于路由,只是不同的队列放在了不同的机器,提高了吞吐量。
  • 镜像集群模式(高可用性):完全复制队列的数据到其他机器,第一,这个性能开销太大,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!第二,不是分布式的,就没有扩展性可言了,如果这个 queue 的数据量很大,大到这个机器上的容量无法容纳了,此时该怎么办呢?

Kafka 的高可用性

Kafka 由多个 broker 组成,每个 broker 是一个节点你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。是天然的分布式消息队列,就是说一个 topic 的数据,是分散partition 放在多个机器上的,每个机器就放一部分数据

1620615352344-201b59a0-ba62-4a0b-91d6-b4205ba29b0c.png

Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)

消费的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。

1620615714563-0e875df2-9fc9-464b-9bac-5f55b3d90ff4.png

如果某个 broker 宕机了,那个 broker上面的 partition 在其他机器上都有副本的。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来。

生产集群的部署与压测

设计一套高可用的消息中间件生产部署架构

保证整个系统运行过程中任何一个环节宕机都不能影响系统的整体运行.

  • NameServer集群化部署,保证高可用性:部署在三台机器上,这样可以充分保证NameServer作为路由中心的可用 性,哪怕是挂掉两台机器,只要有一个NameServer还在运行,就能保证MQ系统的稳定性。里面任何一台 机器都是独立运行的,跟其他的机器没有任何通信。
    • 每台NameServer实际上都会有完整的集群路由信息,包括所有的Broker节点信息,我们的数据信息,等等。所以只要任何一台 NameServer存活下来,就可以保证MQ系统正常运行,不会出现故障。
  • 基于Dledger的Broker主从架构部署:采用RocketMQ 4.5以前的那种普通的Master-Slave架构来部署,能在一定程度上保证数据不丢 失,也能保证一定的可用性。最大的问题就是当Master Broker挂了之后,没办法让Slave Broker自动切换为新的Master Broker,需要手工做一些运维操作,修改配置以及重启机器才行,这个非常麻烦,可能就会导致系统的不可用。
    • Dledger技术是要求至少得是一个Master带两个Slave,这样有三个Broke组成一个Group,也就是作为一个分组来运行。一旦 Master宕机,他就可以从剩余的两个Slave中选举出来一个新的Master对外提供服务。

各个NameServer就是通过跟Broker建立好的长连接不断收到心跳包,然后定时检查Broker有没有120s都没发送心跳包,来判定 集群里各个Broker到底挂掉了没有。

1624665451462-62eeeec6-2d27-4449-8913-033953ace8e8.png

逻辑架构图

为什么说是 高可用、高并发、海量消息、可伸缩

  • NameServer随便一台机器挂了都不怕,他是集群化部署的, 每台机器都有完整的路由信息;
  • Broker随便挂了一台机器也不怕,挂了Slave对集群没太大影响,挂了Master也会基于Dledger技术实现自动Slave切换为Master;实现高可用性。
  • 生产者系统和消费者系统随便挂了一台都不怕,因为他们都是集群化部署的,其他机器会接管工作。
  • 这个架构可以抗下高并发,因为假设订单系统对订单Topic要发起每秒10万QPS的写入,那么只要订单Topic分散在比如5台Broker 上,实际上每个Broker会承载2万QPS写入,也就是说高并发场景下的10万QPS可以分散到多台Broker上抗下来。
  • 集群足以存储海量消息,因为所有数据都是分布式存储的,每个Topic的数据都是存储在多台Broker机器上的,用集群里多台 Master Broker就足以存储海量的消息。
  • 这套架构还具备伸缩性,就是说如果要抗更高的并发,存储跟多的数据,完全可以在集群里加入更多的Broker机器,这样就可以 线性扩展集群了。

部署一个小规模的RocketMQ集群

对这个集群进行压测,看一看在公司的机器配置下,可以抗下多高的QPS。

物理部署架构图得参照这个逻辑架构图才能部署出来。

1624695307065-6b91757f-3390-480d-8f3a-00eaa48f91f3.png

  • NameServer:3台机器,每台机器都是8核CPU + 16G内存 + 500G磁盘 + 千兆网卡 (NameServer是核心的路由服务,所以给8核16G的较高配置的机器,但是他一般就是承载Broker注册和心跳、系统的路由表拉取等请求,负载其实很低,因此不需要特别高的机器配置,部署三台也可以实现高可用的效果了。)
  • Broker:3台机器,每台机器都是24核CPU(两颗x86_64 cpu,每颗cpu是12核) + 48G内存 + 1TB磁盘 + 千兆网卡 (负载最高的,未来要承载高并发写入和海量数据存储,所以把最高配置的机器都会留给他,这里用3台机器组成一个“单 Master + 双Slave”的集群。
  • 生产者:2台机器,每台机器都是4核CPU + 8G内存 + 500GB磁盘 + 千兆网卡 (生产者和消费者机器都是临时用来测试的,而且一般他们都是业务系统,只会部署在标准的4核8G的机器配置下。)
  • 消费者:2台机器,每台机器都是4核CPU + 8G内存 + 500GB磁盘 + 千兆网卡

一台机器快速部署RocketMQ

git clone https://github.com/openmessaging/openmessaging-storage-dledger.git 
cd openmessaging-storage-dledger 
mvn clean install -DskipTests
git clone https://github.com/apache/rocketmq.git 
cd rocketmq 
git checkout -b store_with_dledger origin/store_with_dledger 
mvn -Prelease-all -DskipTests clean install -U
cd distribution/target/apache-rocketmq
# 在这个目录中,需要编辑三个文件,
# 一个是bin/runserver.sh,一个是bin/runbroker.sh,另外一个是bin/tools.sh
# 在里面找到如下三行,然后将第二行和第三行都删了,同时将第一行的值修改为你自己的JDK的主目录
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java 
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java 
[ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!"

# /usr/libexec/java_home -V,修改为你的Java主目录即可


# 快速RocketMQ集群启动:
sh bin/dledger/fast-try.sh start

这个命令会在当前这台机器上启动一个NameServer和三个Broker,三个Broker其中一个是Master,另外两个是Slave,瞬间就可以组 成一个最小可用的RocketMQ集群。

sh bin/mqadmin clusterList -n 127.0.0.1:9876
# 慢
# 会看到三行记录,说是一个RaftCluster, 
# Broker名称叫做RaftNode00,然后BID是0、1、2,也有可能是0、1、3

这就说明RocketMQ集群启动成功了,BID为0的就是Master,BID大于0的就都是Slave,其实在这里也可以叫做Leader和Follower

我们看到三台机器的地址分别为:

192.168.31.153:30921

192.168.31.153:30911

192.168.31.153:30931

现30921端口的Broker的BID是0,说明他是Master

此时我们可以用命令(lsof -i:30921)找出来占用30921端口的进程PID,接着就用kill -9的命令给他杀了,比如我这里占用30921端口 的进程PID是4344,那么就执行命令:kill -9 4344

接着等待个10s,再次执行命令查看集群状态: sh bin/mqadmin clusterList -n 127.0.0.1:9876

此时就会发现作为Leader的BID为0的节点,变成另外一个Broker了,这就是说Slave切换为Master了。

完成正式三台NameServer的部署

在三台NameServer的机器上,按照上面的步骤安装好Java,构建好Dledger和RocketMQ,然后编辑对应的文件,设置 好JAVA_HOME就可以了。

此时可以执行如下的一行命令就可以启动NameServer:

nohup sh mqnamesrv &

这个NameServer监听的接口默认就是9876,所以如果你在三台机器上都启动了NameServer,那么他们的端口都是9876,此时我们 就成功的启动了三个NameServer了

完成一组Broker集群的部署

需要启动一个Master Broker和两个Slave Broker,这个启动也很简单,分别在上述三台为Broker准备的高配置机器上,安装好 Java,构建好Dledger和RocketMQ,然后编辑好对应的文件。

第一个Broker的配置文件是broker-n0.conf,

第二个broker的配置文件可以是broker-n1.conf,

第三个broker 的配置文件可以是broker-n2.conf。

# 这个是集群的名称,你整个broker集群都可以用这个名称 
brokerClusterName = RaftCluster

# 这是Broker的名称,比如你有一个Master和两个Slave,那么他们的Broker名称必须是一样的,
# 因为他们三个是一个分组,
#  如果你有 另外一组Master和两个Slave,你可以给他们起个别的名字,比如说RaftNode01 
brokerName=RaftNode00

# 这个就是你的Broker监听的端口号,如果每台机器上就部署一个Broker,
# 	可以考虑就用这个端口号,不用修改 
listenPort=30911

# 这里是配置NameServer的地址,如果你有很多个NameServer的话,
# 可以在这里写入多个NameServer的地址 
namesrvAddr=127.0.0.1:9876

# 下面两个目录是存放Broker数据的地方,你可以换成别的目录,类似于是/usr/local/rocketmq/node00之类的 
storePathRootDir=/tmp/rmqstore/node00 
storePathCommitLog=/tmp/rmqstore/node00/commitlog

# 这个是非常关键的一个配置,就是是否启用DLeger技术,这个必须是true
enableDLegerCommitLog=true

# 这个一般建议和Broker名字保持一致,一个Master加两个Slave会组成一个Group 
dLegerGroup=RaftNode00

# 这个很关键,对于每一组Broker,你得保证他们的这个配置是一样的,
# 在这里要写出来一个组里有哪几个Broker,
# 比如在这里假设有 三台机器部署了Broker,要让他们作为一个组,
# 那么在这里就得写入他们三个的ip地址和监听的端口号 
dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913

# 这个是代表了一个Broker在组里的id,一般就是n0、n1、n2之类的,这个你得跟上面的dLegerPeers中的n0、n1、n2相匹配
dLegerSelfId=n0

# 这个是发送消息的线程数量,一般建议你配置成跟你的CPU核数一样,
# 比如我们的机器假设是24核的,那么这里就修改成24核 
sendMessageThreadPoolNums=24
nohup sh bin/mqbroker -c conf/dledger/broker-n0.conf &
  • 对每一组Broker,他们的Broker名称、Group名称都是一样的,
  • 给他们配置好一样的dLegerPeers(里面是组内三台Broker 的地址)
  • 配置好对应的NameServer的地址,
  • 最后还有就是每个Broker有自己的ID,在组内是唯一的就可以了,比如说不同的组里都 有一个ID为n0的broker,这个是可以的。

在三台机器上分别用命令启动Broker即可。启动完成过后,可以跟NameServer进 行通信,检查Broker集群的状态,就是如下命令:

sh bin/mqadmin clusterList -n 127.0.0.1:9876。

压测

1624668487239-cf8ec66f-3d62-4a0b-8933-cfa3a1806c48.png

1624668553413-d9fb0102-9836-4a22-a81a-932fda0fe7dd.png

1624668580183-a630ba30-d93f-44bf-9eab-0bf9cc7d3c4b.png

优化生产机器上的os内核参数和RocketMQ的jvm参数了, 这些参数优化好了,才能正式在高配置机器上启动RocketMQ,让他把性能发挥到最高,接着压测才有意义。

调整Broker的OS内核参数、JVM参数然后重新启动Broker,接着就可以启动生产者和消费者去 发送消息和获取消息,然后去观察RocketMQ能承载的QPS,CPU、IO、磁盘、网络等负载。

既然是压测,那么必然是要看RocketMQ集群能承载的最高QPS,同时在承载这个QPS的同时,各个机器的CPU、IO、 磁盘、网络、内存的负载情况,要看机器资源的使用率,还要看JVM的GC情况,等等。

RocketMQ集群的监控、管理和运维

整个RocketMQ集群的元数据都集中在了NameServer里,包括有多少Broker,有哪些Topic,有哪些 Producer,有哪些Consumer,目前集群里有多少消息,等等。

可以随便找一台机器,用NameServer的三台机器中的任意一台机器就可以,在里面执行如下命令拉取RocketMQ运维工作台的源 码:

git clone https://github.com/apache/rocketmq-externals.git

# 然后进入rocketmq-console的目录:
cd rocketmq-externals/rocketmq-console

# 执行以下命令对rocketmq-cosole进行打包,把他做成一个jar包:
mvn package -DskipTests

# 然后进入target目录下,可以看到一个jar包,接着执行下面的命令启动工作台:
java -jar rocketmq-console-ng-1.0.1.jar --server.port=8080 --rocketmq.config.namesrvAddr=127.0.0.1:9876

这里务必要在启动的时候设置好NameServer的地址,如果有多个地址可以用分号隔开,

导航栏里的“集群”,就会进入集群的一个监控界面:

你可以看到各个Broker的分组,哪些是Master,哪些是Slave,他们各自的机器地址和端口 号,还有版本号。他们每台机器的生产消息TPS和消费消息TPS,还有消息总数。通过这个TPS统计,就是每秒写入或者被消费的消息数量,就可以看出RocketMQ集群的TPS和并发访问量。点击状态可以看到这个Broker更加细节和具体的一些统计项,点 击配置可以看到这个Broker具体的一些配置参数的值。(是Transaction Per Second,就是每秒事务的意思,在这里就是每秒消息数量的意思。)

边导航栏的“主题”,可以看到下面的界面,通过这个界面就可以对Topic进行管理了,比如你可以在这里创建、删除和管理 Topic,查看Topic的一些装填、配置,等等,可以对Topic做各种管理。

导航栏里的“消息”和“消息轨迹”,又可以对消息进行查询和管理。

这个工作台,就可以对集群整体的消息数量以及消息 TPS,还有各个Broker的消息数量和消息TPS进行监控。

还可以对Broker、Topic、消费者、生产者、消息这些东西进行对应的查询和管理,非常的便捷。

os内核参数、jvm参数以及自身核心参数都做出相对应的合理的调整,再进行压测和上线。

都是跟磁盘文件IO、网络通信、内存管理、线程数量有关系的

中间件系统肯定要开启大量的线程(跟vm.max_map_count有关) 而且要进行大量的网络通信和磁盘IO(跟ulimit有关) 然后大量的使用内存(跟vm.swappiness和vm.overcommit_memory有关)

对于生产环境的中间件集群,不能直接用各种默认参 数启动,因为那样可能有很多问题,或者没法把中间件的性能发挥出来。

1624695313704-8585370b-7bab-451c-a248-93d0c1cfec83.png1624695500571-b4e64e2b-7027-4fa3-8652-2a73582f0771.png

(1)中间件系统在压测或者上生产之前,需要对三大块参数进行调整:OS内核参数、JVM参数以及中间件核心参数

(2)OS内核参数主要调整的地方都是跟磁盘IO、网络通信、内存管理以及线程管理有关的,需要适当调节大小

(3)JVM参数需要我们去中间件系统的启动脚本中寻找他的默认JVM参数,然后根据机器的情况,对JVM的堆内存大小,新生代大 小,Direct Buffer大小,等等,做出一些调整,发挥机器的资源

(4)中间件核心参数主要也是关注其中跟网络通信、磁盘IO、线程数量、内存 管理相关的,根据机器资源,适当可以增加网络通信线 程,控制同步刷磁盘或者异步刷磁盘,线程数量有多少,内存中一些队列的大小

对RocketMQ集群进行OS内核参数的调整

  • vm.max_map_count 这个参数的值会影响中间件系统可以开启的线程的数量,如果这个参数过小,有的时候可能会导致有些中间件无法开启足够的线程,进而导致报错,甚至中间件系统挂掉。他的默认值是65536,但是这个值有时候是不够的,比如我们大数据团队的生产环境部署的Kafka集群曾经有一次就报出过这个异常, 说无法开启足够多的线程,直接导致Kafka宕机了。因此建议可以把这个参数调大10倍,比如655360这样的值,保证中间件可以开启足够多的线程。

echo 'vm.max_map_count=655360' >> /etc/sysctl.conf

  • “vm.overcommit_memory”这个参数有三个值可以选择,0、1、2。如果值是0的话,在你的中间件系统申请内存的时候,os内核会检查可用内存是否足够,如果足够的话就分配内存给你,如果感觉剩余内存不是太够了,干脆就拒绝你的申请,导致你申请内存失败,进而导致中间件系统异常出错。因此一般需要将这个参数的值调整为1,意思是把所有可用的物理内存都允许分配给你,只要有内存就给你来用,这样可以避免申请内 存失败的问题。比如我们曾经线上环境部署的Redis就因为这个参数是0,导致在save数据快照到磁盘文件的时候,需要申请大内存的时候被拒绝了,进 而导致了异常报错。

echo 'vm.overcommit_memory=1' >> /etc/sysctl.conf

  • vm.swappiness 这个参数是用来控制进程的swap行为的,这个简单来说就是os会把一部分磁盘空间作为swap区域,然后如果有的进程现在可能不是太 活跃,就会被操作系统把进程调整为睡眠状态,把进程中的数据放入磁盘上的swap区域,然后让这个进程把原来占用的内存空间腾出 来,交给其他活跃运行的进程来使用。如果这个参数的值设置为0,意思就是尽量别把任何一个进程放到磁盘swap区域去,尽量大家都用物理内存。如果这个参数的值是100,那么意思就是尽量把一些进程给放到磁盘swap区域去,内存腾出来给活跃的进程使用。默认这个参数的值是60,有点偏高了,可能会导致我们的中间件运行不活跃的时候被迫腾出内存空间然后放磁盘swap区域去。因此通常在生产环境建议把这个参数调整小一些,比如设置为10,尽量用物理内存,别放磁盘swap区域去。

echo 'vm.swappiness=10' >> /etc/sysctl.conf

  • ulimit 控制linux上的最大文件链接数的,默认值可能是1024,一般肯定是不够的,因为你在大量频繁的读写磁盘文件的时候,或 者是进行网络通信的时候,都会跟这个参数有关系。对于一个中间件系统而言肯定是不能使用默认值的,如果你采用默认值,很可能在线上会出现如下错误:error: too many open files。

echo 'ulimit -n 1000000' >> /etc/profile

一百万

对JVM参数进行调整

如mqbroker是用来启动Broker的, mqnamesvr是用来启动NameServer的。

用mqbroker来举例,我们查看这个脚本里的内容,最后有如下一行:

sh ${ROCKETMQ_HOME}/bin/runbroker.sh org.apache.rocketmq.broker.BrokerStartup $@

这一行内容就是用runbroker.sh脚本来启动一个JVM进程,JVM进程刚开始执行的main类就是 org.apache.rocketmq.broker.BrokerStartup

JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g" 
JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m 
  -XX:G1ReservePercent=25 XX:InitiatingHeapOccupancyPercent=30 
  -XX:SoftRefLRUPolicyMSPerMB=0" 
JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:/dev/shm/mq_gc_%p.log 
  -XX:+PrintGCDetails -XX:+PrintGCDateStamps XX:+PrintGCApplicationStoppedTime 
  -XX:+PrintAdaptiveSizePolicy" 
JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 
  -XX:GCLogFileSize=30m" 
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow" 
JAVA_OPT="${JAVA_OPT} -XX:+AlwaysPreTouch" 
JAVA_OPT="${JAVA_OPT} -XX:MaxDirectMemorySize=15g" 
JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages -XX:-UseBiasedLocking" 
JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib" 
#JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n" 
JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}" 
JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"

最后其实就是

“-server -Xms8g -Xmx8g -Xmn4g -XX:+UseG1GC -XX:G1HeapRegionSize=16m 
-XX:G1ReservePercent=25 XX:InitiatingHeapOccupancyPercent=30 
-XX:SoftRefLRUPolicyMSPerMB=0 -verbose:gc 
-Xloggc:/dev/shm/mq_gc_%p.log XX:+PrintGCDetails -XX:+PrintGCDateStamps 
-XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy 
XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m 
-XX:-OmitStackTraceInFastThrow XX:+AlwaysPreTouch 
-XX:MaxDirectMemorySize=15g -XX:-UseLargePages -XX:-UseBiasedLocking”

-server:这个参数就是说用服务器模式启动,这个没什么可说的,现在一般都是如此

-Xms8g -Xmx8g -Xmn4g: 默认的堆大小是8g内存,新生代是4g内存, 但是我们的高配物理机是48g内存的。比如给堆内存20g,其中新生代给10g,甚至可以更多一些,当然要留一些内存给操作系统来用

-XX:+UseG1GC -XX:G1HeapRegionSize=16m:对新生代 和老年代都是用G1来回收。这里把G1的region大小设置为了16m,这个因为机器内存比较多,所以region大小可以调大一些给到16m,不然用2m的region,会 导致region数量过多的

-XX:G1ReservePercent=25:在G1管理的老年代里预留25%的空闲内存,保证新生代对象晋升到老年代的时候有足 够空间,避免老年代内存都满了,默认值是10%,略微偏少,这里RocketMQ给调大了一些

-XX:InitiatingHeapOccupancyPercent=30:堆内存的使用率达到30%之后就会自动启动G1的并发垃圾回收,开 始尝试回收一些垃圾对象。默认值是45%,这里调低了一些,也就是提高了GC的频率,但是避免了垃圾对象过多,一次垃圾回收耗时过长的问题

-XX:SoftRefLRUPolicyMSPerMB=0:这个参数默认设置为0了,建 议这个参数不要设置为0,避免频繁回收一些软引用的Class对象,这里可以调整为比如1000

-verbose:gc -Xloggc:/dev/shm/mq_gc_%p.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy -XX:+UseGCLogFileRotation XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m:这一堆参数都是控制GC日志打印输出的,确定了gc日志文件的地址,要 打印哪些详细信息,然后控制每个gc日志文件的大小是30m,最多保留5个gc日志文件。

-XX:-OmitStackTraceInFastThrow:这个参数是说,有时候JVM会抛弃一些异常堆栈信息,因此这个参数设置之后,就是禁用这个 特性,要把完整的异常堆栈信息打印出来

-XX:+AlwaysPreTouch:这个参数的意思是我们刚开始指定JVM用多少内存,不会真正分配给他,会在实际需要使用的时候再分配给 他所以使用这个参数之后,就是强制让JVM启动的时候直接分配我们指定的内存,不要等到使用内存的时候再分配

-XX:MaxDirectMemorySize=15g:这是说RocketMQ里大量用了NIO中的direct buffer,这里限定了direct buffer最多申请多少, 如果你机器内存比较大,可以适当调大这个值

-XX:-UseLargePages -XX:-UseBiasedLocking:这两个参数的意思是禁用大内存页和偏向锁

所以要改的基本只有堆内存的大小,RocketMQ默认的JVM参数是采用了G1垃圾回收器,默认堆内存大小是8G。GC日志,禁用一些特性,开启一些特性,这些都直接维持RocketMQ的默认值即可。

对RocketMQ核心参数进行调整

dledger的示例配置文件: conf/dledger

在这里主要是有一个较为核心的参数:sendMessageThreadPoolNums=16

这个参数的意思就是RocketMQ内部用来发送消息的线程池的线程数量,默认是16 其实这个参数可以根据你的机器的CPU核数进行适当增加,比如机器CPU是24核的,可以增加这个线程数量到24或者30,都是可以 的。

压测--综合TPS以及机器负载,尽量找到一个最高的TPS同时机器的各项负载在可承受范围之内,这才是压 测的目的。

RocketMQ集群在抗下10万TPS(可以理解为每秒处理10万条消息)的同时,结果机器的CPU负载达到100%,内存几乎消耗殆 尽,IO负载极高,网卡流量打满甚至快要打爆。在机器快挂掉的情况下让中间件抗超高的负载是绝对不行的。这种压测方法,仅仅能压测出来一个极限值而已。实际上我们平时做压测,主要关注的还是要压测出来一个最合适的最高负载。

RocketMQ的TPS和机器的资源使用率和负载之间取得一个平衡。比如RocketMQ集群在机器资源使用率极高的极端情况下可以扛到10万TPS,但是当他仅仅抗下8万TPS的时候,你会发现cpu负载、内 存使用率、IO负载和网卡流量,都负载较高,但是可以接受,机器比较安全,不至于宕机。那么这个8万TPS实际上就是最合适的一个最高负载,也就是说,哪怕生产环境中极端情况下,RocketMQ的TPS飙升到8万TPS,你知 道机器资源也是大致可以抗下来的,不至于出现机器宕机的情况。

所以我们做压测,其实最主要的是综合TPS以及机器负载,尽量找到一个最高的TPS同时机器的各项负载在可承受范围之内,这才是压 测的目的。

RocketMQ的TPS和消息延时

我们让两个Producer不停的往RocketMQ集群发送消息,每个Producer所在机器启动了80个线程,相当于每台机器有80个线程并发的 往RocketMQ集群写入消息。而RocketMQ集群是1主2从组成的一个dledger模式的高可用集群只有一个Master Broker会接收消息的写入。有2个Cosumer不停的从RocketMQ集群消费数据。

每条数据的大小是500个字节,这个非常关键,大家一定要牢记这个数字,因为这个数字是跟后续的网卡流量有关的。一条消息从Producer生产出来到经过RocketMQ的Broker存储下来,再到被Consumer消费,基本上这个时间跨度不会超 过1秒钟,这些这个性能是正常而且可以接受的。

同时在RocketMQ的管理工作台中可以看到,Master Broker的TPS(也就是每秒处理消息的数量),可以稳定的达到7万左右,也就是 每秒可以稳定处理7万消息。

cpu负载情况

Broker机器上的CPU负载,可以通过top、uptime。

load average:12.03,12.05,12.08

代表的是cpu在1分钟、5分钟和15分钟内的cpu负载情况

比如我们一台机器是24核的,那么上面的12意思就是有12个核在使用中。换言之就是还有12个核其实还没使用,cpu还是有很大余力 的。

这个cpu负载其实是比较好的,因为并没有让cpu负载达到极限。

内存使用率

使用free命令就可以查看到内存的使用率,根据当时的测试结果,机器上48G的内存,仅仅使用了一部分,还剩下很大一部分内存都是 空闲可用的,或者是被RocketMQ用来进行磁盘数据缓存了。所以内存负载是很低的。

JVM GC频率

使用jstat命令就可以查看RocketMQ的JVM的GC频率,基本上新生代每隔几十秒会垃圾回收一次,每次回收过后存活的对象很少,几乎不进入老年代

磁盘IO负载

首先可以用top命令查看一下IO等待占用CPU时间的百分比,你执行top命令之后

Cpu(s): 0.3% us, 0.3% sy, 0.0% ni, 76.7% id, 13.2% wa, 0.0% hi, 0.0% si

在这里的13.2% wa,说的就是磁盘IO等待在CPU执行时间中的百分比,如果这个比例太高,说明CPU执行的时候大部分时间都在等待执行IO,也就说明IO负载很高,导致大量的IO等待。

这个当时我们压测的时候,是在40%左右,说明IO等待时间占用CPU执行时间的比例在40%左右,这是相对高一些,但还是可以接受 的,只不过如果继续让这个比例提高上去,就很不靠谱了,因为说明磁盘IO负载可能过高了。

网卡流量

查看服务器的网卡流量:

sar -n DEV 1 2

通过这个命令就可以看到每秒钟网卡读写数据量了。当时我们的服务器使用的是千兆网卡,千兆网卡的理论上限是每秒传输128M数 据,但是一般实际最大值是每秒传输100M数据。

因此当时我们发现的一个问题就是,在RocketMQ处理到每秒7万消息的时候,每条消息500字节左右的大小的情况下,每秒网卡传输 数据量已经达到100M了,就是已经达到了网卡的一个极限值了。

因为一个Master Broker服务器,每秒不光是通过网络接收你写入的数据,还要把数据同步给两个Slave Broker,还有别的一些网络通 信开销。

因此实际压测发现,每条消息500字节,每秒7万消息的时候,服务器的网卡就几乎打满了,无法承载更多的消息了。

总结

服务器的性能瓶颈在网卡上,因为网卡每秒能传输的数据是有限的。当我们使用平均大小为500字节的消息时,最多就是做到RocketMQ单台服务器每秒7万的TPS,而且这个时候cpu负载、内存负 载、jvm gc负载、磁盘io负载,基本都还在正常范围内。

只不过这个时候网卡流量基本已经打满了,无法再提升TPS了。因此在这样的一个机器配置下,RocketMQ一个比较靠谱的TPS就是7万左右。

压测报告:压测的过程,在压测时候的机器各项指标的表现。

即使在搞双11活动高峰的时候,公司后台系统的并发访问量也就是每秒上万,即 使你多考虑一些,每秒几万的并发量也就最多了。因此在部署的时候,建议采用3台机器部署就足够了,而对于Broker而言采用6台机器来部署,2个Master Broker和4个Slave Broker,这样2个Master Broker每秒最多可以处理十几万消息,4个Slave Broker同时也能每秒提供高吞吐的数据 消费,而且全面保证高可用性。

到底应该如何压测:应该在TPS和机器的cpu负载、内存使用率、jvm gc频率、磁盘io负载、网络流量负载之间取得一个平衡,尽量让 TPS尽可能的提高,同时让机器的各项资源负载不要太高。 实际压测过程:采用几台机器开启大量线程并发读写消息,然后观察TPS、cpu load(使用top命令)、内存使用率(使用free命 令)、jvm gc频率(使用jstat命令)、磁盘io负载(使用top命令)、网卡流量负载(使用sar命令),不断增加机器和线程,让TPS不 断提升上去,同时观察各项资源负载是否过高。 生产集群规划:根据公司的后台整体QPS来定,稍微多冗余部署一些机器即可,实际部署生产环境的集群时,使用高配置物理机,同时 合理调整os内核参数、jvm参数、中间件核心参数,如此即可

对MQ集群做过压测吗?

使用什么样的机器配置做的压测?

使用多大规模的集群做的压测?

如何压测的?

在压测的过程中发现单Broker的TPS最高有多少?

在压测过程中,cpu负载、内存使用率、jvm gc频率、磁盘io负载、网卡流量负载,这些值都是如何变化的?

在压测过后,是如何规划生产集群的?

目前公司线上MQ集群的TPS多高?机器资源的负载情况如何?能否抗住?

RocketMQ编程模型

  • 消息发送者的固定步骤

    1. 创建消息生产者producer,并制定生产者组名
    2. 指定Nameserver地址
    3. 启动producer
    4. 创建消息对象,指定主题Topic、Tag和消息体
    5. 发送消息
    6. 关闭生产者producer
  • 消息消费者的固定步骤

    1. 创建消费者Consumer,制定消费者组名
    2. 指定Nameserver地址
    3. 订阅主题Topic和Tag
    4. 设置回调函数,处理消息
    5. 启动消费者consumer

生产者的消息发送方式

同步发送消息到RocketMQ?

SendResult sendResult = producer.send(msg),然后你会卡在这里,代码 不能往下走了。你要一直等待MQ返回一个结果给你,你拿到了SendResult之后,接着你的代码才会继续往下走。

1627686775494-23f0fd87-990b-4ace-a692-0b3dd17450d8.png

异步发送消息到RocketMQ?

1624701961549-f7d85af5-a58d-4d66-b2fe-d25c6c714b47.png

1627686799869-3020ad26-71bb-4c2f-8a85-3d5a30e9cf09.png

把消息发送出去,然后上面的代码就直接往下走了,不会卡在这里等待MQ返回结果给你!

然后当MQ返回结果给你的时候,Producer会回调你的SendCallback里的函数,如果发送成功了就回调onSuccess函数,如果发送失 败了就回调onExceptino函数。

单向消息到RocketMQ?

public class OnewayProducer {
    public static void main(String[] args) throws Exception{
        //Instantiate with a producer group name.
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // Specify name server addresses.
        producer.setNamesrvAddr("localhost:9876");
        //Launch the instance.
        producer.start();
        for (int i = 0; i < 100; i++) {
            //Create a message instance, specifying topic, tag and message body.
            Message msg = new Message("TopicTest" /* Topic */,
                "TagA" /* Tag */,
                ("Hello RocketMQ " +
                    i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
            );
            //Call send message to deliver message to one of brokers.
            producer.sendOneway(msg);
        }
        //Wait for sending to complete
        Thread.sleep(5000);        
        producer.shutdown();
    }
}

这个sendOneway的意思,就是你发送一个消息给MQ,然后代码就往下走了,根本不会关注MQ有没有返回结果给你,你也不需要 MQ返回的结果,无论发送的消息是成功还是失败,都不关你的事。

消费者的消费模式

Push消费模式?简单

Push消费模式本质底层_是基于_这种消费者主动拉取的模式来实现的,只不过他的名字叫做Push而已,意思是Broker会尽可能实时的把新消息交给消费者机器来进行处理,他的消息时效性会更好。一般我们使用RocketMQ的时候,消费模式通常都是基于他的Push模式来做的,因为Pull模式的代码写起来更加的复杂和繁琐,而且 Push模式底层本身就是基于消息拉取的方式来做的,只不过时效性更好而已。

Push模式的实现思路:当消费者发送请求到Broker去拉取消息的时候,如果有新的消息可以消费那么就会立马返回 一批消息到消费机器去处理,处理完之后会接着立刻发送请求到Broker机器去拉取下一批消息。所以消费机器在Push模式下会处理完一批消息,立马发起请求拉取下一批消息,消息处理的时效性非常好看起来就跟Broker一直不停的推送消息到消费机器一样。

Push模式下请求挂起和长轮询的机制

  • 当你的请求发送到Broker,结果他发现没有新的消息给你处理的时候,就会让请求线程挂起,默认是挂起15秒,然后这个期间他会有 后台线程每隔一会儿就去检查一下是否有的新的消息给你,另外如果在这个挂起过程中,如果有新的消息到达了会主动唤醒挂起的线程,然后把消息返回给你。
  • 当然其实消费者进行消息拉取的底层源码是非常复杂的,涉及到大量的细节,但是他的核心思路大致就是如此,我们只要知道,用常见的Push模式消费,****本质也是消费者不停的发送请求到broker去拉取一批一批的消息就行了。

1624702179495-e80a89e1-2d6a-4c67-a342-b98b175d2651.png

Consumer的类名:DefaultMQPushConsumer

Broker会主动把消息发送给你的消费者,你的消费者是被动的接收Broker推送给过来的消息,然后进行处理。

Pull消费模式?

Consumer的类名:DefaultMQPullConsumer

Broker不会主动推送消息给Consumer,而是消费者主动发送请求到Broker去拉取消息过来。

1624702245019-360b0c95-c083-474a-ba84-0d872be7b13e.png

使用哪一种

用户应用的角度

  • 拉取式消费的应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
  • 推动式消费模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。

消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的 是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ 支持两种消息模式:集群消费 (Clustering)和广播消费(Broadcasting)。

集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。 广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。

消息样例

普通消息

也就是上面的生产和消费的样例

顺序消息

public class Producer {
    public static void main(String[] args) throws UnsupportedEncodingException {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
            producer.start();

            String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
            for (int i = 0; i < 100; i++) {
                int orderId = i % 10;
                Message msg =
                    new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i,
                                ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Integer id = (Integer) arg;
                        int index = id % mqs.size();
                        return mqs.get(index);
                    }
                }, orderId);

                System.out.printf("%s%n", sendResult);
            }

            producer.shutdown();
        } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

启动多个Consumer实例,观察下每一个订单的消息分配以及每个订单下多个步骤的消费顺序。

public class Consumer {

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

        consumer.subscribe("TopicTest", "TagA || TagC || TagD");

        consumer.registerMessageListener(new MessageListenerOrderly() {
            AtomicLong consumeTimes = new AtomicLong(0);

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                this.consumeTimes.incrementAndGet();
                if ((this.consumeTimes.get() % 2) == 0) {
                    return ConsumeOrderlyStatus.SUCCESS;
                } else if ((this.consumeTimes.get() % 3) == 0) {
                    return ConsumeOrderlyStatus.ROLLBACK;
                } else if ((this.consumeTimes.get() % 4) == 0) {
                    return ConsumeOrderlyStatus.COMMIT;
                } else if ((this.consumeTimes.get() % 5) == 0) {
                    context.setSuspendCurrentQueueTimeMillis(3000);
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }

                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        consumer.start();
        System.out.printf("Consumer Started.%n");
    }

}

不管订单在多个Consumer实例之前是如何分配的,每个订单下的多条消息顺序都是固定从0~5 的。 RocketMQ保证的是消息的局部有序,而不是全局有序。

RocketMQ也只保证了每个OrderID的所有消息有序(发到了同一个 queue),而并不能保证所有消息都有序。

要保证最终消费到的消息是有序的,需要从Producer、Broker、Consumer三个步骤都保证消息有序才行。

首先在发送者端:在默认情况下,消息发送者会采取Round Robin轮询方式把消息发送到不同的 MessageQueue(分区队列),消息是不能保证顺序的。而只有当一组有序的消息发送到同一个MessageQueue上时,才能利用MessageQueue先进先出的特性保证这一组消息有序。而Broker中一个队列内的消息是可以保证有序的。

然后在消费者端:消费者会从多个消息队列上去拿消息。这时虽然每个消息队列上的消息是有序的,但是多个队列之间的消息仍然是乱序的。消费者端要保证消息有序,就需要按队列一个一个 来取消息,即取完一个队列的消息后,再去取下一个队列的消息。而给consumer注入的 MessageListenerOrderly对象,在RocketMQ内部就会通过锁队列的方式保证消息是一个一个队 列来取的。MessageListenerConcurrently这个消息监听器则不会锁队列,每次都是从多个 Message中取一批数据(默认不超过32条)。因此也无法保证消息有序。

广播消息

在集群状态 (MessageModel.CLUSTERING)下,每一条消息只会被同一个消费者组中的一个实例消费到(这跟 kafka和rabbitMQ的集群模式是一样的)。而广播模式则是把消息发给了所有订阅了对应主题的消 费者,而不管消费者是不是同一个消费者组。

public class PushConsumer {

    public static void main(String[] args) throws InterruptedException, MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_1");

        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

        consumer.setMessageModel(MessageModel.BROADCASTING);

        consumer.subscribe("TopicTest", "TagA || TagC || TagD");

        consumer.registerMessageListener(new MessageListenerConcurrently() {

            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                ConsumeConcurrentlyContext context) {
                System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();
        System.out.printf("Broadcast Consumer Started.%n");
    }
}

延迟消息

开源版本的RocketMQ中,对延迟消息并不支持任意时间的延迟设定(商业版本中支持),而是只支 持18个固定的延迟级别,1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

 public class ScheduledMessageConsumer {
    
     public static void main(String[] args) throws Exception {
         // Instantiate message consumer
         DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer");
         // Subscribe topics
         consumer.subscribe("TestTopic", "*");
         // Register message listener
         consumer.registerMessageListener(new MessageListenerConcurrently() {
             @Override
             public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
                 for (MessageExt message : messages) {
                     // Print approximate delay time period
                     System.out.println("Receive message[msgId=" + message.getMsgId() + "] "
                             + (System.currentTimeMillis() - message.getStoreTimestamp()) + "ms later");
                 }
                 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
             }
         });
         // Launch consumer
         consumer.start();
     }
 }
public class ScheduledMessageProducer {
    
     public static void main(String[] args) throws Exception {
         // Instantiate a producer to send scheduled messages
         DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");
         // Launch producer
         producer.start();
         int totalMessagesToSend = 100;
         for (int i = 0; i < totalMessagesToSend; i++) {
             Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
             // This message will be delivered to consumer 10 seconds later.
             message.setDelayTimeLevel(3);
             // Send the message
             producer.send(message);
         }
    
         // Shutdown producer after use.
         producer.shutdown();
     }
        
 }

批量消息

批量消息是指将多条消息合并成一个批量消息,一次发送出去。这样的好处是可以减少网络IO,提升吞 吐量。

如果批量消息大于1MB就不要用一个批次 发送,而要拆分成多个批次消息发送。也就是说,一个批次消息的大小不要超过1MB。

实际使用时,这个1MB的限制可以稍微扩大点,实际最大的限制是4194304字节,大概4MB。但 是使用批量消息时,这个消息长度确实是必须考虑的一个问题。而且批量消息的使用是有一定限 制的,这些消息应该有相同的Topic,相同的waitStoreMsgOK。而且不能是延迟消息、事务消息等。

过滤消息

TAG是RocketMQ中特有的一个消息属性。RocketMQ的最佳实践中就建议,使用RocketMQ时, 一个应用可以就用一个Topic,而应用中的不同业务就用TAG来区分。

过滤上推到broker,而不是consumer,减少IO

在发送消息的时候,给消息设置tag和属性

1626946751978-6c10d22f-499b-4d53-9cd2-d9e2008d3ef7.png

在消费数据的时候根据tag和属性进行过滤

1626946756548-b8d161bc-5110-48bb-a123-c9fa89974aeb.png

只要tag=TableA和tag=TableB的数据。

可以通过下面的语法去指定,我们要根据每条消息的属性的值进行过滤,此时可以支持一些语法,比如:

1626946788133-cc292bd8-4ce8-418d-83e4-49aac8c4cde1.png

MessageSelector。这里面的sql语句是按照SQL92标准来执行的。sql中可以使用的参数有默认的 TAGS和一个在生产者中加入的a属性。

RocketMQ还是支持比较丰富的数据过滤语法的,如下所示:

(1)数值比较,比如:>,>=,<,<=,BETWEEN,=;

(2)字符比较,比如:=,<>,IN;

(3)IS NULL 或者 IS NOT NULL;

(4)逻辑符号 AND,OR,NOT;

(5)数值,比如:123,3.1415;

(6)字符,比如:'abc',必须用单引号包裹起来;

(7)NULL,特殊的常量

(8)布尔值,TRUE 或 FALSE

只有推模式的消费者可以使用SQL过滤。拉模式是用不了的。因为过滤是在broker进行而不是consumer

事务消息

事务消息只保证了发送者本地事务和发送消息这两个操作的原子性,但是并不保证消费者本地事务 的原子性,所以,事务消息只保证了分布式事务的一半。但是即使这样,对于复杂的分布式事务, RocketMQ提供的事务消息也是目前业内最佳的降级方案。

事务消息是在分布式系统中保证最终一致性的两 阶段提交的消息实现。他可以保证本地事务执行与消息发送两个操作的原子性,也就是这两个操作一起 成功或者一起失败。

事务消息只保证消息发送者的本地事务与发消息这两个操 作的原子性,因此,事务消息的示例只涉及到消息发送者,对于消息消费者来说,并没有什么特别的。

public class TransactionListenerImpl implements TransactionListener {
    private AtomicInteger transactionIndex = new AtomicInteger(0);

    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

    //在提交完事务消息后执行。 
    //返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。 
    //返回ROLLBACK_MESSAGE状态的消息会被丢弃。 
    //返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {

        String tags = msg.getTags();
        if(StringUtils.contains(tags,"TagA")){
            return LocalTransactionState.COMMIT_MESSAGE;
        }else if(StringUtils.contains(tags,"TagB")){
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }else{
            return LocalTransactionState.UNKNOW;
        }
    }

    //在对UNKNOWN状态的消息进行状态回查时执行。返回的结果是一样的。
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        String tags = msg.getTags();
        //TagC的消息过一段时间会被消费者消费到 
        if(StringUtils.contains(tags,"TagC")){ 
            return LocalTransactionState.COMMIT_MESSAGE; 
            //TagD的消息也会在状态回查时被丢弃掉 
        }else if(
            StringUtils.contains(tags,"TagD")){ 
            return LocalTransactionState.ROLLBACK_MESSAGE; 
            //剩下TagE的消息会在多次状态回查后最终丢弃
        }else{
            return LocalTransactionState.UNKNOW;
        }
    }
}

注意事项

1、事务消息不支持延迟消息和批量消息。

2、为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax 参数来修改此限制。如果已经检 查某条消息超过 N 次的话( N = transactionCheckMax )则 Broker 将丢弃此消息,并在默认情况下 同时打印错误日志。用户可以通过重写 AbstractTransactionCheckListener 类来修改这个行为。

3、事务消息将在 Broker 配置文件中的参数 transactionMsgTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变 这个限制,该参数优先于 transactionMsgTimeout 参数。

4、事务性消息可能不止一次被检查或消费。

5、提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。

6、事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息 允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。

事务消息--发消息不丢失

先把要发的发出去,但是不放在业务topic,而是在半消息的topic

1627709282366-4a7778a0-fa14-46fe-bd31-00484aa93c85.png

1626939791726-bd482485-6d2a-4f32-9716-b14b48f02b73.png

发送half消息到MQ去,试探一下MQ是否正常

第一件事,不是先让订单系统做一些增删改操作,而是先发一个half消息给MQ以及收到他的成功的响应,初步 先跟MQ做个联系和沟通

半消息作用:确认一下MQ还活着

这个half消息本质就是一个订单支付成功 的消息,只不过你可以理解为他这个消息的状态是half状态,这个时候消费系统是看不见这个half消息的。然后消费者去等待接收这个half消息写入成功的响应通知

万一要是half消息写入失败了呢?

  1. 订单系统就应该执行一系列的回滚操作,比如对订单状态做一个更新,让状态变成“关闭交易”,同时通知支付系统自动进行退款,这才是正确的做法。
  2. 可以认为MQ的服务是有问题的,这时,就不能通知下游服务了。我们可以在下单时给订单一个状态标记,然后等待MQ服务正常后 再进行补偿操作,等MQ服务正常后重新下单通知下游服务。

half消息成功之后呢?

生产者完成自己的本地事务

这个时候你的订单系统就应该在自己本地的数据库里执行一些增删改操作了,因为一旦half消息写成功了,就说明MQ肯定已经收到这条消息了,MQ还活着,而且目前你是可以跟MQ正常沟通的。

本地事务执行失败了怎么办?

  1. 发送一个rollback请求给MQ就可以了。请求给MQ删除那个half消息之后,你的订单系统就必须走后续的回退流程了,就是通知支付系统退款。
    1. 当然这里可能还有一些订单系统自己的高可用降级的机制需要考虑,比如数据库无法更新了,此时你可能需要在机器本地磁盘文件里写 入订单支付失败的记录。然后你可以开一个后台线程在MySQL数据库恢复之后 ,再把订单状态更新为“已关闭”。
  2. RocketMQ返回一个UNKNOWN状态,写数据库失败(可能是数据库崩了,需要等一段时间才能恢复)。那我们可以另外找个地方 把订单消息先缓存起来(Redis、文本或者其他方式)。这样 RocketMQ就会过一段时间来回查事务状态。我们就可以在回查事务状态时再尝试把订单数据写入数据 库,如果数据库这时候已经恢复了,那就能完整正常的下单,再继续后面的业务。这样这个订单的消息 就不会因为数据库临时崩了而丢失。

本地事务完成之后,接着干什么?

如果订单系统成功完成了本地的事务操作,比如把订单状态都更新为“已完成”了,此时你就可以发送一个commit请求给MQ,要求 让MQ对之前的half消息进行commit操作,让红包系统可以看见这个订单支付成功消息

half消息写入成功后RocketMQ挂了怎么办?

没事,等待重启之后回查,因为已经写入成功了(commitLog文件)

在事务消息的处理机制中,未知状态的事务状态回查是由RocketMQ的Broker主动发 起的。也就是说如果出现了这种情况,那RocketMQ就不会回调到事务消息中回查事务状态的服务。这时,我们就可以将订单一直标记为"新下单"的状态。而等RocketMQ恢复后,只要存储的消息没有丢 失,RocketMQ就会再次继续状态回查的流程。

如果发送half消息成功了,但是没收到响应呢?

这个时候订单系统会误以为是发送half消息到MQ 失败了,订单系统就直接会执行退款流程了,订单状态也会标记为“已关闭”。

RocketMQ这里有一个补偿流程,他会去扫描自己处于half状态的消息,如果我们一直没有对这个消息执行commit/rollback操 作,超过了一定的时间,他就会回调你的订单系统的一个接口,问问你:这个消息到底怎么回事?你到底是打算commit这个消息还是要rollback这个消息?

这个时候我们的订单 系统就得去查一下数据库,看看这个订单当前的状态,一下发现订单状态是“已关闭”,此时就知道,你必然得发送rollback请求给 MQ去删除之前那个half消息了!

如果rollback或者commit发送失败了呢?

MQ里的消息一直是half状态,所以说他过了一定的超时时间会发现这个half消息有问题,他会回调你的 订单系统的接口

本质这个MQ的回调就是一个补偿机制,如果你的half消息响应没收到,或者rollback、commit请求没发送成功,他都会来找你问问对 half消息后续如何处理。

再假设一种场景,如果订单系统收到了half消息写入成功的响应了,同时尝试对自己的数据库更新了,然后根据失败或者成功去执行了 rollback或者commit请求,发送给MQ了。很不巧,mq在这个时候挂掉了,导致rollback或者commit请求发送失败,怎么办?如果是这种情况的话,那就等mq自己重启了,重启之后他会扫描half消息,然后还是通过上面说到的补偿机制,去回调你的接口

half的作用总结

  • MQ有问题或者网络有问题,half消息根本都发不出去,此时half消息肯定是失败的,那么订单系统就不会执行 后续流程了!
  • 如果要是half消息发送出去了,但是half消息的响应都没收到,然后执行了退款流程,那MQ会有补偿机制来回调找你询问要commit还 是rollback,此时你选择rollback删除消息就可以了,不会执行后续流程!
  • 如果要是订单系统收到half消息了,结果订单系统自己更新数据库失败了,那么他也会进行回滚,不会执行后续流程了!
  • 如果要是订单系统收到half消息了,然后还更新自己数据库成功了,订单状态是“已完成”了,此时就必然会发送commit请求给MQ, 一旦消息commit了,那么必然保证红包系统可以收到这个消息!
  • 而且即使你commit请求发送失败了,MQ也会有补偿机制,回调你接口让你判断是否重新发送commit请求

总之,就是你的订单系统只要成功了,那么必然要保证MQ里的消息是commit了可以让红包系统看到他!

使用事务消息的回查机制替换延迟消息

在订单场景下,通常会要求下单完成后,客户在一定时间内,例如10分钟,内完成订单支付,支付完成 后才会通知下游服务进行进一步的营销补偿。

如果不用事务消息,那通常会怎么办?

最简单的方式是启动一个定时任务,每隔一段时间扫描订单表,比对未支付的订单的下单时间,将超过 时间的订单回收。这种方式显然是有很大问题的,需要定时扫描很庞大的一个订单信息,这对系统是个 不小的压力。

那更进一步的方案是什么呢?是不是就可以使用RocketMQ提供的延迟消息机制。往MQ发一个延迟1分 钟的消息,消费到这个消息后去检查订单的支付状态,如果订单已经支付,就往下游发送下单的通知。 而如果没有支付,就再发一个延迟1分钟的消息。最终在第十个消息时把订单回收。这个方案就不用对 全部的订单表进行扫描,而只需要每次处理一个单独的订单消息。

那如果使用上了事务消息呢?我们就可以用事务消息的状态回查机制来替代定时的任务。在下单时,给 Broker返回一个UNKNOWN的未知状态。而在状态回查的方法中去查询订单的支付状态。这样整个业 务逻辑就会简单很多。我们只需要配置RocketMQ中的事务消息回查次数(默认15次)和事务回查间隔时 间(messageDelayLevel),就可以更优雅的完成这个支付状态检查的需求。

(重要)事务消息机制的底层实现原理(三个topic:半消息+OP+业务topic)

先把要发的发出去,但是不放在业务topic,而是在半消息的topic

事务消息机制 本质都是基于CommitLog、ConsumeQueue这套存储机制来做的,只不过中间有一些Topic的变换,****half消息就是写入内部Topic的。

写入一个Topic,最终是定位到这个Topic的某个MessageQueue,然后定位到 一台Broker机器上去,然后写入的是Broker上的CommitLog文件,同时将消费索引写入MessageQueue对应的ConsumeQueue文件。

对于事务消息机制之下的half消息 ,不是写入你指定的 OrderPaySuccessTopic的ConsumeQueue的,他会把这条half消息写入到自己内部的“RMQ_SYS_TRANS_HALF_TOPIC”这个Topic对应的一个ConsumeQueue里去

所以你的红包系统自然无法从OrderPaySuccessTopic的ConsumeQueue中看到这条half消息了

1626939580770-ed34eda5-9722-4dfa-9fdb-4cd5bbb4652a.png

在什么情况下订单系统会收到half消息成功的响应?

必须要half消息进入到RocketMQ内部的RMQ_SYS_TRANS_HALF_TOPIC的 ConsumeQueue文件了,此时就会认为half消息写入成功了,然后就会返回响应给订单系统。所以这个时候,一旦你的订单系统收到这个half消息写入成功的响应,必然就知道这个half消息已经在RocketMQ内部了。

假如因为各种问题,没有执行rollback或者commit会怎么样?

会在后台有定时任务,定时任务会去扫描RMQ_SYS_TRANS_HALF_TOPIC中的half消息,如果你超过一定时间还是 half消息,他会回调订单系统的接口,让你判断这个half消息是要rollback还是commit

1626939694918-7f6ff910-8718-4a2d-9839-65da271046fb.png

如果执行rollback操作的话,如何标记消息回滚?

RocketMQ都是顺序把消息写入磁盘文件的,所以在这里如果你执行rollback,他的本质就是用一个OP操作来标记half消息的状态。RocketMQ内部有一个OP_TOPIC,此时可以写一条rollback OP记录到这个Topic里,标记某个half消息是rollback了,最多就是回调15次,如果15次之后你都没法告知他half消息的状态,就自动把消息标记为rollback。

1626939917833-feea64d6-fa3d-48fb-bbb3-c2f79bf7637a.png

如果执行commit操作,如何让消息对红包系统可见?

执行commit操作之后,RocketMQ就会在OP_TOPIC里写入一条记录,标记half消息已经是commit状态了。

接着需要把放在RMQ_SYS_TRANS_HALF_TOPIC中的half消息给写入到OrderPaySuccessTopic的ConsumeQueue里去,然后我们的 红包系统可以就可以看到这条消息进行消费了,如下图。

1626939791726-bd482485-6d2a-4f32-9716-b14b48f02b73.png

RocketMQ集群架构的原理

架构原理

RocketMQ是如何集群化部署来承载高并发访问的?

假设RocketMQ部署在一台机器上,即使这台机器配置很高,但是一般来说一台机器也就是支撑10万+的并发访 问。部署在多台机器上,假设每台机器都能抗10万并发,然后你只要让几十万请求分散到多 台机器上就可以了,让每台机器承受的QPS不超过10万不就行了。

如果RocketMQ中要存储海量消息,如何实现分布式存储架构?

本质上RocketMQ存储海量消息的机制就是分布式的存储,就是把数据分散在多台机器上来存储,每台机器存储一部分消息,这样多台机器加起来就可以存储海量消息了!

高可用保障:万一Broker宕机了怎么办?

Broker主从架构以及多副本策略。

Broker是有Master和Slave两种角色的,Master Broker收到消息之后会同步给Slave Broker,这样Slave Broker上就能有一模一样的一份副本数据!这个时候如果任何一个Master Broker出现故障,还有一个Slave Broker上有一份数据副本,可以保证数据不丢失,还能继续对外提供 服务,保证了MQ的可靠性和高可用性

数据路由:怎么知道访问哪个Broker?

所有的Broker都会把自己注册到NameServer集群上去,要发送消息到Broker,会找NameServer去获取路由信息,就是集群里有哪些Broker等信息

如果系统要从Broker获取消息,也会找NameServer获取路由信息,去找到对应的Broker获取消息。

1624631627281-d8c16ab7-7819-4429-87f9-00305a94b9b7.png

NameServer 名字服务器:路由原理

名称服务充当路由消息的提供者。

Broker Server会在启动时向所有的Name Server注册自己的服务信 息,并且后续通过心跳请求的方式保证这个服务信息的实时性。生产者或消费者能够通过名字服务查找 各主题相应的Broker IP列表。多个Namesrv实例组成集群,但相互独立,没有信息交换。

这种特性也就意味着NameServer中任意的节点挂了,只要有一台服务节点正常,整个路由服务就不会 有影响。当然,这里不考虑节点的负载情况。

NameServer到底可以部署几台机器?

要管理Broker信息,别人都要通过他才知道跟哪个Broker通信,NameServer一定会多机器部署,实现一个集群,起到高可用的效果

Broker是把自己的信息注册到哪个NameServer上去?

每个Broker启动都得向所有的NameServer进行注册,每个NameServer都会有一份集群中所有Broker的信息。

系统如何从NameServer获取Broker信息?

自己主动去NameServer拉取Broker的路由信息 的。

每个系统自己每隔一段时间,定时发送请求到NameServer去拉取最新的集群Broker信息。

如果Broker挂了,NameServer是怎么感知到的?

靠的是Broker跟NameServer之间的心跳机制,Broker会每隔30s给所有的NameServer发送心跳,告诉每个 NameServer自己目前还活着。

每次NameServer收到一个Broker的心跳,就可以更新一下他的最近一次心跳的时间。然后NameServer会每隔10s运行一个任务,去检查一下各个Broker的最近一次心跳时间如果某个Broker超过120s都没发送心跳了, 那么就认为这个Broker已经挂掉了。

1624661114359-77c53cf7-54eb-4604-b8ee-afc005ac25d2.png

Broker挂了,系统(生产者)是怎么感知到的?

  • 可以考虑不发送消息到那台Broker,改成发到其他Broker上去。
  • 去slave通信

而且过一会儿,系统又会重新从NameServer拉取最新的路由信息了,此时就会知道有一个Broker已经宕机了。

NameServer的核心工作原理:最主要是知道他的集群化部署、Broker会注册到所有NameServer去、30s心跳机制和120s故障感知机制、生产者和消费者的客户 端容错机制,这些是最核心的。

Broker Server 代理服务器

存储消息、转发消息。

存储消息相关的元数据,包括消费者组、 消费进度偏移和主题和队列消息等。

多个重要的子模块:

  • Remoting Module:整个Broker的实体,负责处理来自clients端的请求。
  • Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
  • Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
  • HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
  • Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。

主从集群架构。

RocketMQ中有两种Broker架构模式:

  • 普通集群:这种集群模式下会给每个节点分配一个固定的角色,master负责响应客户端的请求,并存储消息。 slave则只负责对master的消息进行同步保存,并响应部分客户端的读请求。消息同步方式分为同步同 步和异步同步。 这种集群模式下各个节点的角色无法进行切换,也就是说,master节点挂了,这一组Broker就不可用 了。

  • Dledger高可用集群:Dledger是RocketMQ自4.5版本引入的实现高可用集群的一项技术。这个模式下的集群会随机选出一个节点作为master,而当master节点挂了后,会从slave中自动选出一个节点升级成为master。

    • Dledger技术做的事情:1、接管Broker的CommitLog消息存储 2、从集群中选举出master节点 3、完成master节点往slave节点的消息同步。
    • Dledger的关键部分是在他的节点选举上。Dledger是使用Raft算法来进行节点选举的。

Raft

Broker的主从架构原理

Master Broker是如何将消息同步给Slave Broker的?

为了保证MQ的数据不丢失而且具备一定的高可用性,Master需要在接收到消息之后,将数据同步给Slave,这样一旦Master Broker挂了,还有Slave上有一份数据。RocketMQ的Master-Slave模式采取的是Slave Broker不停的发送请求到Master Broker去拉取消息。RocketMQ自身的Master-Slave模式采取的是Pull模式拉取消息。

Slave Broker也会向所有的NameServer每30s发送心跳

RocketMQ 实现读写分离了吗?

写:在写入消息的时候,通常来说肯定是选择Master Broker去写入的,然后会同步给Slave Broker,那么其实本质上Slave Broker也应该有一 份一样的数据。

读:消费者的系统在获取消息的时候,有可能从Master Broker获取消息,也有可能从Slave Broker获取消息。作为消费者的系统在获取消息的时候会先发送请求到Master Broker上去,请求获取一批消息,此时Master Broker是会返回一批消息 给消费者系统的。然后Master Broker在返回消息给消费者系统的时候,会根据当时Master Broker的负载情况和Slave Broker的同步情况,向消费者系统建议下一次拉取消息的时候是从Master Broker拉取还是从Slave Broker拉取。

如果Slave Broke挂掉了有什么影响?

有一点影响,但是影响不太大。只是消息的消费必须去master,不管master压力大不大

因为消息写入全部是发送到Master Broker的,然后消息获取也可以走Master Broker,只不过有一些消息获取可能是从Slave Broker 去走的。所以如果Slave Broker挂了,那么此时无论消息写入还是消息拉取,还是可以继续从Master Broke去走,对整体运行不影响。只不过少了Slave Broker,会导致所有读写压力都集中在Master Broker上。

如果Master Broker挂掉了该怎么办?

这个时候就对消息的写入和获取都有一定的影响了。有可能只是部分数据同步

此时RocketMQ可以实现直接自动将Slave Broker切换为Master Broker吗?

不能,在RocketMQ 4.5版本之前,都是用Slave Broker同步数据,尽量保证数据不丢失,但是一旦Master故障了,Slave是没法自动切换成 Master的。

所以在这种情况下,如果Master Broker宕机了,这时就得手动做一些运维操作,把Slave Broker重新修改一些配置,重启机器给调整 为Master Broker,这是有点麻烦的,而且会导致中间一段时间不可用。

所以这种Master-Slave模式不是彻底的高可用模式,他没法实现自动把Slave切换为Master

1624662660836-7d54b5e2-278b-47da-a248-edc230eb80ca.png

消息主从复制

  • 同步复制:同步复制是等Master和Slave都写入消息成功后才反馈给客户端写入成功的状态。
    • 如果Master节点故障,Slave上有全部的数据备份,这样容易恢复数据。但是同步复制 会增大数据写入的延迟,降低系统的吞吐量。
  • 异步复制: 异步复制是只要master写入消息成功,就反馈给客户端写入成功的状态。然后再异步的将消息复制给 Slave节点。
    • 在异步复制下,系统拥有较低的延迟和较高的吞吐量。但是如果master节点故障,而有些数据没有完成 复制,就会造成数据丢失。

配置方式:消息复制方式是通过Broker配置文件里的brokerRole参数进行设置的,这个参数可以被设置成 ASYNC_MASTER、 SYNC_MASTER、SLAVE三个值中的一个。

基于Dledger(多副本)实现RocketMQ高可用自动切换以及主从同步原理

在RocketMQ 4.5之后,这种情况得到了改变,因为RocketMQ支持了一种新的机制,叫做Dledger.

是基于Raft协议实现的一个机制。把Dledger融入RocketMQ之后,就可以让一个Master Broker对应多个Slave Broker,

1624662790412-fb73840f-d09a-4986-b221-499d77766361.png

一旦Master Broker宕机了,就可以在多个副本,也就是多个Slave中,通过Dledger技术和Raft协议算法进行leader选举,直接将 一个Slave Broker选举为新的Master Broker,然后这个新的Master Broker就可以对外提供服务了。整个过程也许只要10秒或者几十秒的时间就可以完成

Broker高可用架构 :多副本同步+Leader自动切换

Broker接收到数据写入之后,同步给其他的Broker做多副本冗余的。一条数据就会在三个Broker上有三份副本,此时如果Leader Broker宕机,那么就直接让其他的Follower Broker自动切换 为新的Leader Broker,继续接受客户端的数据写入就可以了。

基于DLedger技术替换Broker的CommitLog

DLedger技术实际上首先他自己就有一个CommitLog机制,你把数据交给他,他会写入CommitLog磁盘文件里去。如果基于DLedger技术来实现Broker高可用架构,实际上就是用DLedger先替换掉原来Broker 自己管理的CommitLog,由DLedger来管理CommitLog,然后Broker还是可以基于DLedger管理的CommitLog 去构建出来机器上的各个ConsumeQueue磁盘文件。

1626921674830-9b032f46-303d-4cdb-94a8-a5e50d2b0309.png

Broker集群启动时,DLedger是如何基于Raft协议选举Leader Broker的?

首先基于DLedger替换各个Broker上的CommitLog管理组件了,那么就是每个Broker上都有一个DLedger组件了

DLedger是基于Raft协议来进行Leader Broker选举的,发起一轮一轮的投票,通过三台机器互相投票选出来一个人作为Leader。三台Broker机器启动的时候,他们都会投票自己作为Leader,然后把这个投票发送给其他Broker。似乎每个人都很自私,都在投票给自己,所以第一轮选举是失败的。

接着每个人会进入一个随机时间的休眠,比如说Broker01休眠3秒,Broker02休眠5秒,Broker03休眠4秒。此时Broker01必然是先苏醒过来的,他苏醒过来之后,直接会继续尝试投票给自己,并且发送自己的选票给别人。接着Broker03休眠4秒后苏醒过来,他发现Broker01已经发送来了一个选票是投给Broker01自己的,此时他自己因为没投票,所以会 尊重别人的选择,就直接把票投给Broker01了,同时把自己的投票发送给别人。

接着Broker02苏醒了,他收到了Broker01投票给Broker01自己,收到了Broker03也投票给了Broker01,那么他此时自己是没投票 的,直接就会尊重别人的选择,直接就投票给Broker01,并且把自己的投票发送给别人。

此时所有人都会收到三张投票,都是投给Broker01的,那么Broker01就会当选为Leader。

其实只要有(3台机器 / 2) + 1个人投票给某个人,就会选举他当Leader,这个(机器数量 / 2) + 1就是大多数的意思。

这就是Raft协议中选举leader算法的简单描述,简单来说,他确保有人可以成为Leader的核心机制就是一轮选举不出来Leader的话, 就让大家随机休眠一下,先苏醒过来的人会投票给自己,其他人苏醒过后发现自己收到选票了,就会直接投票给那个人。

依靠这个随机休眠的机制,基本上几轮投票过后,一般都是可以快速选举出来一个Leader。

只有Leader可以接收数据写入,Follower只能接收Leader同步过来的数据。

Leader Broker写入之后,DLedger是如何基于Raft协议进行多副本同步的?

基于DLedger技术和Raft协议同步给Follower Broker。

数据同步会分为两个阶段,一个是uncommitted阶段,一个是commited阶段

1626921840912-00ef0288-b67d-40fd-a889-87a8c3b41b08.png

首先Leader Broker上的DLedger收到一条数据之后,会标记为uncommitted状态,然后他会通过自己的DLedgerServer组件把这个 uncommitted数据发送给Follower Broker的DLedgerServer。

接着Follower Broker的DLedgerServer收到uncommitted消息之后,必须返回一个ack给Leader Broker的DLedgerServer,然后****如果Leader Broker收到超过半数的Follower Broker返回ack之后,就会将消息标记为committed状态。

然后Leader Broker上的DLedgerServer就会发送commited消息给Follower Broker机器的DLedgerServer,让他们也把消息标记为 comitted状态。

这个就是基于Raft协议实现的两阶段完成的数据同步机制。

如果Leader Broker崩溃了怎么办?

高可用Broker架构而言,无论是CommitLog写入,还是多副本同步,都是基于DLedger来实现的。

如果Leader Broker挂了,此时剩下的两个Follower Broker就会重新发起选举,他们会基于DLedger采用Raft协议的算法,去选举出来一个新的Leader Broker继续对外提供服务,而且会对没有完成的数据同步进行一些恢复性的操作,保证数据不会丢失。

MQ的核心数据模型:Topic到底是什么--包含某一类信息的数据集合

Topic其实就是一个数据集合的意思,不同类型的数据你得放不同的Topic里去。

消费的时候需要什么数据就要去对应的mq消费。

系统如果要往MQ里写入消息或者获取消息,首先得创建一些Topic,作为数据集合存放不同类型的消息,比如说 订单Topic,商品Topic,等等。

Topic作为一个数据集合是怎么在Broker集群里存储的?

分布式存储

可以在创建Topic的时候指定让他里面的数据分散存储在多台Broker机器上,比如一个Topic里有1000万条数据,此时有2台 Broker,那么就可以让每台Broker上都放500万条数据。

每个Broke在进行定时的心跳汇报给NameServer的时候,都会告诉NameServer自己当前的数据情况, 比如有哪些Topic的哪些数据在自己这里,这些信息都是属于路由信息的一部分。

生产者系统是如何将消息发送给Broker的?

知道你要发送的Topic,那么就可以跟NameServer建立一个TCP长连接,然后定时从他那里拉取到最新的路由信息,包括 集群里有哪些Broker,集群里有哪些Topic,每个Topic都存储在哪些Broker上。

生产者系统自然就可以通过路由信息找到自己要投递消息的Topic分布在哪几台Broker上,此时可以根据负载均衡算法,从里面选 择一台Broke机器出来,比如round robine轮询算法,或者是hash算法,都可以。

选择一台Broker之后,就可以跟那个Broker也建立一个TCP长连接,然后通过长连接向Broker发送消息即可.

这里唯一要注意的一点,就是生产者一定是投递消息到Master Broker的,然后Master Broker会同步数据给他的Slave Brokers,实现 一份数据多份副本,保证Master故障的时候数据不丢失,而且可以自动把Slave切换为Master提供服务。

消费者是如何从Broker上拉取消息的?

跟NameServer建立长连接,然后拉取路由信息,接着找到自己要获取消息的 Topic在哪几台Broker上,就可以跟Broker建立长连接,从里面拉取消息了。

MQ中间件的底层原理

负载均衡

producer负载均衡

  • Producer发送消息时,默认会轮询目标Topic下的所有MessageQueue,并采用递增取模的方式往不同的MessageQueue上发送消息,以达到让消息平均落在不同的queue上的目的。而由于 MessageQueue是分布在不同的Broker上的,所以消息也会发送到不同的broker上。

  • 同时生产者在发送消息时,可以指定一个MessageQueueSelector。通过这个对象来将消息发送到自己 指定的MessageQueue上。这样可以保证消息局部有序。

consumer负载均衡

Consumer也是以MessageQueue为单位来进行负载均衡。分为集群模式和广播模式。

集群模式

在集群消费模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个实例即可。

RocketMQ采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue。

而每当消费者实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照queue的数量和实例的数量平均分配queue给每个实例。每次分配时,都会将MessageQueue和消费者ID进行排序后,再用不同的分配算法进行分配。内置的分 配的算法共有六种,分别对应AllocateMessageQueueStrategy下的六种实现类,可以在consumer中 直接set来指定。默认情况下使用的是最简单的平均分配策略。

  • AllocateMachineRoomNearby: 将同机房的Consumer和Broker优先分配在一起。 这个策略可以通过一个machineRoomResolver对象来定制Consumer和Broker的机房解析规则。然后还需要引入另外一个分配策略来对同机房的Broker和Consumer进行分配。一般也就用简单的平均分 配策略或者轮询分配策略。

    • 源码中有测试代码AllocateMachineRoomNearByTest。在示例中:Broker的机房指定方式: messageQueue.getBrokerName().split("-")[0],而Consumer 的机房指定方式:clientID.split("-")[0]
    • clinetID的构建方式:见ClientConfig.buildMQClientId方法。按他的测试代码应该是要把clientIP 指定为IDC1-CID-0这样的形式。
  • AllocateMessageQueueAveragely:平均分配。将所有MessageQueue平均分给每一个消费者

  • AllocateMessageQueueAveragelyByCircle: 轮询分配。轮流的给一个消费者分配一个 MessageQueue。

  • AllocateMessageQueueByConfig: 不分配,直接指定一个messageQueue列表。类似于广播模 式,直接指定所有队列。

  • AllocateMessageQueueByMachineRoom:按逻辑机房的概念进行分配。又是对BrokerName和 ConsumerIdc有定制化的配置。

  • AllocateMessageQueueConsistentHash。源码中有测试代码

    • AllocateMessageQueueConsitentHashTest。这个一致性哈希策略只需要指定一个虚拟节点数, 是用的一个哈希环的算法,虚拟节点是为了让Hash数据在换上分布更为均匀。

广播模式

广播模式下,每一条消息都会投递给订阅了Topic的所有消费者实例,所以也就没有消息分配这一说。 而在实现上,就是在Consumer分配Queue时,所有Consumer都分到所有的Queue。

生产者到底如何发送消息的?

创建Topic的时候为何要指定MessageQueue数量?

数据分片机制

创建Topic的时候需要指定一个很关键的参数,就是MessageQueue。Topic对应了多少个队列,MessageQueue就是RocketMQ中非常关键的一个数据分片机制,在每个Broker机器上都存储一些MessageQueue。通过这个方法,就可以实现Topic数据的分布式存储!

1626919537916-30927523-7123-488f-a243-1496180d02eb.png

生产者发送消息的时候写入哪个MessageQueue?

有一个Topic,我们为他指定创建了4个MessageQueue。

假设你的Topic有1万条数据, 大致可以认为会在每个MessageQueue中放 入2500条数据,有可能有的MessageQueue的数据多,有的数据少,这个要根据你的消息写入MessageQueue的策略来定。

生产者从NameServer中就会知道,一个Topic有几个MessageQueue,哪些MessageQueue在哪台Broker机器上,暂时先认为生产者会均匀的把消息写入各个MessageQueue,就是比如这个生产者发送出去了20条数据,那么4个 MessageQueue就是每个都会写入5条数据。

1626919797270-1bb5341c-67a7-4990-bb33-d4c9c8661e1d.png

如果某个Broker出现故障该怎么办?

此时正在等待的其他Slave Broker自动热切换为 Master Broker,那么这个时候对这一组Broker就没有Master Broker可以写入了

通常来说建议大家在Producer中开启一个开关,就是sendLatencyFaultEnable。会有一个自动容错机制,比如如果某次访问一个Broker发现网络延迟有500ms,然后还无法访问,那么 就会自动回避访问这个Broker一段时间,比如接下来3000ms内,就不会访问这个Broker了。这样的话,就可以避免一个Broker故障之后,短时间内生产者频繁的发送消息到这个故障的Broker上去,出现较多次数的异常。

消费者是如何获取消息处理以及进行ACK的?

消费组到底是个什么概念?一条消息在多个消费组中是如何分配的?

每个组都会收到同一个topic的消息

1626922654035-ce052f6f-0c94-4504-86d6-a80be4324452.png

比如库存系统部署了4台机器,每台机器上的消费者组的名字都是“stock_consumer_group”,那么这4台机器就同属于一个消费者组。

不同的系统应该设置不同的消费组,如果不同的消费组订阅了同一个Topic,对Topic里的一条消息,每 个消费组都会获取到这条消息。

假设库存系统和营销系统作为两个消费者组,都订阅了“TopicOrderPaySuccess”这 个订单支付成功消息的Topic,此时假设订单系统作为生产者发送了一条消息到这个Topic,正常情况下来说,这条消息进入Broker之后,库存系统和营销系统作为两个消费组,每个组都会拉取到这条消息。

在一个消费组内部的消费模式:集群模式消费 vs 广播模式消费

  • 默认情况下都是集群模式,一个消费组获取到一条消息,只会交给组内的一台机器去处理,不是每台机器都可以获取到 这条消息的。
  • 可以通过如下设置来改变为广播模式:consumer.setMessageModel(MessageModel.BROADCASTING); 消费组获取到的一条消息,组内每台机器都可以获取到这条消息。

Topic、MessageQueue、CommitLog、ConsumeQueue、ConsumeQueueGroup

但是对于一个Broker机器而言,存储在他上面的所有Topic以及MessageQueue的消息数据都是写入一个统一的CommitLog的,对于Topic的各个MessageQueue而言,就是通过各个ConsumeQueue文件来存储属于MessageQueue 的消息在CommitLog文 件中的物理地址,就是一个offset偏移量。

一个Topic在创建的时候我们是要设置他有多少个MessageQueue的,Topic中的多个MessageQueue会分散在多个Broker上,在每个Broker机器上,一 个MessageQueue就对应了一个ConsumeQueue,当然在物理磁盘上其实是对应了多个ConsumeQueue文件的。

1626922954041-422b5a3a-76dc-4d3d-be9e-b76e2f034ef3.png

对于一个Topic上的多个MessageQueue,是如何由一个消费组中的多台机器来进行消费的呢?

其实这里的源码实现细节是较为复杂的,但我们可以简单的理解为,他会均匀的将MessageQueue分配给消费组的多台机器来消费。举个例子,假设我们的“TopicOrderPaySuccess”有4个MessageQueue,这4个MessageQueue分布在两个Master Broker上,每 个Master Broker上有2个MessageQueue。

然后库存系统作为一个消费组里有两台机器,那么正常情况下,当然最好的就是让这两台机器每个都负责2个MessageQueue的消费了

比如库存系统的机器01从Master Broker01上消费2个MessageQueue,然后库存系统的机器02从Master Broker02上消费2个 MessageQueue,这样不就把消费的负载均摊到两台Master Broker上去了?

1626923650601-b800bead-4bf2-4d56-b1c8-276bf2fc83b0.png

一个Topic的多个MessageQueue会均匀分摊给消费组内的多个机器去消费,这里的一个原则就是,一个 MessageQueue只能被一个消费机器去处理,但是一台消费者机器可以负责多个MessageQueue的消息处理。

broker是如何基于ConsumeQueue和CommitLog,读取消息返回给消费者机器的?

假设一个消费者机器发送了拉取请求到Broker了,他说我这次要拉取MessageQueue0中的消息,然后我之前都没拉取过消息,所以就 从这个MessageQueue0中的第一条消息开始拉取好了。于是,Broker就会找到MessageQueue0对应的ConsumeQueue0,从里面找到第一条消息的offset。

接着Broker就需要根据ConsumeQueue0中找到的第一条消息的地址,去CommitLog中根据这个offset地址去读取出来这条消息的数据,然后把这条消息的数据返回给消费者机器

消费消息的时候,本质就是根据你要消费的MessageQueue以及开始消费的位置,去找到对应的ConsumeQueue读取里面对应位置的消息在CommitLog中的物理offset偏移量,然后到CommitLog中根据offset读取消息数据,返回给消费者机器。

消费者机器如何处理消息、进行ACK以及提交消费进度?

1626924333835-9659d1f9-544f-43a6-91dc-83594781e919.png

接着消费者机器拉取到一批消息之后,就会将这批消息回调我们注册的一个函数,当我们处理完这批消息之后,消费者机器就会提交我们目前的一个消费进度到Broker上去,然后Broker就会存储我们的消费进度。

比如我们现在对ConsumeQueue0的消费进度假设就是在offset=1的位置,那么他会记录下来一个ConsumeOffset的东西去标记我们的消费进度,那么下次这个消费组只要再次拉取这个ConsumeQueue的消息,就可以从Broker记录的消费位置开始继续拉取,不用重头开始拉取 了。

1626924398904-49a109ef-125c-4d91-a37e-f88259d38788.png

如果消费组中出现机器宕机或者扩容加机器,会怎么处理?

这个时候其实会进入一个rabalance的环节,也就是说重新给各个消费机器分配他们要处理的MessageQueue

机器01负责MessageQueue0和Message1,机器02负责MessageQueue2和MessageQueue3,现在机器 02宕机了,那么机器01就会接管机器02之前负责的MessageQueue2和MessageQueue3。

或者如果此时消费组加入了一台机器03,此时就可以把机器02之前负责的MessageQueue3转移给机器03,然后机器01就仅仅负责一个MessageQueue2的消费了,这就是_负载重平衡_的概念。

消费者到底是根据什么策略从Master或Slave上拉取消息的?

刚开始消费者都是连接到Master Broker机器去拉取消息的,然后如果Master Broker机器觉得自己负载比较高,就会 告诉消费者机器,下次可以从Slave Broker机器去拉取。

写入CommitLog时先进入os cache缓存,而不是直接进入磁盘的机制,就可以实现broker写CommitLog文件的性能是 内存写级别的,这才能实现broker超高的消息接入吞吐量。

从os cache里读取ConsumeQueue的数据

拉取消息的时候必然会先读取ConsumeQueue文件,ConsumeQueue会被大量的消费者发送的请求给高并发的读取,所以 ConsumeQueue文件的读操作是非常频繁的,而且同时会极大的影响到消费者进行消息拉取的性能和消费吞吐量。实际上broker对ConsumeQueue文件同样也是基于os cache来进行优化的,对于Broker机器的磁盘上的大量的ConsumeQueue文件,在写入的时候也都是优先进入os cache中的

os自己有一个优化机制,就是读取一个磁盘文件的时候,他会自动把磁盘文件的一些数据缓存到os cache中。

ConsumeQueue文件主要是存放消息的offset,所以每个文件很小,30万条消息的offset就只有5.72MB而已。ConsumeQueue文件几乎都是放在os cache里的。

所以实际上在消费者机器拉取消息的时候,第一步大量的频繁读取ConsumeQueue文件,几乎可以说就是跟读内存里的数据的性能是 一样的,通过这个就可以保证数据消费的高性能以及高吞吐

1626925679419-189c7086-0277-4c30-89bc-14317bcdad06.png

CommitLog是基于os cache+磁盘一起读取的

根据你读取到的offset去CommitLog里读取消息的完整数据了。CommitLog是用来存放消息的完整数据的,所以内容量是很大的,毕竟他一个文件就要1GB,所以整体完全有可能多达几个TB。

os cache对于CommitLog而言,主要是提升文件写入性能,当你不停的写入的时候,很多最新写入的数据都会先停留在os cache里,比如这可能有10GB~20GB的数据。 之后os会自动把cache里的比较旧的一些数据刷入磁盘里,腾出来空间给更新写入的数据放在os cache里,所以大部分数据可能多达几 个TB都是在磁盘上的。

第一种可能,如果你读取的是那种刚刚写入CommitLog的数据,那么大概率他们还停留在os cache中,此时你可以顺利的直接从os cache里读取CommitLog中的数据,这个就是内存读取,性能是很高的。

第二种可能,你也许读取的是比较早之前写入CommitLog的数据,那些数据早就被刷入磁盘了,已经不在os cache里了,那么此时你 就只能从磁盘上的文件里读取了,这个性能是比较差一些的。

什么时候会从os cache读?什么时候会从磁盘读?

如果你的消费者机器一直快速的在拉取和消费处理,紧紧的跟上了生产者写入broker的消息速率,那么你每次拉取几乎都是在拉取最近人家刚写入CommitLog的数据,那几乎都在os cache里。

但是如果broker的负载很高,导致你拉取消息的速度很慢,或者是你自己的消费者机器拉取到一批消息之后处理的时候性能很低,处理 的速度很慢,这都会导致你跟不上生产者写入的速率。

比如人家都写入10万条数据了,结果你才拉取了2万条数据,此时有5万条最新的数据是在os cache里,有3万条你还没拉取的数据是在 磁盘里,那么当后续你再拉取的时候,必然很大概率是从磁盘里读取早就刷入磁盘的3万条数据。接着之前在os cache里的5万条数据可能又被刷入磁盘了,取而代之的是更新的几万条数据在os cache里,然后你再次拉取的时候,又 会从磁盘里读取刷入磁盘里的5万条数据,相当于你每次都在从磁盘里读取数据了!

到底什么时候Master Broker会让你从Slave Broker拉取数据?

假设此时你的broker里已经写入了10万条数据,但是你仅仅拉取了2万条数据,下 次你拉取的时候,是从第2万零1条数据开始继续往后拉取的,也就是说,此时你有8万条数据是没有拉取的!

然后broker自己是知道机器上当前的整体物理内存有多大的,而且他也知道自己可用的最大空间占里面的比例,他是知道自己的消息最 多可以在内存里放多少的!比如他心知肚明,他最多也就在内存里放5万条消息而已!

因为他知道,他最多只能利用10GB的os cache去放消息,这么多内存最多也就放5万左右的消息。

然后这个时候你过来拉取消息,他发现你还有8万条消息没有拉取,这个8万条消息他发现是大于10GB内存最多存放的5万条消息的, 那么此时就说明,肯定有3万条消息目前是在磁盘上的,不在os cache内存里!

所以他经过上述判断,会发现此时你很大概率会从磁盘里加载3万条消息出来!他会认为,出现这种情况,很可能是因为自己作为 master broker负载太高了,导致没法及时的把消息给你,所以你落后的进度比较多。

这个时候,他就会告诉你,我这次给你从磁盘里读取3万条消息,但是下次你还是从slave broker去拉取吧!

如果你没拉取的消息超过了最大能使用的内存的量,那么说明你后续会频繁从磁盘加载数据,此时就让你从slave broker去加载数据了!

RocketMQ是如何基于Netty扩展出高性能网络通信架构的?

Reactor主线程与长短连接

作为Broker而言,他会有一个Reactor主线程。假设我们有一个Producer他现在想要跟Broker建立一个TCP长连接。

短连接:如果你要给别人发送一个请求,必须要建立连接 -> 发送请求 -> 接收响应 -> 断开连接,下一次你 要发送请求的时候,这个过程得重新来一遍)。

每次建立一个连接之后,使用这个连接发送请求的时间是很短的,很快就会断开这个连接,所以他存在时间太短了,就是短连接。

长连接的话,就是反过来的意思,你建立一个连接 -> 发送请求 -> 接收响应 -> 发送请求 -> 接收响应 -> 发送请求 -> 接收响应

建立好一个长连接之后,可以不停的发送请求和接收响应,连接不会断开,等你不需要的时候再断开就行了,这个连接会存在很长时间,所以是长连接。

TCP长连接

按照TCP协议建立的长连接。

Producer和Broker建立一个长连接

Broker上的这个Reactor主线程,他会在端口上监听到这个 Producer建立连接的请求,接着这个Reactor主线程就专门会负责跟这个Producer按照TCP协议规定的一系列步骤和规范,建立好一个长连接。

Producer里面会有一个SocketChannel,Broker里也会有一个SocketChannel,这两个SocketChannel就代表了他们俩建立好的这个 长连接。

基于Reactor线程池监听连接中的请求

还不能让Producer发送消息给Broker,因为虽然我们有一个SocketChannel组成的长连接,但是他仅仅是一 个长连接而已!Brocker 的 Reactor线程池,默认是3个线程!Reactor主线程建立好的每个连接SocketChannel,都会交给这个Reactor线程池里的其中一个线程去监听请求。

基于Worker线程池完成一系列准备工作

接着Reactor线程从SocketChannel中读取出来一个请求,这个请求在正式进行处理之前,必须就先要进行一些准备工作和预处理,比 如SSL加密验证、编码解码、连接空闲检查、网络连接管理,诸如此类的一些事

Worker线程池,他默认有8个线程

基于业务线程池完成请求的处理--因为netty不建议耗时处理在worker的pipeline里面执行,这里使用单独的线程执行

那么现在如果Worker线程完成了一系列的预处理之后,比如SSL加密验证、编码解码、连接空闲检查、网络连接管理,等等,接着就需 要对这个请求进行正式的业务处理了!

你接收到了消息,肯定是要写入CommitLog文件的,后续还有一些ConsumeQueue之类的事情需要处理,类似这种操作,就是业务 处理逻辑。这个时候,就得继续把经过一系列预处理之后的请求转交给业务线程池

比如对于处理发送消息请求而言,就会把请求转交给SendMessage线程池,SendMessage线程是可以配置的,你配置的越多,自然处理消息的吞吐量越高。

1626926645956-bdc67460-0c76-4da6-a9f7-0565a3a407ba.png

(重点)为什么这套网络通信框架会是高性能以及高并发的?

专门分配一个Reactor主线程出来,就是专门负责跟各种Producer、Consumer之类的建立长连接。

(bossGroup)一旦连接建立好之后,大量的长连接均匀的分配给Reactor线程池里的多个线程。每个Reactor线程负责监听一部分连接的请求,这个也是一个优化点,通过多线程并发的监听不同连接的请求,可以有效的提升大量并 发请求过来时候的处理能力,可以提升网络框架的并发能力。

(childGroup)大量并发过来的请求都是基于Worker线程池进行预处理的,当Worker线程池预处理多个请求的时候,Reactor线程还是可以有条不紊的继续监听和接收大量连接的请求是否到达。

(业务处理)最终的读写磁盘文件之类的操作都是交给业务线程池来处理的,当他并发执行多个请求的磁盘读写操作的时候,不影响其他线程池同时接收请求、预处理请求,没任何的影响。

所以最终的效果就是:

3个建立长连接 Reactor线程池并发的监听多个连接的请求是否到达 ,

8个Worker请求并发的对多个请求进行预处理,

业务线程池并发的对多个请求进行磁盘读写业务操作

这些事情全部是利用不同的线程池并发执行的!任何一个环节在执行的时候,都不会影响其他线程池在其他环节进行请求的处理!

这样的一套网络通信架构,最终实现的效果就是可以高并发、高吞吐的对大量网络连接发送过来的大量请求进行处理,这是保证Broker 实现高吞吐的一个非常关键的环节,就是这套网络通信架构。

Broker是如何持久化存储消息的?

为什么Broker数据存储是最重要的一个环节?

不只是让你写入消息和获取消息那么简单,他们本身最重要的就是提供强大的数据存储能力

决定了生产者消息写入的吞吐量,决定了消息不能丢 失,决定了消费者获取消息的吞吐量,这些都是由他决定的。

(重点)消息的存储分为三个部分:

1627723720749-4f37c989-8e28-4b86-a85f-263fe7019bd3.png

  • CommitLog:存储消息的元数据。所有消息都会顺序存入到CommitLog文件当中。CommitLog 由多个文件组成,每个文件固定大小1G。以第一条消息的偏移量为文件名。
  • ConsumerQueue:存储消息在CommitLog的索引。一个MessageQueue一个文件,记录当前 MessageQueue被哪些消费者组消费到了哪一条CommitLog
  • IndexFile:为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来 查找消息的方法不影响发送与消费消息的主流程

其余文件

  • abort:这个文件是 RocketMQ 用来判断程序是否正常关闭的一个标识文件。正常情况下,会在启动时创建,而关闭服务时删除。但是如果遇到一些服务器宕机,或者kill -9这样一些非正常关闭服 务的情况,这个abort文件就不会删除,因此RocketMQ就可以判断上一次服务是非正常关闭的, 后续就会做一些数据恢复的操作。

  • checkpoint:数据存盘检查点

  • config/*.json:这些文件是将RocketMQ的一些关键配置信息进行存盘保存。例如Topic配置、消 费者组配置、消费者组消息偏移量Offset 等等一些信息。

CommitLog消息顺序写入机制

他接收到了一条消息,会把这个消息直接写入磁盘上的一个日志文件,叫做CommitLog,直接顺序写入这个文件。CommitLog是很多磁盘文件,每个文件限定最多1GB,Broker收到消息之后就直接追加写入这个文件的末尾, 如果一个CommitLog写满了1GB,就会创建一个新的CommitLog文件。

1626920556300-585f8dd5-b7f5-4351-9e02-3add1abee734.png

MessageQueue在数据存储中是体现在ConsumeQueue指向的物理位置

MessageQueue对应的ConsumeQueue物理位置存储机制 : ConsumeQueue会存储Topic下各个MessageQueue的消息的物理位置。

在Broker中,对Topic下的每个MessageQueue都会有一系列的ConsumeQueue文件。会有下面这种格式的一系列文件:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}

1626920768992-d999fb8a-e4f0-4114-aa05-6c2376ed1433.png

对存储在这台Broker机器上的Topic下的一个MessageQueue,他有很多的ConsumeQueue文件,这个ConsumeQueue文件里 存储的是一条消息对应在CommitLog文件中的offset偏移量。(ConsumeQueue中的一个物理位置其实是对CommitLog文件中一个消息的引用。

ConsumeQueue中存储的每条数据不只是消息在CommitLog中的offset偏移量,还包含了消息的长度,以及tag hashcode,一条数据是20个字节,每个ConsumeQueue文件保存30万条数据,大概每个文件是5.72MB。

有4个MessageQueue,然后在两台Broker机器上,每台Broker机器会存储两个MessageQueue。

那么此时假设生产者选择对其中一个MessageQueue写入了一条消息,此时消息会发送到Broker上。

ConsumeQueue0和ConsumeQueue1,他们分别对应着 Topic里的MessageQueue0和MessageQueue1。

假设Queue的名字叫做:TopicOrderPaySuccess,那么此时在Broker磁盘上应该有如下两个路径的文件:

$HOME/store/consumequeue/TopicOrderPaySuccess/MessageQueue0/ConsumeQueue0磁盘文件

$HOME/store/consumequeue/TopicOrderPaySuccess/MessageQueue1/ConsumeQueue1磁盘文件

Broker收到一条消息写入了CommitLog之后,其实他同时会将这条消息在CommitLog中的物理位置,也就是一个文件偏移量,就是一个offset,写入到这条消息所属的MessageQueue对应的ConsumeQueue文件中去。

如何让消息写入CommitLog文件近乎内存写性能的?

基于CommitLog顺序写+OS Cache+异步刷盘的高吞吐消息写入的机制

Broker是以顺序的方式将消息写入CommitLog磁盘文件的,也就是每次写入就是在文件末尾追加一条数据就可以了,对文件进行 顺序写的性能要比对文件随机写的性能提升很多

1626921183440-97e6ed93-7b00-40ba-a462-28f36d76799f.png

消息刷盘机制:同步刷盘与异步刷盘

刷盘方式是通过Broker配置文件里的flushDiskType 参数设置的,这个参数被配置成 SYNC_FLUSH、ASYNC_FLUSH中的 一个。

  • 高吞吐写入+丢失数据风险
  • 写入吞吐量下降+数据不丢失

异步刷盘模式下,生产者把消息发送给Broker,Broker将消息写入OS PageCache中,就直接返回ACK给生产者了。异步刷盘的的策略下,可以让消息写入吞吐量非常高,但是可能会有数据丢失的风险

同步刷盘模式的话,那么生产者发送一条消息出去,broker收到了消息,必须直接强制把这个 消息刷入底层的物理磁盘文件中,然后才会返回ack给producer,此时你才知道消息写入成功了。

1626921292975-f4e0db69-f6cc-4a14-91c0-293680c50543.png

Broker基于mmap内存映射实现磁盘文件的高性能读写(MappedByteBuffer)(不是零拷贝)

实际上在Broker读写磁盘的时候,是大量把mmap技术和pagecache技术结合起来使用的,通过mmap技术减少数据拷贝次数,然后 利用pagecache技术实现尽可能优先读写内存,而不是物理磁盘。

mmap:Broker读写磁盘文件的核心技术

Broker中就是大量的使用mmap 技术去实现CommitLog这种大磁盘文件的高性能读写优化的。

Broker对磁盘文件的写入主要是借助直接写入os cache来实现性能优化的,因为直接写入os cache,相当于就是写入内存一样的性能,后续等os内核中的线程异步把cache中的数据刷入磁盘文件即可。

传统文件IO操作的多次数据拷贝问题

  1. 从磁盘复制数据到内核态内存;

  2. 从内核态内存复 制到用户态内存;

  3. 然后从用户态 内存复制到网络驱动的内核态内存;

  4. 最后是从网络驱动的内核态内存复 制到网卡中进行传输。

1626936348754-324a2b70-3197-4ab0-abfb-cb4f9c3c94d9.png1626936500490-9af6dee1-f4a6-4ae5-9e60-ee7165fbd040.png1626936805536-bc50d2a1-6895-439f-b356-e7673997457e.png

(重点)RocketMQ是如何基于mmap技术+page cache技术优化的?

RocketMQ底层对CommitLog、ConsumeQueue之类的磁盘文件的读写操作,基本上都会采用mmap技术来实现。如果具体到代码层面,就是基于JDK NIO包下的MappedByteBuffer的map()函数,来先将一个磁盘文件(比如一个CommitLog文 件,或者是一个ConsumeQueue文件)映射到内存里来,刚开始建立映射的时候,并没有任何的数据拷贝操作,其实磁盘文件还是停留在那里,只不过他把物理上的磁盘文件的一些地址和用户进程私有空间的一些虚拟内存地址进行了一个映射

地址映射的过程,就是JDK NIO包下的MappedByteBuffer.map()函数干的事情,底层就是基于mmap技术实现的。采用MappedByteBuffer这种内存映射的方式有几个限制,一般有大小限制,在1.5GB~2GB之间,所以RocketMQ才让CommitLog单个文件在1GB,ConsumeQueue文件在5.72MB,不会太大。这样限制了RocketMQ底层文件的大小,就可以在进行文件读写的时候,很方便的进行内存映射了。

关于零拷贝,JAVA的NIO中提供了两种实现方式,mmap和sendfile,其中mmap适合比较小的文 件,而sendfile适合传递比较大的文件。

PageCache,实际上在这里就是对应于虚拟内存

要写入消息到CommitLog文件,你先把一个CommitLog文件 通过MappedByteBuffer的map()函数映射其地址到你的虚拟内存地址。就可以对这个MappedByteBuffer执行写入操作了,写入的时候他会直接进入PageCache中,然后过一段时间之后,由os的线程 异步刷入磁盘中

1626937086330-a78f186b-0a86-4d09-965b-1d0aabe60ce7.png

预映射机制 + 文件预热机制

(1)内存预映射机制:Broker会针对磁盘上的各种CommitLog、ConsumeQueue文件预先分配好MappedFile,也就是提前对一些 可能接下来要读写的磁盘文件,提前使用MappedByteBuffer执行map()函数完成映射,这样后续读写文件的时候,就可以直接执行 了。

(2)文件预热:在提前对一些文件完成映射之后,因为映射不会直接将数据加载到内存里来,那么后续在读取尤其是CommitLog、 ConsumeQueue的时候,其实有可能会频繁的从磁盘里加载数据到内存中去。所以其实在执行完map()函数之后,会进行madvise系统调用,就是提前尽可能多的把磁盘文件加载到内存里去。

通过上述优化,才真正能实现一个效果,就是写磁盘文件的时候都是进入PageCache的,保证写入高性能;同时尽可能多的通过map + madvise的映射后预热机制,把磁盘文件里的数据尽可能多的加载到PageCache里来,后续对CosumeQueue、CommitLog进行读 取的时候,才能尽可能从内存里读取数据。

MQ可能遇到的一些问题以及解决方案

消息丢失(可靠传输)

数据的丢失问题,可能出现在生产者传输的时候(网络)、MQ挂了、消费者挂了

rocketMQ

发送的时候使用事务机制发送

broker消息零丢失:使用同步刷盘的策略

consumer消费消息不丢失:手动提交加上自动故障转移

自动故障转移就是broker一直没有收到offset认为当前的consumer宕机, 就会转交给其他机器,

RabbitMQ

RabbitMQ生产者传输时数据丢失--事务(同步) or confirm(异步)机制

  • (不推荐)RabbitMQ 的事务功能:就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错此时就可以回滚事务**<font style="color:#F5222D;">channel.txRollback</font>**,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。事务机制基本上吞吐量会下来,因为太耗性能
// 开启事务
channel.txSelect
try {
    // 这里发送消息
} catch (Exception e) {
    channel.txRollback
    // 这里再次重发这条消息
}
// 提交事务
channel.txCommit
  • (推荐)confirm 模式:在生产者那里设置开启 <font style="color:#F5222D;">confirm</font> 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 <font style="color:#F5222D;">ack</font> 消息如果 RabbitMQ 没能处理这个消息,会回调你的一个 <font style="color:#F5222D;">nack</font> 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。

事务机制和 **<font style="color:#F5222D;">confirm</font>** 机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是 **<font style="color:#F5222D;">confirm</font>** 机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的一个接口通知你这个消息接收到了。所以一般在生产者这块避免数据丢失,都是用 **<font style="color:#F5222D;">confirm</font>** 机制的。

RabbitMQ 自己弄丢了数据--开启rabbitMQ持久化

设置持久化有两个步骤

  • 创建 queue 的时候将其设置为持久化这样就可以保证 RabbitMQ 持久化 queue 的元数据但是它是不会持久化 queue 里的数据的
  • 第二个是发送消息的时候将消息的 <font style="color:#F5222D;">deliveryMode</font> 设置为 2。就是将消息设置为持久化的,此时 RabbitMQ 就会将要发送的消息持久化到磁盘上去。

必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。

消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。所以,持久化可以跟生产者那边的 <font style="color:#F5222D;">confirm</font> 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 **<font style="color:#F5222D;">ack</font>**,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 <font style="color:#F5222D;">ack</font>,你也是可以自己重发的。

RabbitMQ 消费端弄丢了数据--关闭自动rabbitMQ ACK

关闭 RabbitMQ 的自动 **<font style="color:#F5222D;">ack</font>**,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 **<font style="color:#F5222D;">ack</font>**

kafka

Kafka消费端弄丢了数据--关闭自动提交offset

关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。

Kafka 弄丢了数据--同步到replica之后才返回

  • 给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
  • 在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
  • 在 producer 端设置 acks=all:这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了。否则生产者会自动重传。
  • 在 producer 端设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。

kafka 生产者会不会弄丢数据?

如果按照上述的思路设置了 acks=all,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。

为什么一定要使用事务消息方案?

能不能基于 同步发消息 + 反复重试 来确保消息到达MQ?

  1. 本地执行完突然宕机,MQ收不到
  2. 事务注解可以解决回滚,因为服务不可用时候没有commit,但是重试耗费性能,会阻塞导致其他服务回调超时
  3. 事务注解只能回滚数据库,会导致缓存中间件数据不一致。

在基于Kafka作为消息中间件的消息零丢失方案中,对于发送消息这块,因为Kafka本身不具备RocketMQ这种事务消息的高级功能,所以一般我们都是对Kafka会采用同步发消息 + 反复重试多次的方案,去保证消息成功投递到Kafka的。

实现:先执行订单本地事务,还是先发消息到MQ去?

如果我们先执行订单本地事务,接着再发送消息到MQ的话,如果订单本地事务执行失败了,则不会继续发送消息 到MQ了;如果订单事务执行成功了,发送MQ失败了,自动进行几次重试,重试如果一直失败,就回滚订单事务。

1626940283140-e6d25a02-8c57-4ffc-ae97-72372fe106af.png

但是这里有一个问题,假设你刚执行完成了订单本地事务了,结果还没等到你发送消息到MQ,结果你的订单系统突然崩溃了!这就导致你的订单状态可能已经修改为了“已完成”,但是消息却没发送到MQ去!这就是这个方案最大的隐患。

1626940313100-a9c3f090-d22a-4e28-a00b-e63b66d90e6d.png

把订单本地事务和重试发送MQ消息放到一个事务代码@Transactional中?如果你刚执行了orderService.finishOrderPay(),结果订单系统直接崩溃了,此时订单本地事务会回滚,因为没有commit。

但是对于这个方案,还是非常的不理想,原因就出在那个MQ多次重试的地方,可能会导致订单系统的这个接口性能很差,假设用户支付成功了,然后支付系统回调通知你的订单系统说,有一笔订单已经支付成功了,这个时候你的订单系统卡在多次重试MQ 的代码那里,可能耗时了好几秒种,此时回调通知你的系统早就等不及可能都超时异常了。

1626940370161-6cb38fbb-c48c-4ffb-ac72-c71271e85a3f.png

虽然在方法上加了事务注解,已 经完成了订单数据库更新、Redis缓存更新、ES数据更新了,结果没送MQ,订单系统崩溃了。虽然订单数据库的操作会回滚,但是Redis、Elasticsearch中的数据更新会自动回滚吗?

不会的,因为他们根本没法自动回滚,此时数据还是会不一致的。所以说,完全寄希望于本地事务自动回滚是不现实的。

1626940508880-d4e3f78e-a786-42d9-b85c-d36e29af8b29.png

保证业务系统一致性的最佳方案:基于RocketMQ的事务消息机制

所以分析完了这个同步发送消息 + 反复多次重试的方案之后,我们会发现他实际落地的时候是可以的,但是里面存在一些问题 比如可能会让订单事务执行成功,结果消息没发送出去,或者是订单事务执行成功了,但是反复多次重试发送消息到MQ极为耗时,导致调用你接口的人频繁超时异常。

所以真正要保证消息一定投递到MQ,同时保证业务系统之间的数据完全一致,业内最佳的方案还是用基于RocketMQ的事务消息机制。因为这个方案落地之后,他可以保证你的订单系统的本地事务一旦成功,那么必然会投递消息到MQ去,通知红包系统去派发红包,保证业务系统的数据是一致的。而且整个流程中,你没必要进行****长时间的阻塞和重试。

如果half消息发送就失败了,你就直接回滚整个流程。如果half消息发送成功了,后续的rollback或者commit发送失败了,你不需要自己去卡在那里反复重试,你直接让代码结束即可,因为后续MQ会过来回调你的接口让你判断再次rollback or commit的。

用发布文章,分析RocketMQ事务消息的代码实现细节

基于官方文档

发送half事务消息出去

1626940851877-c973854c-feb0-4d24-8100-0b82b5843cc1.png

假如half消息发送失败,或者没收到half消息响应怎么办?

1626940908756-f0b0fcdd-002c-48f3-b2ea-69757a359d67.png

没有收到half消息发送成功的通知呢?

针对这个问题,我们可以把发送出去的half消息放在内存里,或者写入本地磁盘文件,后台开启一个线程去检查,如果一个half消息超 过比如10分钟都没有收到响应,那就自动触发回滚逻辑。

如果half消息成功了,如何执行订单本地事务?

1626940914226-bc43463a-aaa3-49ff-aee7-2e6b52538914.png

如果没有返回commit或者rollback,如何进行回调?

1626940917670-4dd4b4b7-79ae-4008-acbd-3bb8ec702e97.png

在这段代码运行的过程中,各个地方如果出现网络异常,或者是系统突然崩溃了,这套机制是如何确保 消息投递稳定运行的。

Broker消息零丢失方案:同步刷盘 + Raft协议主从同步

  • 消息又仅仅停留在os cache中,还没进入到ConsumeQueue磁盘文件里去,然后此时这台机器突然宕机 了,os cache中的数据全部丢失
  • 磁盘突然就坏了,就会一样导致消息丢失,而且可能消息再也找不回来了,同样会丢失数据。

异步刷盘 vs 同步刷盘

将异步刷盘调整为同步刷盘。

broker的配置文件,将其中的flushDiskType配置设置为:SYNC_FLUSH,默认他的值是ASYNC_FLUSH,即默认是异步刷盘的。写入MQ的每条消息,只要MQ告诉我们写入成功了,那么他们就是已经进入了磁盘文件了!

1626941783289-6793aed6-aff1-47d1-81b1-7e7dcf950f43.png

通过dledger主从架构两阶段提交避免磁盘故障导致的数据丢失?

1627727326057-e05b0706-145b-495c-b2e9-7091605c295d.png

基于DLedger技术和Raft协议的主从同步架构,所有的消息写入,只要他写入成功,那就一定会通过Raft协议同步给其他的Broker机器

Dledger会通过两阶段提交的方式保证文件在主从之间成 功同步。

简单来说,数据同步会通过两个阶段,一个是uncommitted阶段,一个是commited阶段。Leader Broker上的Dledger收到一条数据后,会标记为uncommitted状态,然后他通过自己的 DledgerServer组件把这个uncommitted数据发给Follower Broker的DledgerServer组件。

接着Follower Broker的DledgerServer收到uncommitted消息之后,必须返回一个ack给 Leader Broker的Dledger。然后如果Leader Broker收到超过半数的Follower Broker返回的ack 之后,就会把消息标记为committed状态。

再接下来, Leader Broker上的DledgerServer就会发送committed消息给Follower Broker 上的DledgerServer,让他们把消息也标记为committed状态。这样,就基于Raft协议完成了两阶 段的数据同步。

Consumer消息零丢失方案:手动提交offset + 自动故障转移

Kafka消费者的数据丢失问题

Kafka的消费者采用的消费的方式跟 RocketMQ是有些不一样的,如果按照Kafka的消费模式,就是会产生数据丢失的风险。拿到一批消息,还没来得及处理呢,结果就提交offset到broker去了

RocketMQ消费者的与众不同的地方

手动提交

1626942051986-c86a61d9-7c87-4b51-ad07-6157b6f4e3ec.png

RocketMQ的消费者中会注册一个监听器,就是上面小块代码中的MessageListenerConcurrently这个东西,当你的消费者获取到一批消息之后,就会回调你的这个监听器函数,让你来处理这一批消息。

然后当你处理完毕之后,你才会返ConsumeConcurrentlyStatus.CONSUME_SUCCESS作为消费成功的示意,告诉RocketMQ,这批消息我已经处理完毕了。

所以在这个情况下,如果你对一批消息都处理完毕了,然后再提交消息的offset给broker,接着红包系统崩溃了,此时是不会丢失消息的

自动故障转移

你对一批消息都没提交他的offset给broker的话,broker不会认为你已经处理完了这批消息,此时你突然红包系统 的一台机器宕机了,他其实会感知到你的红包系统的一台机器作为一个Consumer挂了。

接着他会把你没处理完的那批消息交给红包系统的其他机器去进行处理,所以在这种情况下,消息也绝对是不会丢失的。

需要警惕的地方:不能异步消费消息

如下错误的示范,我们开启了一个子线程去处理这批消息, 然后启动线程之后,就直接返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS状态了,可能就会出现你开启的子线程还没处理完消息呢,你已经返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS状态了,就可能提交这批消息的offset给broker了,认为已经处理结束了。然后此时你红包系统突然宕机,必然会导致你的消息丢失了!

1626942436775-490af52d-0224-4891-9c21-8d771e2b9b47.png

因此在RocketMQ的场景下,我们如果要保证消费数据的时候别丢消息,你就老老实实的在回调函数里处理消息,处理完了你再返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS状态表明你处理完毕了!

nameServer全部挂了保证消息不丢失--金融级降级保护

路由中心这样的功能,在所有的MQ中都是需要的。kafka是用zookeeper和一个作为Controller的Broker 一起来提供路由服务,整个功能是相当复杂纠结的。而RabbitMQ是由每一个Broker来提供路由服务。 而只有RocketMQ把这个路由中心单独抽取了出来,并独立部署。

如果集 群中所有的NameServer节点都挂了,生产者和消费者是立即就无法 工作了的,不会使用缓存副本。

在这种情况下,RocketMQ相当于整个服务都不可用了,只能自己设计一个降级方案来处理这个问题了。如果多次尝试发送RocketMQ不成功,那就只能另外找给地方(Redis、文件或者内存等)把订单消息缓存下来,然后起一个线程定时的扫描这些失败的订单消息,尝试往RocketMQ发送。这样等 RocketMQ的服务恢复过来后,就能第一时间把这些消息重新发送出去。整个这套降级的机制,在大型 互联网项目中,都是必须要有的。

阶段总结--全链路消息零丢失方案总结

发送消息到MQ的零丢失:

方案一(同步发送消息 + 反复多次重试) ,会导致数据不一致,而且效率低下

方案二(事务消息机制),半消息发过去放在半消息的topic,ack之后开始执行本地事务,只有本地事务提交之后才会转移到真正的topic

MQ收到消息之后的零丢失:

开启同步刷盘策略 + 主从架构同步机制,只要让一个Broker收到消息之后同步写入磁盘,同时同步复制 给其他Broker,然后再返回响应给生产者说写入成功,此时就可以保证MQ自己不会弄丢消息

消费消息的零丢失:

采用RocketMQ的消费者天然就可以保证你处理完消息之后,才会提交消息的offset到broker去,只要记住别采用多线程异步处理消息的方式即可

消息零丢失方案的优势与劣势

从头到尾的消息流转链路的性能大幅度下降,让你的MQ的吞吐量大幅度的下降

比如本身你的系统和MQ配合起来,每秒可以处理几万条消息的,结果当你落地消息零丢失方案之后,可能每秒只能处理几千条消息 了。

改成了基于事务消息的机制之后呢:这里涉及到half消息、commit or rollback、写入内部topic、回调机制,复制到副本,写到磁盘,等待消费者处理完才返回等

消息零丢失方案到底适合什么场景?

一般我们建议,对于跟金钱、交易以及核心数据相关的系统和核心链路,可以上这套消息零丢失方案。

  • 比如支付系统,他是绝对不能丢失任何一条消息的,你的性能可以低一些,但是不能有任何一笔支付记录丢失。
  • 比如订单系统,公司一般是不能轻易丢失一个订单的,毕竟一个订单就对应一笔交易,如果订单丢失,用户还支付成功了,你轻则要给 用户赔付损失,重则弄不好要经受官司,特别是一些B2B领域的电商,一笔线上交易可能多大几万几十万。

而对于其他大部分没那么核心的场景和系统,其实即使丢失一些数据,也不会导致太大的问题,此时可以不采取这些方案,或者说你可以在其他的场景里做一些简化。比如你可以把事务消息方案退化成“同步发送消息 + 反复重试几次”的方案,如果发送消息失败,就重试几次,但是大部分时候可能 不需要重试,那么也不会轻易的丢失消息的!最多在这个方案里,可能会出现一些数据不一致的问题。

或者你把broker的刷盘策略改为异步刷盘,但是上一套主从架构,即使一台机器挂了,os cache里的数据丢失了,但是其他机器上还有 数据。但是大部分时候broker不会随便宕机,那么异步刷盘策略下性能还是很高的。

所以说,对于非核心的链路,非金钱交易的链路,大家可以适当简化这套方案,用一些方法避免数据轻易丢失,但是同时性能整体很 高,即使有极个别的数据丢失,对非核心的场景,也不会有太大的影响。

消息重复(使用幂等)

发送时候重复

当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会 收到两条内容相同并且 Message ID 也相同的消息。

投递时候重复

消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。

负载均衡时消息重复(包括但不限于网络抖动、Broker 重启以及订阅方应用重启)

当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费 者可能会收到重复消息。

如何保证消息不被重复消费啊?如何保证消费的时候是幂等的啊?

消费者宕机之后offset没有提交就会导致重复消费,MQ没有收到offset 就会认为当前的数据还没有被提交。

明确不重复消费不是由消息队列保证,而是由业务代码保证。RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题。

RocketMQ的每条消息都有一个唯一的MessageId,这个参数在多次投递的过程中是不会改变的,所以业务上可以用这个MessageId来作为判断幂等的关键依据。但是,这个MessageId是无法保证全局唯一的,也会有冲突的情况。所以在一些对幂等性要求严格的场 景,****最好是使用业务上唯一的一个标识比较靠谱。例如订单ID。而这个业务标识可以使用Message的 Key来进行传递。

最后常见的解决方案可以基于数据库和Redis两个方面入手,具体看业务使用的是数据库还是Redis或者复杂业务有没有全局唯一ID

  • 借助数据库检测数据是否已经存在,有的话直接update一下而不是insert,或者直接使用数据库的唯一键约束防止重复数据的插入,比如发放优惠券,数据库里面有话就不要重复发放了。以及version是否是期望的,有的话或者version是更新的就不会再操作。
  • Redis查询一下是不是消费过,由于Redis是天然幂等的,所以无论set多少次都不会出错。如果是使用全局唯一ID,可以将ID放到Redis里面,消费消息的时候将ID放到里面,使用setnx,如果成功的话就说明没有消费过,失败的话就说明消费过。

到底什么是幂等性机制?

用来避免对同一个请求或者同一条消息进行重复处理的机制,所谓的幂等,他的意思就是,比如你有一个接 口,然后如果别人对一次请求重试了多次,来调用你的接口,你必须保证自己系统的数据是正常的,不能多出来一些重复的数据,这就是幂等性的意思。

发送消息到MQ的时候如何保证幂等性?有必要吗?

  1. 第一个方案就是业务判断法,也就是说你的订单系统必须要知道自己到底是否发送过消息到MQ去,消息到底是否已经在MQ里了。当支付系统重试调用你的订单系统的接口时,你需要发送一个请求到MQ去,查询一下当前MQ里是否存在针对这个订 单的支付消息?

  2. 基于Redis缓存的幂等性机制,就是状态判断法。如果你成功发送了一个消息到MQ里去,你得在 Redis缓存里写一条数据,标记这个消息已经发送过,那么当你的订单接口被重复调用的时候,你只要根据订单id去Redis缓存里查询一下

基于Redis的状态判断法,有可能没办法完全做到 幂等性。有时候你刚发送了消息到MQ,还没来得及写Redis,系统就挂了,之后你的 接口被重试调用的时候,你查Redis还以为消息没发过,就会发送重复的消息到MQ去。

RocketMQ虽然是支持你查询某个消息是否存在的,但是在这个环节你直 接从MQ查询消息是没这个必要的,他的性能也不是太好,会影响你的接口的性能。

另外基于Redis的消息发送状态的方案,在极端情况下还是没法100%保证幂等性,所以也不是特别好的一个方案。

所以对于我们而言,在这里建议是不用在这个环节保证幂等性,也就是我们可以默许他可能会发送重复的消息到MQ里去。

消息处理失败--延时的重试队列、过期的死信队列

首先对于广播模式的消息, 是不存在消息重试的机制的,即消息消费失败后,不会再重新进行发送,而只是继续消费新的消息。

而对于普通的消息,当消费者消费消息失败后,你可以通过设置返回状态达到消息重试的结果

如果优惠券系统的数据库宕机,会怎么样?

优惠券系统的数据库宕机了,就必然会导致我们从MQ里获取到消息之后是没办法进行处理的,

优惠券系统应该怎么对消息进行重试?重试多少次才行?万一反复重试都没法 成功,这个时候消息应该放哪儿去?直接给扔了吗?

数据库宕机的时候,你还可以返回CONSUME_SUCCESS吗?

如果你返回的话,下一次就会处理下一批消息,但是这批消息其实没处理成功,此时必然导致这批消息就丢失了。

肯定会导致有一批用户没法收到优惠券的!

如果对消息的处理有异常,可以返回RECONSUME_LATER状态--放到重试队列

所以实际上如果我们因为数据库宕机等问题,对这批消息的处理是异常的,此时没法处理这批消息,我们就应该返回一个 RECONSUME_LATER状态

他的意思是,我现在没法完成这批消息的处理,麻烦你稍后过段时间再次给我这批消息让我重新试一下

1626944195647-d6a12775-c120-4d4b-981c-5990873e96ec.png

RocketMQ在收到你返回的RECONSUME_LATER状态之后,RocketMQ会有一个针对你这个ConsumerGroup的重试队列。返回了RECONSUME_LATER状态,他会把你这批消息放到你这个消费组的重试队列中去

比如你的消费组的名称是“VoucherConsumerGroup”,意思是优惠券系统的消费组,那么他会有一个 “%RETRY%VoucherConsumerGroup”这个名字的重试队列, 然后过一段时间之后,重试队列中的消息会再次给我们,让我们进行处理。如果再次失败,又返回了RECONSUME_LATER,那么会再 过一段时间让我们来进行处理,默认最多是重试16次!

消息重试次数

每次重试之间的间隔时间是不一样的,这个间隔时间可以如下进行配置:

messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

上面这段配置的意思是,第一次重试是1秒后,第二次重试是5秒后,第三次重试是10秒后,第四次重试是30秒后,第五次重试是1分钟 后,以此类推,最多重试16次!

注意延迟消息是一样的处理时间间隔!!!

关于重试次数:RocketMQ可以进行定制。例如通过 consumer.setMaxReconsumeTimes(20);将重试次数设定为20次。当定制的重试次数超过16次后,消 息的重试时间间隔均为2小时。

关于MessageId:在老版本的RocketMQ中,一条消息无论重试多少次,这些重试消息的MessageId始终都是一样的。但是在4.7.1版本中,每次重试MessageId都会重建。

配置覆盖:消息最大重试次数的设置对相同GroupID下的所有Consumer实例有效。并且最后启动的Consumer会 覆盖之前启动的Consumer的配置。

如果连续重试16次还是无法处理消息,然后怎么办?

就需要另外一个队列了,叫做死信队列,所谓的死信队列,顾名思义,就是死掉的消息就放这个队列里。

其实就是一批消息交给你处理,你重试了16次还一直没处理成功,就不要继续重试这批消息了,你就认为他们死掉了就可以了。然后这 批消息会自动进入死信队列。

死信队列的名字是“%DLQ%+ConsumerGroup”,我们其实在RocketMQ的管理后台上都是可以看到的。

一个死信队列对应一个ConsumGroup,而不是对应某个消费者实例 如果一个ConsumeGroup没有产生死信队列,RocketMQ就不会为其创建相应的死信队列。 一个死信队列包含了这个ConsumeGroup里的所有死信消息,而不区分该消息属于哪个Topic。 死信队列中的消息不会再被消费者正常消费。 死信队列的有效期跟正常消息相同。默认3天,对应broker.conf中的fileReservedTime属性。超过 这个最长时间的消息都会被删除,而不管消息是否消费过。

处理死信队列

通常,一条消息进入了死信队列,意味着消息在消费处理的过程中出现了比较严重的错误,并且无法自行恢复。此时,一般需要人工去查看死信队列中的消息,对错误原因进行排查。然后对死信消息进行处 理,比如转发到正常的Topic重新进行消费,或者丢弃。

注:默认创建出来的死信队列,他里面的消息是无法读取的,在控制台和消费者中都无法读取。 这是因为这些默认的死信队列,他们的权限perm被设置成了2:禁读(这个权限有三种 2:禁读,4:禁 写,6:可读可写)。需要手动将死信队列的权限配置成6,才能被消费(可以通过mqadmin指定或者 web控制台)。

其实这个就看你的使用场景了,比如我们可以专门开一个后台线程,就是订阅“%DLQ%+ConsumerGroup”这个死信队列,对死信队列中的消息,还是一直不停的重试。

1626944812070-7cd89ec8-2e73-4b35-b61a-b9ab885e91a9.png

消息乱序

即使同一个topic下同一个tag会发到同一个mq

binlog数据恢复的时候,多线程消费数据,执行顺序还是会乱

简单来说,比如订单系统在更新订单数据库的时候,有两条SQL语句:

insert into order values(xx, 0)

update order set xxvalue=100 where id=xxx

每个Topic指定多个MessageQueue,然后你写入消息的时候,其实是会把消息均匀分发给 不同的MessageQueue的。把insert binlog写入到一个MessageQueue里去,update binlog写入到另外一个 MessageQueue里去,两台机器去获取binlog,原本有顺序的消息,完全可能会分发到不同的MessageQueue中去,然后不同机器上部署的Consumer可能会用混乱的顺序从不同的MessageQueue 里获取消息然后处理。

rocketMQ--SUSPEND_CURRENT_QUEUE_A_MOMENT

  • 相同的订单ID加入到相同的queue里面
  • 消费的时候一个queue只能交给一个consumer处理,
  • consumer失败之后不能返回消息到重试队列,也就是不能返回RECONSUME_LATER,而是要返回SUSPEND_CURRENT_QUEUE_A_MOMENT,也就是一会儿再处理这一批,先处理其他队列,这个消息字面意思就是暂时不处理这个mq)

全局有序和局部有序

  • 全局有序:整个MQ系统的所有消息严格按照队列先入先出顺序进行消费。
  • 局部有序:只保证一部分关键消息的消费顺序。

大部分的MQ业务场景,我们只需要能够保证局部有序就可以了。

  • 只需要保证一个聊天窗口里的消息有序就可以了。
  • 而对于电商订单场景,也只要保证一个订单的所有消息是有序的就可以 了。

至于全局消息的顺序,并不会太关心。全局有序都可以压缩成局部有序的问题。

例如以前我们常用的聊天室,就是个典型的需要保证消息全局有序的场景。但是这种场景,通常可以压缩 成只有一个聊天窗口的QQ来理解。即整个系统只有一个聊天通道,这样就可以用QQ那种保证一个聊天 窗口消息有序的方式来保证整个系统的全局消息有序。

发送MessageQueue轮询的方式保证消 息尽量均匀的分布到所有的MessageQueue上,而消费者也就同样需要从多个MessageQueue上消费 消息。

对于局部有序的要求,只需要将有序的一组消息都存入同一个MessageQueue里,这样 MessageQueue的FIFO设计天生就可以保证这一组消息的有序。RocketMQ中,可以在发送者发送消息时指定一个 MessageSelector 对象,让这个对象来决定消息发入哪一个MessageQueue。这样就可以保 证一组有序的消息能够发到同一个MessageQueue里。

另外,通常所谓的保证Topic全局消息有序的方式,就是将Topic配置成只有一个MessageQueue队列 (默认是4个)。这样天生就能保证消息全局有序了。这个说法其实就是我们将聊天室场景压缩成只有一个 聊天窗口的QQ一样的理解方式。而这种方式对整个Topic的消息吞吐影响是非常大的,如果这样用,基本上就没有用MQ的必要了。

让属于同一个订单的binlog有序进入一个MessageQueue

2个binlog都直接进入到Topic下的一个MessageQueue里去。完全可以根据订单id来进行判断,我们可以往MQ里发送binlog的时候,根据订单id来判断一下,如果订单id相同,你必须保证他进入同一个MessageQueue。

Canal作为一个中间件从 MySQL那里监听和获取binlog,那么当binlog传输到Canal的时候,也必然是有先后顺序的,先是insert binlog,然后是update binlog,将binlog发送给MQ的时候,必须将一个订单的binlog都发送到一个MessageQueue里去,而且发送过去的时候,也必须是 严格按照顺序来发送的

Consumer有序处理一个订单的binlog

一个Consumer可以处理多个MessageQueue的消息,但是一个MessageQueue只能交给一个Consumer来进 行处理,所以一个订单的binlog只会有序的交给一个Consumer来进行处理!

万一消息处理失败了可以走重试队列吗?

Consumer处理消息的时候,可能会因为底层存储挂了导致消息处理失败,返回RECONSUME_LATER状态,然后broker会过一会儿自动给我们重试。

绝对是不行的,因为如果你的consumer获取到订单的一个insert binlog,结果处理失败了,此时返回了RECONSUME_LATER,那 么这条消息会进入重试队列,过一会儿才会交给你重试。

但是此时broker会直接把下一条消息,也就是这个订单的update binlog交给你来处理,此时万一你执行成功了,就根本没有数据可以 更新!又会出现消息乱序的问题,所以对于有序消息的方案中,如果你遇到消息处理失败的场景,就必须返回SUSPEND_CURRENT_QUEUE_A_MOMENT这个状态,不能进入重试队列,而是暂停等待一会儿,继续处理这批消息。

代码实现

如何让一个订单的binlog进入一个MessageQueue?

1626946129587-29d0f28b-4e03-497e-b56f-54686b08da45.png

一个是发送消息的时候传入一个MessageQueueSelector,在里面你要根据 订单id和MessageQueue数量去选择这个订单id的数据进入哪个MessageQueue。

同时在发送消息的时候除了带上消息自己以外,还要带上订单id,然后MessageQueueSelector就会根据订单id去选择一个 MessageQueue发送过去,这样的话,就可以保证一个订单的多个binlog都会进入一个MessageQueue中去。

消费者如何保证按照顺序来获取一个MessageQueue中的消息?MessageListenerOrderly:Consumer会对每一个ConsumeQueue,都仅仅用一个线程来处理其中的消息。并且失败的时候返回等待一会儿再处理队列消息

1626946318320-d1ff0635-2003-4110-94fd-a38bd4aa1960.png

RabbitMQ--只让一个消费

Kafka--同一个消费者同一个内存队列

一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。

  • 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。

1592484076496-962b9adf-0cb9-4fb4-8705-77b201e129eb.png

消息积压

如何确定RocketMQ有大量的消息积压?

部 分消费者系统出现故障,就会造成大量的消息积累。

对于消息积压,如果是RocketMQ或者kafka还好,他们的消息积压不会对性能造成很大的影响。而 如果是RabbitMQ的话,那就惨了,大量的消息积压可以瞬间造成性能直线下滑。

对于RocketMQ来说,有个最简单的方式来确定消息是否有积压。那就是使用web控制台,就能直接 看到消息的积压情况。在Web控制台的主题页面,可以通过 Consumer管理 按钮实时看到消息的积压情况。

也可以通过mqadmin指令在后台检查各个Topic的消息延迟情况。

还有RocketMQ也会在他的 ${storePathRootDir}/config 目录下落地一系列的json文件,也可以用来跟 踪消息积压情况。

1627728228805-48831ed2-c8e6-4767-a20f-33ace8f8fdc3.png

不指定默认是4队列

如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?--紧急扩容+消息转移

高峰期内,大概就会有100多万条消息进入RocketMQ。消费者系统依赖的NoSQL数据库就挂掉了,然后生产者系统在晚上几个小时的高峰期内,就往MQ里写入了100多万的消息,此时都积压在MQ里了,根本没人消费和处理。

  • 丢弃:如果这些消息允许丢失,那么此时你就可以紧急修改消费者系 统的代码,在代码里对所有的消息都获取到就直接丢弃,不做任何的处理,这样可以迅速的让积压在MQ里的百万消息被处理掉

  • 消息还要的话:

    • 增加消费者节点:如果Topic下的MessageQueue配置得是足够多的,那每个Consumer实际上会分配多个 MessageQueue来进行消费。这个时候,就可以简单的通过增加Consumer的服务节点数量来加快消息 的消费,等积压消息消费完了,再恢复成正常情况。最极限的情况是把Consumer的节点个数设置成跟 MessageQueue的个数相同。但是如果此时再继续增加Consumer的服务节点就没有用了。
    • 创建新的topic转移消息:而如果Topic下的MessageQueue配置得不够多的话,可以创建一个新的Topic,配置足够多的 MessageQueue。然后把所有消费者节点的目标Topic转向新的Topic,并紧急上线一组新的消费者,只 负责消费旧Topic中的消息,并转储到新的Topic中,这个速度是可以很快的。然后在新的Topic上,就可 以通过增加消费者个数来提高消费速度了。之后再根据情况恢复成正常情况。

在官网中,还分析了一个特殊的情况。就是如果RocketMQ原本是采用的普通方式搭建主从架 构,而现在想要中途改为使用Dledger高可用集群,这时候如果不想历史消息丢失,就需要先将消 息进行对齐,也就是要消费者把所有的消息都消费完,再来切换主从架构。因为Dledger集群会接 管RocketMQ原有的CommitLog日志,所以切换主从架构时,如果有消息没有消费完,这些消息 是存在旧的CommitLog中的,就无法再进行消费了。这个场景下也是需要尽快的处理掉积压的消 息。

mq 中的消息过期失效了--手动重发

假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了

批量重导,写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。

假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

其余rocketMQ高级特性

基于延迟消息机制优化大量订单的定时退款扫描问题

当一个订单下单之后,超过比如30分钟没有支付,那么就必须订单系统自动关闭这个订单,后续你如果要购买这个订单里的商品,就得重新下订单了。

订单系统一个后台线程,不停的扫描订单数据库里所有的未支付状态的订单,不好:

  • 一个原因是未支付状态的订单可能是比较多的,然后你需要不停的扫描他们,可能每个未支付状态的订单要被扫描N多遍,才会发现他已经超过30分钟没支付了
  • 另外一个是很难去分布式并行扫描你的订单。因为假设你的订单数据量特别的多,然后你要是打算用多台机器部署订单扫描服务,但是 每台机器扫描哪些订单?怎么扫描?什么时候扫描?这都是一系列的麻烦问题。

所谓延迟消息,意思就是说,我们订单系统在创建了一个订单之后,可以发送一条消息到MQ里去,我们指定这条消息是延迟消息,比如要等待30分钟之后,才能被订单扫描服务给消费到。这样当订单扫描服务在30分钟后消费到了一条消息之后,就可以针对这条消息的信息,去订单数据库里查询这个订单。

另外就是如果你的订单数量很多,你完全可以让订单扫描服务多部署几台机器,然后对于MQ中的Topic可以多指定一个 MessageQueue,这样每个订单扫描服务的机器作为一个Consumer都会处理一部分订单的查询任务。

1626947311037-6d1eba86-4bda-4430-89df-48bce3ac342a.png

其实发送延迟消息的核心,就是设置消息的delayTimeLevel,也就是延迟级别。

RocketMQ默认支持一些延迟级别如下:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

所以上面代码中设置延迟级别为3,意思就是延迟10s,你发送出去的消息,会过10s被消费者获取到。那么如果是订单延迟扫描场景, 可以设置延迟级别为16,也就是对应上面的30分钟。

1626947326500-a4fa67ae-88c8-4319-8c51-3b54bc657dfe.png

RocketMQ的生产实践中经验总结

(1)灵活的运用 tags来过滤数据

合理的规划Topic和里面的tags,一个Topic代表了一类业务消息数据,然后对于这类业务消息数据,如果你希望继续划分一些类别的话,可以在发送消息的时候设置tags。

假设你现在一个系统要发送外卖订单 数据到MQ里去,就可以针对性的设置tags,比如不同的外卖数据都到一个“WaimaiOrderTopic”里去。但是不同类型的外卖可以有不同的tags:“meituan_waimai”,“eleme_waimai”,“other_waimai”,等等。然后对你消费“WaimaiOrderTopic”的系统,可以根据tags来筛选,可能你就需要某一种类别的外卖数据罢了。

(2)基于消息key来定位消息是否丢失

设置一个消息的key为订单id:message.setKeys(orderId),这样这个消息就具备一个 key了。broker上,会基于key构建hash索引,这个hash索引就存放在IndexFile索引文件里。

可以通过MQ提供的命令去根据key查询这个消息,类似下面这样:

mqadmin queryMsgByKey -n 127.0.0.1:9876 t SCANRECORD -k orderId

(3)消息零丢失方案的补充

MQ集群彻底故障了,此时就是不可用了,那 么怎么办呢?其实对于一些金融级的系统,或者跟钱相关的支付系统,或者是广告系统,类似这样的系统,都必须有超高级别的高可用保障机制。

一般假设MQ集群彻底崩溃了,你生产者就应该把消息写入到本地磁盘文件里去进行持久化,或者是写入数据库里去暂存起来,等待 MQ恢复之后,然后再把持久化的消息继续投递到MQ里去。

详细看下面的金融场景

(4)提高消费者的吞吐量

增加consumer的线程数量,可以设置consumer端的参数:consumeThreadMin、consumeThreadMax,这样一台 consumer机器上的消费线程越多,消费的速度就越快。

此外,还可以开启消费者的批量消费功能,就是设置consumeMessageBatchMaxSize参数,他默认是1,但是你可以设置的多一些, 那么一次就会交给你的回调函数一批消息给你来处理了,此时你可以通过SQL语句一次性批量处理一些数据,比如:update xxx set xxx where id in (xx,xx,xx)。

(5)要不要消费历史消息

其实consumer是支持设置从哪里开始消费消息的,

一个是从Topic的第一条数据开始消费,一个是从最后一次消费过 的消息之后开始消费。对应的是:CONSUME_FROM_LAST_OFFSET,CONSUME_FROM_FIRST_OFFSET

一般来说,我们都会选择CONSUME_FROM_FIRST_OFFSET,这样你刚开始就从Topic的第一条消息开始消费,但是以后每次重启, 你都是从上一次消费到的位置继续往后进行消费的。

权限机制的控制

Topic资源级别的用户访问控制。规定好订单团队的用户,只能使用“OrderTopic”,然后商品团队的用户只能使用“ProductTopic”,大家互相之间不能混乱的使用别人的Topic。

在Client客户端通过 RPCHook注入AccessKey和SecretKey签名;在每个Broker的配置文件里需要设置aclEnable=true 这个配置,开启权限控制;同时,将对应的权限控制属性(包括Topic访问权限、IP白名单和AccessKey和SecretKey签名等)设置在 $ROCKETMQ_HOME/conf/plain_acl.yml的配置文件中。Broker端对AccessKey所拥有的权限进行校验,校验不过,抛出异常; ACL客户端可以参考:org.apache.rocketmq.example.simple包下面的 AclClient代码。

**plain_acl.yml**的配置文件:

#全局白名单,不受ACL控制
#通常需要将主从架构中的所有节点加进来 
globalWhiteRemoteAddresses:
- 10.10.103.*
- 192.168.0.*

accounts: 
#第一个账户
- accessKey: RocketMQ 
  secretKey: 12345678 
  whiteRemoteAddress:
  admin: false 
  defaultTopicPerm: DENY #默认Topic访问策略是拒绝 
  defaultGroupPerm: SUB #默认Group访问策略是只允许订阅 
  topicPerms:
  - topicA=DENY #topicA拒绝
  - topicB=PUB|SUB #topicB允许发布和订阅消息
  - topicC=SUB #topicC只允许订阅 
  groupPerms:
  # the group should convert to retry topic
  - groupA=DENY
  - groupB=PUB|SUB
  - groupC=SUB 
  
#第二个账户,只要是来自192.168.1.*的IP,就可以访问所有资源
- accessKey: rocketmq2 
  secretKey: 12345678 
  whiteRemoteAddress: 192.168.1.* 
  # if it is admin, it could access all resources 
  admin: true

注意,如果要在自己的客户端中使用RocketMQ的ACL功能,还需要引入一个单独的依赖包

<dependency> 
  <groupId>org.apache.rocketmq</groupId> 
  <artifactId>rocketmq-acl</artifactId> 
  <version>4.7.1</version> 
</dependency>

如果你一个账号没有对某个Topic显式的指定权限,那么就是会采用默认Topic权限。

接着我们看看在你的生产者和消费者里,使用一个账号的时候,就只能访问你有权 限的Topic。

1626948889338-6aeb97be-82a5-4184-9b67-25ba83c01049.png

上面的代码中就是在创建Producer的时候后,传入进去一个AclClientRPCHook,里面就可以设置你这个Producer的账号密码,对于 创建Consumer也是同理的。通过这样的方式,就可以在Broker端设置好每个账号对Topic的访问权限,然后你不同的技术团队就用不 同的账号就可以了。

消息轨迹的追踪?

Producer端 Consumer端 Broker端
生产实例信息 消费实例信息 消息的Topic
发送消息时间 投递时间,投递轮次 消息存储位置
消息是否发送成功 消息是否消费成功 消息的Key值
发送耗时 消费耗时 消息的Tag值

首先需要在broker的配置文件里开启**<font style="color:#F5222D;">traceTopicEnable=true</font>**这个选项,此时就会开启消息轨迹追踪的功能。 启动这个Broker的时候会自动创建出来一个内部系统级别的Topic,就是**<font style="color:#F5222D;">RMQ_SYS_TRACE_TOPIC</font>** 这个Topic就是用来存储所有的消息轨迹追踪的数据的。

1627728647184-4d918db0-7cb1-413b-a087-18f3c26e7ca2.png

1626949058335-dae9a2fb-0d1e-4c55-80f3-9c0e47f0030d.png

在发送消息的时候开启消息轨迹,此时创建Producer的时候要用第二个参数,就是enableMsgTrace参数,他设置为true,就是说可以对消息开启轨迹追踪。在订阅消息的时候,对于Consumer也是同理的,在构造函数的第二个参数设置为true,就是开启了消费时候的轨迹追踪。

Producer在发送消息的时候, 就会上报这个消息的一些数据到内置的RMQ_SYS_TRACE_TOPIC里去,此时会上报如下的一些数据:Producer的信息、发送消息的时间、消息是否发送成功、发送消息的耗时。

接着消息到Broker端之后,Broker端也会记录消息的轨迹数据,包括如下:消息存储的Topic、消息存储的位置、消息的key、消息的 tags。

消息被消费到Consumer端之后,他也会上报一些轨迹数据到内置的RMQ_SYS_TRACE_TOPIC里去,包括如下一些东西: Consumer的信息、投递消息的时间、这是第几轮投递消息、消息消费是否成功、消费这条消息的耗时。

在RocketMQ控制台里,在导航栏里就有一个消息轨迹,在里面可以创建查询任务,你可 以根据messageId、message key或者Topic来查询,查询任务执行完毕之后,就可以看到消息轨迹的界面了。

在消息轨迹的界面里就会展示出来刚才上面说的Producer、Broker、Consumer上报的一些轨迹数据了。

金融级的系统如何针对RocketMQ集群崩溃设计高可用方案?

连续重试了比如超过3次还是失败,说明此时可能就是你的MQ集群彻底崩溃了,此时你必须把这条重要的消息写入到本地 存储中去,可以是写入数据库里,也可以是写入到机器的本地磁盘文件里去,或者是NoSQL存储中去

之后你要不停的尝试发送消息到MQ去,一旦发现MQ集群恢复了,你必须有一个后台线程可以把之前持久化存储的消息都查询出来, 然后依次按照顺序发送到MQ集群里去,这样才能保证你的消息不会因为MQ彻底崩溃会丢失。

这里要有一个很关键的注意点,就是你把消息写入存储中暂存时,一定要保证他的顺序,比如按照顺序一条一条的写入本地磁盘文件去 暂存消息。

一旦MQ集群故障了,你后续的所有写消息的代码必须严格的按照顺序把消息写入到本地磁盘文件里去暂存,这个顺序性是要严格 保证的。

为什么要给RocketMQ增加消息限流功能保证其高可用性?

限流功能就是对系统的一个保护功能。业务代码报错了然后进入了 catch代码块,结果那个工程师居然在catch代码块里写了一个while死循环,不停的发送消息。而且上述系统当时是部署在10多台机器上的一个系统,所以相当于10多台机器都频繁的开足CPU的马力,拼命的往MQ里写消息,瞬间 就导致MQ集群的TPS飙升,里面混入了大量的重复消息,而且MQ集群都快挂了。

需要修改源码,在接收消息这块,必须引入一个限流机制,也就是说要限制好,你这台机器每秒钟最多就只能处理比如3万条消息,根据你的MQ集群 的压测结果来,你可以通过压测看看你的MQ最多可以抗多少QPS,然后就做好限流。很多互联网大厂其实都会改造开源MQ的内核源码,引入限流机制,然后只能允许指定范围内的消息被 在一秒内被处理,避免因为一些异常的情况,导致MQ集群挂掉。

一般来说,限流算法可以采取令牌桶算法也就是说你每秒钟就发放多少个令牌,然后只能允许多少个请求通过

设计一套Kafka到RocketMQ的双写+双读技术方案,实现无缝迁移!

不可能说在某一个时间点突然之间就说把所有的Producer系 统都停机,然后更新他的代码,接着全部重新上线,然后所有Producer系统都把消息写入到RocketMQ去了

基本上对于类似的一些重要中间件的迁移,往往都会采取双写双读的方法,双写双读一段时间,然后观察两个方案的结果都一致了,你再正式 下线旧的一套东西。

  • 双写,在所有的Producer系统中,要引入一个双写的代码,让他同时往Kafka和RocketMQ中去写入消息,然后多写几天,起码双写要持续个1周左右,因为MQ一般都是实时数据,里面数据也就最多保留一周。
    • 当你的双写持续一周过后,你会发现你的Kafka和RocketMQ里的数据看起来是几乎一模一样了,因为MQ反正也就保留最近几天的数 据,当你双写持续超过一周过后,你会发现Kafka和RocketMQ里的数据几乎一模一样了。
  • 但是光是双写还是不够的,还需要同时进行双读,也就是说在你双写的同时,你所有的Consumer系统都需要同时从Kafka和 RocketMQ里获取消息,分别都用一模一样的逻辑处理一遍。
    • 只不过从Kafka里获取到的消息还是走核心逻辑去处理,然后可以落入数据库或者是别的存储什么的,但是对于RocketMQ里获取到的 消息,你可以用一样的逻辑处理,但是不能把处理结果具体的落入数据库之类的地方。

你的Consumer系统在同时从Kafka和RocketMQ进行消息读取的时候,你需要统计每个MQ当日读取和处理的消息的数量,这点非常的重要,同时对于RocketMQ读取到的消息处理之后的结果,可以写入一个临时的存储中。

同时你要观察一段时间,当你发现持续双写和双读一段时间之后,如果所有的Consumer系统通过对比发现,从Kafka和RocketMQ读 取和处理的消息数量一致,同时处理之后得到的结果也都是一致的,此时就可以判断说当前Kafka和RocketMQ里的消息是一致的,消费offset是一致的才是最重要的,而 且计算出来的结果也都是一致的。

这个时候就可以实施正式的切换了,你可以停机Producer系统,再重新修改后上线,全部修改为仅仅写RocketMQ,这个时候他数据 不会丢,因为之前已经双写了一段时间了,然后所有的Consumer系统可以全部下线后修改代码再上线,全部基于RocketMQ来获取消 息,计算和处理,结果写入存储中。

基于MQ实现异步化改造

SpringBoot集成RocketMQ的 starter依赖是由Spring社区提供的,目前正在快速迭代的过程当中,不同版本之间的差距非常 大,甚至基础的底层对象都会经常有改动。例如如果使用rocketmq-spring-boot-starter:2.0.4版 本开发的代码,升级到目前最新的rocketmq-spring-boot-starter:2.1.1后,基本就用不了了。

也可以直接使用rocket的依赖

解决首要问题--发布帖子同步流程长,核心链路执行时间过长,

在用户发布完毕后,只要执行最核心的增加文章就可以了,保证速度足够快。

然后诸如通知关注的用户,给用户发布奖励 的操作,都可以通过MQ实现异步化执行。

Topic是一个逻辑上的概念,实际上他的数据是分布式存储在多个Master Broker中的,因此当你发送一个订单消息过去的时候,会根据一定的负载均衡算法和容错算法把消息发送到一个Broker中去。

1624701053999-96c97f22-31b9-4277-954d-ebc041d1531e.png

1624701280396-8dbf1060-89b4-44b4-8717-7375ca16ade8.png

基于MQ实现秒杀订单系统的异步化架构以及精准扣减库存的技术方案

秒杀活动主要涉及到的并发压力就是两块,一个是高并发的读,一个是高并发的写。

页面静态化 之后 CDN + Nginx + Redis的多级缓存架构

如果在Redis中还是没有找到,就由Nginx中的Lua脚本直接把请求转发到商品详情页系统里去加载就可以了,此时就会直接从数据库中加载数据出来,但是一般来说数据一般是可以从CDN、Nginx、Redis中加载到的,可能只有极少的请求会直接访问到商品系统去从数据库里加载商品 页数据。

  • 在前端/客户端设置秒杀答题,错开大量人下单的时间,阻止作弊器刷单
  • 为秒杀独立出来一套订单系统,同时还有很多其他的用户这个时候并不在参与秒杀系统,他们在进行其他商品的常规性浏览和下单。
  • 优先基于Redis进行高并发的库存扣减,一旦库存扣完则秒杀结束,通常在秒杀场景下,一般会将每个秒杀商品的库存提前写入Redis中,然后当请求到来之后,就直接对Redis中的库存进行扣减
  • 秒杀结束之后,Nginx层过滤掉无效的请求,大幅度削减转发到后端的流量,抢购完毕之后提前过滤无效请求,比如一旦商品抢购完毕,可以在ZooKeeper中写入一个秒杀完毕的标志位,然后ZK会反向通知Nginx中我们自己写的Lua脚本,通过 Lua脚本后续在请求过来的时候直接过滤掉,不要向后转发了。
  • 瞬时高并发下单请求进入RocketMQ进行削峰,如果判断发现通过Redis完成了库存扣减,此时库存还大于0,就说明秒杀成功了需要生成订单,此时就 直接发送一个消息到RocketMQ中即可。然后让普通订单系统从RocketMQ中消费秒杀成功的消息进行常规性的流程处理即可,利用RocketMQ抗下每秒几万并发的下单请求,然后让订单系统以每秒几千的速率慢慢处理就可以了,也就是延迟个可能几 十秒,这些下单请求就会处理完毕。

1626919023896-234bddd8-d9fc-47a6-9b92-d0f16e34429f.png

总结

1626919092105-ca57371f-1c2c-4c67-89f6-bba7be419a23.png

1626927081307-9fd5a5bf-9de1-477f-a086-05b38ac94aac.png

(1)Kafka、RabbitMQ他们有类似的数据分片机制吗?他们是如何把一个逻辑上的数据集合概念(比如一个Topic)给在物理上拆分 为多个数据分片的?然后拆分后的多个数据分片又是如何在物理的多台机器上分布式存储的?

(2)为什么一定要让MQ实现数据分片的机制?如果不实现数据分片机制,让你来设计MQ中一个数据集合的分布式存储,你觉得好设 计吗?

(3)同步刷盘和异步刷盘两种策略,分别适用于什么不同的场景呢?

(4)异步刷盘可以提供超高的写入吞吐量,但是有丢失数据的风险,这个适用于什么业务场景?在你所知道的业务场景,或者工作接 触过的业务场景中,有哪些场景需要超高的写入吞吐量,但是可以适度接受数据丢失?

(5)同步刷盘会大幅度降低写入吞吐量,但是可以让你的数据不丢失,你接触哪些场景,是严格要求数据务必不能丢失任何一条,但 是吞吐量并没有那么高的呢?

(6)Kafka、RabbitMQ他们的broker收到消息之后是如何写入磁盘的?采用的是同步刷盘还是异步刷盘的策略?为什么?

(7)每次写入都必须有超过半数的Follower Broker都写入消息才可以算做一次写入成功,那么大家思考一个问题,这样做是不是会对 Leader Broker的写入性能产生影响?是不是会降低TPS?是不是必须要在所有的场景都这么做?为什么呢?

(8)一般我们获取到一批消息之后,什么时候才可以认为是处理完这批消息了?是刚拿到这批消息就算处理完吗?还是说要对这批消 息执行完一大堆的数据库之类的操作,才算是处理完了?

(9)如果获取到了一批消息,还没处理完呢,结果机器就宕机了,此时会怎么样?这些消息会丢失,再也无法处理了吗?如果获取到 了一批消息,已经处理完了,还买来得及提交消费进度,此时机器宕机了,会怎么样呢?

(10)消费者机器到底是跟少数几台Broker建立连接,还是跟所有Broker都建立连接?这是不少朋友之前在评论区提出的问题,但是 我想这里大家肯定都有自己的答案了。

(11)RocketMQ是支持主从架构下的读写分离的,而且什么时候找Slave Broker读取大家也都了解的很清楚了,那么大家思考一下, Kafka、RabbitMQ他们支持主从架构下的读写分离吗?支持Slave Broker的读取吗?为什么呢?

(12)如果支持读写分离的话,有没有一种可能,就是出现主从数据不一致的问题?比如有的数据刚刚到Master Broker和部分Slave Broker,但是你刚好是从那个没有写入数据的Slave Broker去读取了?

(13)消费吞吐量似乎是跟你的处理速度有很大关系,如果你消费到一批数据,处理太慢了,会导致你严重跟不上数据写入的速度,这 会导致你后续几乎每次拉取数据都会从磁盘上读取,而不是os cache里读取,所以你觉得你在拉取到一批消息处理的时候,应该有哪些 要点需要注意的?

理解误区

mmap和sendfile

  • mmap:4次上下文切换,3次数据拷贝,内存地址映射,java里面的MappedByteBuffer
  • sendfile:2次上下文切换,2次数据拷贝,都没有使用CPU,都是DMA直接操作数据的拷贝,从硬盘到内核缓存再到网卡。(缓存的位置以及数据的大小放到socket缓冲区,网卡直接读取元数据之后拷贝数据),java 里面的channel的transferTo方法

相对于sendfile的大容量传输,mmap的小容量传输更适合rocketmq,而rocketmq由于是用java开发的,大对象会对jvm的垃圾回收机制有一定影响,故选择了mmap。

消费消息的时候,kafka使用sendfile,rocketMQ使用MMAP(严格来说MMAP就是MMAP,不算零拷贝),

mmap,和sendfile,本质上差不多,所以选择那种都无可厚非。mmap适合的场景,sendfile也都差不多,至于rmq为什么选择mmap,个人觉得更方便做内存处理操作

java 实现mmap:

https://www.cnblogs.com/davidwang456/p/3853977.html

mmap和sendfile(重要):

https://blog.csdn.net/qq32933432/article/details/107642985

Kafka和RocketMQ底层存储之那些你不知道的事

https://blog.csdn.net/g6U8W7p06dCO99fQ3/article/details/107678759

mmap 和 sendFile 零拷贝原理

https://blog.csdn.net/shulianghan/article/details/106438212

posted on 2025-10-13 01:09  chuchengzhi  阅读(29)  评论(0)    收藏  举报

导航

杭州技术博主,专注分享云计算领域实战经验、技术教程与行业洞察, 打造聚焦云计算技术的垂直博客,助力开发者快速掌握云服务核心能力。

褚成志 云计算 技术博客