MQ对比
- ActiveMQ, RabbitMQ,RocketMQ,Kafka
- 并发量:ActiveMQ 8000TPS,RabbitMQ 12000TPS,RocketMQ 10WTPS ,Kafka 100W TPS
- 支持语言:ActiveMQ,RabbitMQ,Kafka 支持主流的语言都支持, RocketMQ值支持java
- 持久化:都支持,但是ActiveMQ,RabbitMQ 开启持久化对性能有影响,RockerMQ,Kafaka本就是基于文件系统天生就支持持久化
- 综合:
- ActiveMQ适用于老的系统没有大规模系统的运用案例
- RabbitMQ 稳定性高,但是不支持动态扩容,适用于并发度要求不少那么高的系统,并且Erleang 语言懂得人少,了解内部机制比较困难
- RockerMQ 功能丰富,例如定时消息,事务消息,简单易用,Java语言开发源码易懂,有大规模应用,但只支持java
- Kafka对大数据处理很友好,支持流式处理API,天生分布式,但是对运维要求高,带宽要求高。
RocketMQ
RocketMQ应用场景
-
削峰填谷:对于秒杀,广告系统,等会带来突发的较高的流量的业务场景中,很可能因为没有做相应的保护而导致超负荷甚至奔溃而导致项目的不可用,RocketMQ可以提供削峰填谷的功能解决这个问题
- 例如在公司项目广告系统zhenai-advertising-api中,因为接入了各个媒体的流量数据,需要接受各媒体的点击信息,因为流量的不可预估性质(这个取决于媒体端的用户习惯,广告位置,以及广告投放预算),因此随时都有可能有突增的流量过来,虽然我们用Sentinel做了流控,但是对于被拦截超时的部分点击我们是损失了这部分数据的,因此过多的拦截是会导致广告系统最终上报用户数据不准确,造成用户模型的预估不准确。为了不损失这部分点击信息我们需要有一个随时能抵御流量洪峰的中间件,那么RocketMQ是最佳的选择,他能够扛住10W级别TPS。在上了RocketMQ之后,能看到异常超时数据明显降低
-
异步:RocketMQ实现异步通信为业务做异步处理,确保主业务的连续性
- 例如在app中登陆模块zhenai-account-api,在用户登陆之后会给当前用户进行一些资源的预加载,登陆时间的维护,给粉丝推送登陆信息等这些操作,但是这些操作对于用户来说是阻塞他登陆的一些流程,因此不能在登陆时候同步执行,要不然阻塞正常登陆导致用户长时间不能进入app,因此会在登陆权限确认之后,通过RocketMQ进行一些异步操作,通过发送登陆消息异步的消费登陆消息去执行登陆之后的一些loginAsync,以此达到异步执行的目的
-
解耦:RocketMQ通过消息机制为业务做解耦合,让不同模块之间业务流程独立运行
- 例如在app中用户关注模块与动态模块,是两个独立的模块,但是当用户触发关注行为的时候,需要维护当前用户的一个动态列表,动态列表中维护的是我关注的用户所发的动态的一个时间轴数据。因此在关注后我需要将新增的关注用户的动态添加到我的动态数据仓库中。因此这两不同模块的逻辑却有了联系,我们 可以借助RocketMQ进行解耦,发送一条关注消息,通过消费这条消息来达到维护动态数据的目的,最终做到数据的一致性。
-
分布式一致性:在交易系统中,支付订单状态与积分信息等等需要保持数据的最终一致性,我们可以通过RocketMQ的分布式事务既可以实现系统的解耦,又可以保证数据的最终一致性
-
分布式缓存同步:在大型项目中,因大量并发访问我们通常都是用缓存去承载查询,当我们查询的信息中有敏感数据时候例如价格,我们需要及时更新,利用RockerMQ的广播消息,对每一个节点进行数据广播,可以实现通知山坡数据实时变化的目的
MQ存在的缺点
- 降低系统可用性,因为引入了外部系统,因此需要额外的手段保证中间件的一个可靠性
- 代码复杂性提高
- 一致性问题,RockerMQ存在消息重复投递问题,需要业务字节保证消费幂等信。
RocketMQ 整体架构
- 常用的消息中间件kafka,rabbitMQ,RocketMQ等都是基于发布/订阅 机制,消息发送者 将消息发送到服务端,消息消费者从消息服务器订阅感兴趣的消息,整个过程中消息发送者与消息消费者都是客户端,消息服务器是服务端,客户端与服务端双方通过注册中心感知对方的存在
- RocketMQ架构有如下四个部分组成:
- Producer:消息发布角色,主要负责将消息发送到Broker,支持分布式集群部署
- Consumer:消息消费者的角色,主要负责从Broker订阅消息消费,支持分布式集群部署
- Broker:消息存储的角色,主要负责消息的存储,投递查询,以及服务高可用的保证,支持分布式集群部署
- NameServer:服务管理的角色,主要负责管理Broker集群路由,为客户端Producer与Consumer提供服务发现。支持分布集群部署。
- NameServer是一个非常简单的Topic路由注册中心,他就和Kafka中的zookeeper一样,支持Broker的动态注册与发现,主要包括以下两个功能:
- 服务注册:NameServer接收Broker集群的注册信息,保存下来作为路由信息的基本数据,并且提供心跳检测机制,检查Broker是否还存活
- 路由信息管理:NameServer保存了Broker集群路由信息,用于提供给客户端查询Broker的队列信息,Producer与Consumer通过NameServer可以指定Broker集群的路由信息,从而进行消息的投递和消费。
放弃Zookeeper 选择 NameServer(Zookeeper 与 NameServer 对比)
-
kafka中服务注册发现是通过zookeeper完成,RockerMQ早起也是用的Zookeeper做的集群管理,但是后来的版本放弃了Zookeeper转用的自己开发的NameServer,这两者的区别还是非常大的
-
在kafka中 Topic 是逻辑概念,分区(Partition) 是物理分区,1个Topic可以设置多个分区,每个分区可以设置多个副本(Replication),即一个Master分区,多个Slave分区,如下图结构
-
发消息只能通过master,并且我们发送消息到Broker0 就由于 part0-m接收,让后由part0 同步数据到part1 与part2
-
RocketMQ中Topic是逻辑概念,队列(Queue)是物理概念,和Kafka很想。1个Topic可以有多个Queue默认4个。每个队列也可以多个副本slave,即一个Master多个Slave如下图
- 上图中,同样TopicA,队列数量只有三个,但是总共有9个Broker组成集群
Kafka 与 RockerMQ 差异:
- 在Kafka中,Master 和Slave在同一台Broker机器上,Broker集群上有多个分区,每个分区的Master/Slave身份是在运行过程中选举出来的,既有Master分区,也有Slave分区
- RockerMQ中,Master和Slave不在一台Broker机器上,每一台Broker只能是Master或者Slave,Broker 的身份是在配置文件中设置的,Broker启动之前就决定了。
Zokeeper 与 NameServer差异
-
Kafka中是通过Zookeeper选举的出的Master/Slave,Zookeeper具备选举功能,选举机制的原理是少数服从多数,那么Zookeeper的选举机制必定由Zookeeper集群中多个实例共同完成,Zookeeper集群中多个实例必须相互通讯,如果实例太多,网络通讯就会变得非常复杂,并且zookeeper在自身master挂掉,发生选举master节点期间是不对外提供服务的,这样的话系统会变得非常复杂
-
NameServer的设计目标是让网络通讯变得简单,从而使性能得到最大的提升,为了避免单点故障,NameServer必须以集群方式部署,但是集群节点之间是互相不通讯的,NameServer是无状态的,可以任意部署多个实例。Broker想每一台NameServer注册自己的路由信息,因此每一个NameServer都保存了一份完整的Broker路由信息。同时NameServer和每一台Broker保持长连接,每隔30s从路由注册表中将故障机器排除,NameServer降低了实现的复杂度,移除后并不会立刻通知Producer 和Consumer
-
因此 分析可以得出,Zookeeper是基于CP的架构,NameServer是基于AP的架构。
RoekctMQ 顺序消息发送技术原理
- RockerMQ中顺序消息有两种: 全局有序,局部有序
- 局部有序:指发送同一个队列的消息有序,可以在发送消息时候指定队列,这样在消费的时候也是按顺序消费的,例如同一个订单ID的消息我们可以通过Hash的方式选择发送到某个Queue,这样相同订单ID就能有序,而不同ID订单之间的逻辑就相互不影响
- 全局有序:设置Topic只有一个队列,那么就可以实现全局有序,创建Topic的时候手动设置只有一个Queue,这样就牺牲了高性能来达成全局有序
RockerMQ普通消息发送技术原理
- 普通消息与顺序消息对比,不同在于选择队列的策略不同,普通消息发送选择饿队列有两种机制:
- 轮询机制(默认):一个Topic有多个队列,轮询选择其中一个队列,轮询机制原理是路由信息TopicPublishInfo中维护了一个计数器sendWitchQueue,每发送一次消息都需要查询一次路由每次+1,通过计数器的index 与Queue的数量取模来实现轮询算法,
- 轮询的弊端在于,如果其中一个Broker宕机来,因为ServerName中不会立刻剔除改Broker,经过取模后可能还是路由到宕机的那台,造成无法规避发送失败的情况,因此就有了故障规避机制
- 故障规避机制:开启故障规避机制后,如果新消息发送失败会将broker-a“悲观”的认为在接下来的一段时间内钙broker是不可用的,在之后的一段时间内所有客户端不会向这个broker发送消息。具体的延迟时间计算:在延迟时间计算时候,定义了两个数组,一个数组定义了各种量级的超时时间,另外一个数据定义了同样多的故障排除时间,也就是RockerMQ更具故障发生时候消息处理的时间来定义这个故障需要多久才能排出,这个数据是根据使用经验得出来的。
- 故障规避的弊端:如果所有Broker因为某一瞬间的压力特别大导致所有Broker都触发故障规避,此时Broker是可用的的,但是规避机制导致没有Broker可用,从而导致服务不可用的呃情况。所以RockerMQ是默认不开启故障规避机制的
- 故障规避的弊端:如果所有Broker因为某一瞬间的压力特别大导致所有Broker都触发故障规避,此时Broker是可用的的,但是规避机制导致没有Broker可用,从而导致服务不可用的呃情况。所以RockerMQ是默认不开启故障规避机制的
- 轮询机制(默认):一个Topic有多个队列,轮询选择其中一个队列,轮询机制原理是路由信息TopicPublishInfo中维护了一个计数器sendWitchQueue,每发送一次消息都需要查询一次路由每次+1,通过计数器的index 与Queue的数量取模来实现轮询算法,
RockerMQ消息发送三种方式
- 同步:发送消息后,同步等待Broker服务器的返回结果,支持失败重试,适用于很重要的消息通知
- 异步:发送消息后,不会阻塞当前线程,不支持失败重试,发送方可以通过回调接口的方式接受服务器的响应,并对响应结果做处理,但是这个响应的回调也是异步的,类似一个监听,适用于对响应时间有很高要求的场景
- 单向:单向发送原理和异步一样,单不支持回调函数,适用于响应时间非常短,对可靠性要求不高,比如批量的营销短信,日志收集这些场景。
消息消费的技术原理
- RocketMQ支持两种消费模式:集群消费(Clustering)和广播消费(Broadcasting)。两者区别在于,广播消费模式下,消息会被ConsumerGroup中的每一个Consumer消费,在集群消息模式下每一条消息只会被ConsumerGroup中的一个Consumer消费。
- 多数情况下是集群消费模式,每次消息代表一次业务处理,集群消费用的比较少,例如用来更新缓存
顺序消费
- 顺序消费情况下,原理是一个消息队列只允许一个Consumer中的一个消费线程拉取消费。Consumer中有一个消费线程池,多个线程会同时取消费消息。在顺序消费场景下,消费线程请求到Broker时会先申请独占锁,获得独占锁的线程才允许消费
- 消息消费成功后会向Broker提交消费进度ACK,更新消息消费点位,避免下次拉取到已经消费的消息。顺序消费中如果消费线程在进行业务处理时候抛出异常,则不会提交消费进度,消费进度会阻塞在当前这条消息。因此此种情况也不会继续消费之后的消息从而保证消费的有序性。因此如果程序有Bug导致无法消费,会一直阻塞直到达到重试次数上限而造成消息堆积。
并发消费
-
并发消费是默认消费方式,同一个消费队列提供给Consumer中多个消费线程览区消费。Consumer中会维护一个消费线程池,多个消费线程可以并发取同一个消息队列中拉取消息。
-
异常队列 & 死信队列:
- 重试队列:在Consumer由于业务异常导致消费失败,将消费失败的消息重新发送给Broker保存在重试队列,这样设计的原因是不能影响整体的消费进度,又必须放置消息丢失的问题。重试队列放在单独的一个Topic中,不在原有的Topic,Consumer会自动订阅这个Topic。
- 死信队列:由于业务逻辑的Bug导致Consumer对部分消息长时间消费失败重试失败,为了保证重试队列的消息能继续消费,并且异常消费的消息不丢失,会将超过最大重试次数的消息放入死信队列中。消息进入死信队列不会自动消费,需要人工处理。同样死信队列也有一个单独的Topic
-
重试机制:
- 通常故障恢复需要一定时间,如果不间断的重试,重试又失败情况概率大。所以RocketMQ重试机制采用衰退的模式,首次10s,之后30s,依次递增每次重试时间间隔都会加长直到重试次数达到16次默认次数,会将消息丢到死信队列。
消息幂等性
- RockerMQ不保证消息不被重复消费,如果业务对消息重复非常敏感,必须在业务层进行幂等性处理。
- 所有消息系统中消费消息有三种模式:at-most-once(最多一次),at-last-once(最少一次),exactly-only-once(精确投递一次),分布式系统都是三者之间取平衡,前两个是广泛使用的:
- at-most-once(最多一次):消息投递后不论是否成功不重复投递,有可能导致消息没有被消费的情况,RockerMQ没有使用
- at-last-once(最少一次):消息透体之后,消费完成,需要向服务返回ACK(确认机制)没有消费则一定不返回ACK,由于网络一次,客户端重启,服务器未能收到ACk,服务器会重新投递,这就导致了重复消费问题,RockerMQ利用ACK机制保障消息至少被消费一次
- exactly-only-once(精确投递一次):必须满足两点,发送消息不允许重复,消费消息不允许重复,在分布式系统中这种模式开销特别大,性能必然降低,但是安全性最高,RockerMQ追求极致性能也没有用这种情况。
业务幂等性处理
- 因为MQ无法保证消息的重复消费问题,那么我们需要在消费消息的是对消费进行一个幂等性消费处理,有如下两种方式:
- 第一种,利用消息消费时候给丁的group+MsgId的唯一性来做一次入库操作,每次操作之前,查询这条消息是否已经处理过,如果已经处理,则直接返回SUCCESS即可,否则正常消费
- 第二种,直接将消息消费的逻辑设计成幂等性的逻辑,例如对例如在修改的时候利用乐观锁的方式,增加一个版本号,版本号一致才会修改成功。
事务消息
- 单机事务场景我们可以通过Spring 中Transaction注解来保持业务的事务ACID。但是分布式事务问题我们没法在单台JVM里面解决,因此RockerMQ事务消息可以用来解决分布式事务ACID。
- RockerMQ用两阶段提交协议2PC来提交事物消息。第一阶段,Producer向Broker发送预处理消息,此消息还不能被消费,第二阶段Producer向Broker发送提交或者回滚消息。流程如上图所说
- Producer发送预处理消息,Broker 返回ACK
- producer提交本地事物,commit or rollback
- producer 将本地事物执行结果提交给Broker
- 如果第三步骤中因为网络故障无法投递结果信息给Broker,Broker会主动发起一次消息回查
- 依据3,4 步骤中得到的事物执行结果 决定是否需要将预提交信息投递给Consumer
- Consumer 通过RockerMQ的重试机制来做到最终一致性。
RockerMQ 高性能设计
- RockerMQ 高性能设计体现在三个方面:
- 数据存储设计:顺序写盘,消息队列设计,消息跳跃读,数据零拷贝
- 动态伸缩:消息队列扩容,Broker扩容
- 消息实时投递
存储设计
- RockerMQ的高吞吐量的关键就在于存储方式的设计。数据存储核心有两个部分,一个是CommitLog数据存储文件,一个是ConsumerQueue队列消费问题,Producer将消息发送到Broker,Broker服务将消息存储到CommitLog,在由CommitLog转发到ConsumerQueue提供给各个Consumer消费
顺序写盘
- 因为在之前机械硬盘时代一次次磁盘的读写过程分为三个步骤:
- 磁头寻址:磁头移动到指定的磁道,时间长,找到数据在磁盘的那个地方
- 磁盘转动:磁盘的转动来找到数据所在的扇区,时间短
- 数据传输:数据通过系统总线传输到内存,时间短
- 磁盘读写依赖两个机械部分刺针和磁盘转动来找数据,而机械运动与内存读写存在指数级别的效率差异,因此时间都花在了寻址上,随机写回导致磁头不断寻址,时间都花在了机械运动上,顺序写则没有寻址过程。
- 存储位置:
- CommitLog文件复杂存放消息数据,所有文件都在 HOME/store/commitlog文件夹下,消息写入commitLog是加锁串行追加写入。
- 存储方式:
- CommitLog存储没有区分Topic,存储了所有数据从而保证数据是完全的顺序写
- 文件结构:
- 每个CommitLog文件默认1G大小,写满后在新键文件,文件命名按照文件开始的字节偏移量offset命名,固定长度20 因此第一个文件名就是20个0。第二文件名就是偏移量1G开始
消费队列设计
- 当消息到达CommitLog后,会通过线程异步几乎实时的将消息转发给消费队列文件ConsumerQueue保存,每个queue都有自己的ConsumerQueue文件
- 在CommitLog中所有topic的数据都是混合在一起的,但是消费是分Topic消费的,因此如果消费之间读取commitLog那么性能会非常差,吞吐量低,为了解决文件顺序写对读取的不友好,RockerMQ设计了另外一个存储ConsumerQueue,
- ConsumerQueue复杂消费队列文件,相对于CommitLog他类似一个索引文件,因为他只存储了CommitLog offset,消息size,消息tag 对应的hashcode值
- 因为在ConsumerQueue中每一条记录是固定的,长度固定,存储上没一个ConsumerQueue的大小是30W个记录,也是固定大小文件命名规则同样也是按偏移量来命名
- 集群模式下读取流程
- 当Consumer读取一个数据,Broker会记录客户端的消费偏移量offse,
- 通过消费偏移量offset计算出当前在那个ConsumerQueue中
- 然后取出对应的CommitLog offset,offset 与 1G 数字取模,计算出CommitLog文件名
- 在利用size + tag Hashcode 取对应的详细数据
消息跳跃读
- 为了提高文件读的新能,除了磁盘顺序读写的文件设计以外,还使用了操作系统的Page Cache机制
- RockerMQ 读取消息依赖操作系统的PageCache,PageCache命中率越高读取性能越高。因此操作系统会尽量做预读数据,让应用尽可能的在pageCache中读取数据,流程如下:
- 检查要读的数据是否在与读取的Cache中
- 如果没有在cache中,操作系统会去磁盘中读取数据页,并且将改书记页之后的连续多个页一起读入cache中,在将数据返回给应用,这种方式称为跳跃读
- 如果在Cache中,说明上次缓存的数据是有效的,操作认为你在顺序读磁盘,他会继续扩大缓存数据的范围,将之前缓存的页面的之后几个页面继续读到cache中
数据零拷贝
- 数据零拷贝主要有两种方式,java NIO中的 MappedByteBuffer 内存映射 和sendFile
- RockerMQ中文件读写主要通过Java NIO中的MappedByteBuffer 来进行内存映射,可以让用户态共享内核缓冲区,减少了两次内核缓冲区都用户态数据拷贝的过程,提高了读写速度。
Kafka高性能
- 利用Partition实现并行处理
- 顺序写磁盘 与RockerMQ一样
- 充分利用pageCache,消息跳跃读
- 零拷贝,Kafka零拷贝与RockerMQ用的不一样,他使用的是SendFile的技术,他不经过用户缓冲区,可以直接在两个内核缓冲区去进行考吧,只需要给Kafka一个文件描述符来进行指定内核缓冲区地址既可以做到消除两次CPU拷贝
- 批处理 + 数据压缩
消息实时投递
- 任何一款消息中间件都有两种模式:push,pull各有各的优缺点适用不同场景
- Push模式:当消息发送到服务端时候,由服务端推送给客户端Consumer
- 优点是Consumer能及时的收到数据
- 缺点是Consumer消费是耗时的,消费推送速度大于消费速度的时候,Consumer消息消费不过来可能造成缓冲区溢出,并且消息推送次数一多给服务端造成压力
- Pull模式:客户端Consumer自动拉数据,每隔一段时间字轮询
- 优点是可以更具当前Consumer的消费速度去获取消息
- 缺点是大多数时候可能都是无效的请求没有数据获取到,从而浪费网络资源和服务端资源
- Push模式:当消息发送到服务端时候,由服务端推送给客户端Consumer
- RocketMQ优化模式:
- RockerMQ提供了一种推/拉结合的长轮询机制来平衡推拉的缺点。还是由于Consumer发送pull请求,如果服务端有数据则直接返回,如果没有数据。不会立刻释放请求,而是挂起请求缓存到本地,等有数据的时候,由Broker本地的一个线程去检查挂起的请求在将消息放好Consumer
RocketMQ 如何进行路由注册,路由发现,路由剔除
- 路由注册:由Broker每隔30s向NameServer发送心跳包,心跳包中包好当前broker的一些基础信息,NameServer通过处理Broker的心跳包来完成Broker的注册以及服务续期
- 路由发现:路由发现并非通过NameServer的push来同步的,而是客户端定时去拉去NameServer中最新的路由信息然后更新本地路由表
- 路由剔除:存在两种请求,第一种:NameServer每10s检查所有Broker信息,但是要Broker的心跳时间超过120s没有续期才会剔除。第二种Broker的正常关机,会调用执行unregisterBroker指令,这个方法会执行NameServer中该Broker的信息更新。
RocketMQ 高可用设计 & RocketMQ中Broker部署方式
- 消息发送高可用: Broker宕机后,NameServer检测到Broker有延迟,NameServer每10s检查所有Broker信息,但是要Broker的心跳时间超过120s没有续期才会剔除,所以Producer不能及时感知Broker的下限,期间很多消息会发送失败,NameServer这样设计的目的在于,NameServer追求的是高性能,而对于Broker检测,Producer通知都需要复杂的通讯机制,使系统变得复杂,因此高可用实现依赖发送端的重试机制 + 故障延迟机制
- 消息存储高可用:通过消息持久化(同步刷盘,异步刷盘),主从复制,读写分离机制
- 消息消费高可用:主要依赖消费重试 与 ACK机制保障
- 集群管理高可用:NameServer是无状态的,单点宕机不影响,只有一个NameServer也存储不所有Broker的路由信息,全部宕机也不影响现有的已经在允许的Producer与Consumer
- RockerMQ架构设计高可用:线上RockerMQ我们可以采用Master - Slave机制,在依托异步刷盘,同步复制的机制来提供架构级别的高可用,Master节点支持读,写,slave节点只支持读
- 同步复制,消息发送时候,必须复制到slave节点,才返回success,这就能保证数据不丢失,即使Master宕机或者特别繁忙,slave也有全部数据,consumer端能从slave消费到所有数据,只是producer无法写入新的数据
- 异步刷盘,在producer写入数据到master的内存,就可以认为写成功写入master,不用完成磁盘写入的高消耗操作,提升吞吐量
- 升级版本,我们可以采用多master。多slave 结构,将一个topic的数据通过一定规则分别投递到多个master节点上,这样即使其中一个master宕机也不会造成服务不可用情况。
RockerMQ的总体架构以及每个组件的功能
- RocketMQ的客户端包括 发送数据的Producer,接收消费数据的Consumer,客户端中有一个重要的组件是NRC(Netty Remoting Client),在Prodcer或者Consumer 在生产或者消费的时候,需要先与NameServer进行通讯来获取Broker的注册信息topic等信息,因此NRC就是负责与NRS(Netty Remoting server)通讯的组件
- Broker是最核心的组件,我们Producer发送消息,Consumer拉取消息都是在Broker中通讯获取的,同HertBeats的心跳保持也是发送到Broker。同时Broker接收到消息的callback也需要与Producer进行通讯,所以Broker 中既有 NRC ,也有NRS。 Broker最重要的是文件存储组件 Store Service,作用是接收到消息将信息从内存落入文件中利用MMap 零拷贝,同时读取消息时候,从文件读取到内存
- NameService:中主要用来注册中心的节点,存储了Broker的基础信息,所以主要是用来和Broker,producer,consumer的通讯,因此最重要的组件就死NRS(Netty Remoting Server)
同步刷盘与异步刷盘
-
同步刷盘:当消息写入到内存胡,会等待数据写入到磁盘的CommitLog文件,才会返回成功。RockerMQ后台刷盘线程提交刷盘任务到队列,每隔10毫秒执行一次刷盘任务,用批量刷盘的方式提升写入性能提升IO吞吐量降低压力
- 刷盘任务有两个,一个写队列,负责存储写入数据,一个读队列在刷盘之前将写队列数据放入读队列然后执行刷盘,读写分离互不影响
-
异步刷盘:RockerMQ默认用的异步刷盘,异步刷盘可以开启缓冲池策略:
- 不开启缓冲池,是默认策略,线程会每隔500ms去刷盘,并且刷盘之前判断上一次执行时间超过10s,并且需要刷盘的数据超过4页 16KB,这样即使宕机也只会损失10s 或者16KB的数据。
- 开启缓冲池:RockerMQ会申请一块和CommitLog一样大小的堆外内存做缓冲池,数据优先写入缓冲池,同样每隔500ms尝试提交到文件通道等待刷盘,与不开启缓冲池一样,安时间 + 刷盘数据多条件判断是否刷盘。从而提升IO性能
主从复制
- 同步复制:Master服务器和Slave都写入成功后才会返回给客户端写入成功的状态
- 优点:保障数据安全性,Master宕机,Slave有全量数据
- 缺点:写入数据延迟,消息发送过程变长,降低系统吞吐量
- 异步复制:Master服务器写成功后就返回成功,之后异步的写入Slave
- 优点:写入数据更快,消息发送时间变短,提升吞吐量
- 缺点:master宕机后Slave中数据是缺失的。
- 实际生产环境,我们用异步刷盘 + 同步复制的组合,保障每次发送数据都只有一次文件的顺序写,在性能与数据安全性之间做折中。
有几百万消息持续积压几个小时,解决方案
- 一个消费者1s 消费1000条数据,共三台消费者3000条/s,一分钟18W,如果积压了上千万,需要差不多一个小时的时间才能恢复。
- 一般这个时候只能紧急扩容,具体操作如下:
- 首先先修改Consumer问题,确保消费的Bug已经修复
- 第二新建一个Topic ,Queue是原来的10倍,然后写一个临时的分发程序,不做任何事情,只是消费原来Queue中积压的数据,并且在发送到RockerMQ中,发送时候会依据轮询的算法将原先积压的数据散列到扩容10倍后的机器中
- 第四,扩容原来Consumer消费,也同样扩容到原来的10倍,再次进行消费,因为扩容后速度是原来的10倍速度,等消息全部消费完后,在恢复到原来的架构
- RockerMQ是支持动态扩容的。
RockerMQ过程中遇到的问题
- 消息积压问题: 上面方案解决
- 消息丢失问题 x
- 消息重复消费问题:唯一id方式 , 业务幂等性方式
- RockerMQ内存不够OOM问题:
- 查看RockerMQ日志,定位具体原因,如果是内存不足,则增加内存配置
- 查看RockerMQ的JVM参数配置,例如堆内存,元数据区域配置
- 查看RockerMQ的消息存储机制,有两种机制,一种是内存存储,一种是文件存储,如果设置的是内存存储的机制,那么最好改成文件存储的方式,内存存储方式对机器内存要求更高并且还存在数据丢失问题
- 查看RockerMQ的消息消费 & 生产速度是否过快,可以通过增加Topic动态扩容,并且同时增加Consumer的方式来缓解
Slave节点保证的高可用
- 由于消息分布在各个broker上,一旦某个broker宕机,则broker上消息读写会收到影响,所以RockerMQ提供Master/Slave结构。slave同步复制/异步复制从master同步数据,如果master宕机,则slave提供考费服务,但是不能提供写入服务,此过程对应用透明,由rocketMQ内部解决。
- 两个关键点:
- master宕机,默认情况最多30秒才能感知,这个时间内producer发送的消息都是失败的,consumer 消费消息失败
- consumer得到master宕机信息后,转向Slave消费(重定向),但是slave不能保证master消费100%同步过去,因此会有少量消息丢失。但是消息最终不会丢,master恢复后,未同步过去的消息会被重新消费,只是延迟了。
RockerMQ延迟消息实现
- 消息线写入commitLog文件
- 消费线程将数据保存在SCHEDULE_TOPIC_XXX的topic中,并且以延迟力度分queueId区分
- 定时任务扫描SCHEDULE_TOPIC下面的每一个Queue,分别为每一个Queue分配一个线程消费,到时间后就写入consumerQueue(实时队列)中。
- 如下目录,安时间区分度来分queue,并且文件存储也是单独的,单时间到了,就从对于延迟队列的文件中拷贝到对应的 实时消费队列中。
- 总结:
- 优点:设计简单,将相同维度时间的消息归位一类queue中,定时扫描可以保证消费有序
- 缺点:定时器扫描用的timer,timer是单线程,如果延迟消息量大,可能处理过不来导致到时间没发出去。
- 改进点:每个队列各分配一个timer,或者用timer扫描只坐任务分发,到时间了将任务交给一个线程池中线程完成。类似单Reactor 多线程模型。
- 目录结构:
RocketMQ 提高的负载均衡策略
-
平均负载均衡
- 把消费者进行排序;
- 计算每个消费者可以平均分配的 MessageQueue 数量;
- 如果消费者数量大于 MessageQueue 数量,多出的消费者就分不到;
- 如果不可以平分,就使用 MessageQueue 总 数量对消费者数量求余数 mod;
- 对前 mod 数量消费者,每个消费者加一个,这样就获取到了每个消费者分配的 MessageQueue 数量。
-
循环分配
-
自定义分配策略
-
按照机房分配策略:这种方式 Consumer 只消费指定机房的 MessageQueue
-
按照机房就近分配:跟按照机房分配原则相比,就近分配的好处是可以对没有消费者的机房进行分配
分布式系统中定时任务所在服务器时间不一致解决方案
- 分布式系统由于各个阶段服务器时间不一致,可能导致定时任务执行出现偏差,有以下解决方案:
- 使用NTP同步时间:NTP(Network Time Protocol)是一种用于同步计算机系统时间的协议,在分布式系统中,使用NTP将各个节点服务器时间同步到一个统一的参考时间,从而保证服务的执行时间一致性。
- 使用统一的时间服务:可以搭建一个专门的时间服务器,所有节点都向这个服务器请求时间,并以这个服务器的时间作为统一的参考时间。例如用Redis缓存的系统时间
- 任务调度中心:搭建一个中心化的任务调度中心,所有任务都由他来统一调度分发,这样就以中心节点的时间作为最终的参考时间
- 使用分布式锁:可以使用分布式锁保证同一个时刻只有一个节点执行任务,其他节点等待锁的释放后在执行。