RocketMq总结

RocketMq总结

转载自:https://zhuanlan.zhihu.com/p/525640488

一、RocketMq组成

进行一个比喻,在RocketMq中有四个部分组成,分别是Producer,Consumer,Broker,以及NameServer,类比于生活中的邮局,分别是发信者,收信者,负责暂存,传输的邮局,以及协调各个地方邮局的管理机构。

二、Broker

是消息的中转中心,负责消息的存储以及转发

前面提到对于Broker来说相当于一个负责暂存的邮局,也可以把其理解成为提供业务的服务器。

  • 单个Broker节点与所有的NameServer节点保持长连接及心跳,并会定时将Topic信息注册到NameServer,顺带一提底层的通信和连接都是基于Netty实现的。
  • Broker负责消息存储,以Topic为纬度支持轻量级的队列,单机可以支撑上万队列规模,支持消息推拉模型。
  • 官网上有数据显示:具有上亿级消息堆积能力,同时可严格保证消息的有序性

三、NameServer

主要负责对于数据源的管理,包括了对于Topic和路由信息的管理.

对于NameServer 来说,是一个功能齐全的服务器,和zookeeper来说有很多的相似的地方,但是更加轻便一些。主要就是说 对于namersever来说每一个都是互相独立的,没有很多的信息的交互。

NameServer 的压力不会很大,在平时主要用于维护心跳检测,和提供Topic-Broker之间的关系数据。

但是需要注意的是对于Broker在向NameServer发送心跳的时候,会带上自己当前所负责的所有的Topic信息,这些信息如果太多的话,可能在一次的心跳检测的过程中出现网络波动,导致失败,最终就会导致NameServer 误认为Broker心跳检测失败。

对于NameServer 来说 是被设计成为无状态的,可以进行横向的扩展,节点之间相互并没有数据的通行,通过部署多台机器可以伪装成一个集群。

对于有状态和无状态的解释:无状态就是说不会保存过多的数据信息,例如我们的Web站点就被设计成为是无状态的,每一次的登陆都需要进行信息的验证,但是对于有状态的寓意会保存数据信息,也就是说 服务端会记录每次回话的客户端信息,从而在客户端接入链接的时候能够识别客户端。典型的设计就是tomcat中的session。

对于每一个Broker 来说 在启动的时候,都会到NameServer中进行一个注册,对于生产者还是消费者,在进行对应的操作之前会根据对应的Topic到NameServer中获取到Broker的路由信息。

四、Producer

消息的生产者,一般由业务系统来生产对应的业务消息。消费者进行数据的消费。

Producer由用户进行分布式部署,消息由Producer通过多种负载均衡模式发送到Broker集群,发送低延时,支持快速失败。

RocketMQ 提供了三种方式发送消息:同步、异步和单向

  • 同步发送:同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信。
  • 异步发送:异步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务。
  • 单向发送:单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集。

五、Consumer

消息消费者,负责消费消息,一般是后台系统负责异步消费。

Consumer也由用户部署,支持PUSH和PULL两种消费模式,支持集群消费和广播消息,提供实时的消息订阅机制。

  • Pull:拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。
  • Push:推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。

六、实现

  1. Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳。
  2. Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息。
  3. Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费

七、其他

7.1 Message

一条消息必须有一个主题(Topic),主题可以看做是你的信件要邮寄的地址。

一条消息也可以拥有一个可选的标签(Tag)和额处的键值对,它们可以用于设置一个业务 Key 并在 Broker 上查找此消息以便在开发期间查找问题。

7.2 Topic

Topic(主题)可以看做消息的规类,它是消息的第一级类型。比如一个电商系统可以分为:交易消息、物流消息等,一条消息必须有一个 Topic 。

Topic 与生产者和消费者的关系非常松散,一个 Topic 可以有0个、1个、多个生产者向其发送消息,一个生产者也可以同时向不同的 Topic 发送消息。

一个 Topic 也可以被 0个、1个、多个消费者订阅。

7.3 Tag

Tag(标签)可以看作子主题,它是消息的第二级类型,用于为用户提供额外的灵活性。使用标签,同一业务模块不同目的的消息就可以用相同 Topic 而不同的 Tag 来标识。比如交易消息又可以分为:交易创建消息、交易完成消息等,一条消息可以没有 Tag 。

标签有助于保持您的代码干净和连贯,并且还可以为 RocketMQ 提供的查询系统提供帮助。

7.4 Group

分组,一个组可以订阅多个Topic。

分为ProducerGroup,ConsumerGroup,代表某一类的生产者和消费者,一般来说同一个服务可以作为Group,同一个Group一般来说发送和消费的消息都是一样的

7.5 Queue

Kafka中叫Partition,每个Queue内部是有序的,在RocketMQ中分为读和写两种队列,一般来说读写队列数量一致,如果不一致就会出现很多问题。

Message Queue

Message Queue(消息队列),主题被划分为一个或多个子主题,即消息队列。

一个 Topic 下可以设置多个消息队列,发送消息时执行该消息的 Topic ,RocketMQ 会轮询该 Topic 下的所有队列将消息发出去。

消息的物理管理单位。一个Topic下可以有多个Queue,Queue的引入使得消息的存储可以分布式集群化,具有了水平扩展能力。

7.6 Offset

在RocketMQ 中,所有消息队列都是持久化,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用Offset 来访问,Offset 为 java long 类型,64 位,理论上在 100年内不会溢出,所以认为是长度无限。

也可以认为 Message Queue 是一个长度无限的数组,Offset 就是下标。

八、消息可靠性

8.1 生产者丢失

对于生产者丢失的情况出现在程序发送失败抛出来之后没有做重试处理,或者说发送的过程是成功的,但是网络闪退,MQ并没有收到信息,这个时候消息发送就失败了。

由于同步发送的一般不会出现这样使用方式,所以我们就不考虑同步发送的问题,我们基于异步发送的场景来说。

异步发送分为两个方式:异步有回调和异步无回调,无回调的方式,生产者发送完后不管结果可能就会造成消息丢失,而通过异步发送+回调通知+本地消息表的形式我们就可以做出一个解决方案。以下单的场景举例。

  1. 下单后先保存本地数据和MQ消息表,这时候消息的状态是发送中,如果本地事务失败,那么下单失败,事务回滚。
  2. 下单成功,直接返回客户端成功,异步发送MQ消息
  3. MQ回调通知消息发送结果,对应更新数据库MQ发送状态
  4. JOB轮询超过一定时间(时间根据业务配置)还未发送成功的消息去重试
  5. 在监控平台配置或者JOB程序处理超过一定次数一直发送不成功的消息,告警,人工介入。

一般而言,对于大部分场景来说异步回调的形式就可以了,只有那种需要完全保证不能丢失消息的场景我们做一套完整的解决方案。

8.2 MQ丢失

如果生产者保证消息发送到MQ,而MQ收到消息后还在内存中,这时候宕机了又没来得及同步给从节点,就有可能导致消息丢失。

比如RocketMQ:

RocketMQ分为同步刷盘和异步刷盘两种方式,默认的是异步刷盘,就有可能导致消息还未刷到硬盘上就丢失了,可以通过设置为同步刷盘的方式来保证消息可靠性,这样即使MQ挂了,恢复的时候也可以从磁盘中去恢复消息。

虽然我们可以通过配置的方式来达到MQ本身高可用的目的,但是都对性能有损耗,怎样配置需要根据业务做出权衡。

8.3 消费者丢失

消费者丢失消息的场景:消费者刚收到消息,此时服务器宕机,MQ认为消费者已经消费,不会重复发送消息,消息丢失。

在RocketMq中默认是需要消费者回复ack确认,若是说出现了消费者对这个数据完成了消费,但是没有返回ack确认信息,重发的机制会根据MQ的类型不同进行不同间隔的时间的重复发送。若是说出现了多次重复发送仍然不能够收到正确的Ack确定信息,就会进入死信队列。这个时候就需要外人干预处理。

如何保证数据的高容错

  • 在不开启容错的情况下,轮询队列进行发送,如果失败了,重试的时候过滤失败的Broker
  • 如果开启了容错策略,会通过RocketMQ的预测机制来预测一个Broker是否可用
  • 如果上次失败的Broker可用那么还是会选择该Broker的队列
  • 如果上述情况失败,则随机选择一个进行发送
  • 在发送消息的时候会记录一下调用的时间与是否报错,根据该时间去预测broker的可用时间

九、消息消费失败导致消息积压

考虑到时消息消费失败导致的消息积压可以从以下几个方面进行考虑:

  1. 消费者出错,肯定是程序或者其他问题导致的,如果容易修复,先把问题修复,让consumer恢复正常消费
  2. 如果时间来不及处理很麻烦,做转发处理,写一个临时的consumer消费方案,先把消息消费,然后再转发到一个新的topic和MQ资源,这个新的topic的机器资源单独申请,要能承载住当前积压的消息
  3. 处理完积压数据后,修复consumer,去消费新的MQ和现有的MQ数据,新MQ消费完成后恢复原状
  4. 若是说是因为消息发送太多,但是消费者太少的时候,可以增加线上的消费者,对数据进行大批量的消费。

十、Consumer和Queue出现了不对等情况,上线多台无法解决

  1. 准备一个临时的topic,在这个topic中queue的数量是堆积数量的多倍
  2. 把这些queue分配到多个Broker中
  3. 上线一台Consumer做为消息的搬运工,不进行任何业务的处理,把原来topic中积压的消息移动到新的topic中。
  4. 上线多个consumer同时对临时topic中的数据进行消费。
  5. 解决导致这个问题出现的bug,继续消费之前的Topic。

十一、如何实现负载均衡

主要通过Topic在多Broker中分布式存储实现。

11.1 producer

发送端指定message queue发送消息到相应的broker,来达到写入时的负载均衡

提升写入吞吐量,当多个producer同时向一个broker写入数据的时候,性能会下降 消息分布在多broker中,为负载消费做准备 默认策略是随机选择:

producer维护一个index 每次取节点会自增 index向所有broker个数取余 自带容错策略 其他实现:

SelectMessageQueueByHash hash的是传入的args SelectMessageQueueByRandom SelectMessageQueueByMachineRoom 没有实现 也可以自定义实现MessageQueueSelector接口中的select方法

MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);

11.2 consumer

采用的是平均分配算法来进行负载均衡。

其他负载均衡算法

平均分配策略(默认)(AllocateMessageQueueAveragely) 环形分配策略(AllocateMessageQueueAveragelyByCircle) 手动配置分配策略(AllocateMessageQueueByConfig) 机房分配策略(AllocateMessageQueueByMachineRoom) 一致性哈希分配策略(AllocateMessageQueueConsistentHash) 靠近机房策略(AllocateMachineRoomNearby

11.3 当消费负载均衡consumer和queue不对等的时候会发生什么?

Consumer和queue会优先平均分配,如果Consumer少于queue的个数,则会存在部分Consumer消费多个queue的情况,如果Consumer等于queue的个数,那就是一个Consumer消费一个queue,如果Consumer个数大于queue的个数,那么会有部分Consumer空余出来,白白的浪费了。

11.4 消息删除

对于在Broker中的消息消费完成之后 并不会立马的删除,每条消息都会持久化到CommitLog 中,每个Consumer连接到Broker后会维持消费进度信息,当有消息消费后只是当前Consumer的消费进度(CommitLog的offset)更新了。

11.5 进入死信队列

在消息重复消费失败之后经过了16次会进入死信队列中,源码如下:

public class SubscriptionGroupConfig {
    private int retryMaxTimes = 16;
}

// {@link org.apache.rocketmq.broker.processor.SendMessageProcessor#asyncConsumerSendMsgBack}
// maxReconsumeTimes = 16
int maxReconsumeTimes = subscriptionGroupConfig.getRetryMaxTimes();
// 如果重试次数大于等于16,则创建死信队列
if (msgExt.getReconsumeTimes() >= maxReconsumeTimes || delayLevel < 0) {
    // MixAll.getDLQTopic()就是给原有groupname拼上DLQ,死信队列
    newTopic = MixAll.getDLQTopic(requestHeader.getGroup());
    // 创建死信队列
    topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(xxx)
}

对于每一次的间隔时常如下:

public class MessageStoreConfig {
    // 每隔如下时间会进行重试,到最后一次时间重试失败的话就进入死信队列了。
    private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
}

信心的你一定会发现,明明是16次的重试,为什么会有18次的时间间隔,处理如下:

/**
 * {@link org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#sendMessageBack()}
 *
 * sendMessageBack()这个方法是消费失败后会请求他,意思是把消息重新放到队列,进行重试。
 */
public void sendMessageBack(MessageExt msg, int delayLevel, final String brokerName) {
    Message newMsg = new Message();
    // 淦    
    // !!!,3 + xxx。他是从第三个开始的。也就是舍弃了前两个时间间隔,18 - 2 = 16。也就是说第一次重试是在10s,第二次30s。
    newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes());
    this.mQClientFactory.getDefaultMQProducer().send(newMsg);
 }

11.6 消息去重

去重原则:使用业务端逻辑保持幂等性

幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用,数据库的结果都是唯一的,不可变的。

只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样,需要业务端来实现。

去重策略:保证每条消息都有唯一编号(比如唯一流水号),且保证消息处理成功与去重表的日志同时出现。

建立一个消息表,拿到这个消息做数据库的insert操作。给这个消息做一个唯一主键(primary key)或者唯一约束,那么就算出现重复消费的情况,就会导致主键冲突,那么就不再处理这条消息。 ———————————————— 版权声明:本文为CSDN博主「敖 丙」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_35190492/article/details/103341634

11.7 消息重复

消息领域有一个对消息投递的QoS定义,分为:

最多一次(At most once) 至少一次(At least once) 仅一次( Exactly once) QoS:Quality of Service,服务质量

几乎所有的MQ产品都声称自己做到了At least once。

既然是至少一次,那避免不了消息重复,尤其是在分布式网络环境下。

比如:网络原因闪断,ACK返回失败等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者。

不同的消息队列发送的确认信息形式不同,例如RabbitMQ是发送一个ACK确认消息,RocketMQ是返回一个CONSUME_SUCCESS成功标志,Kafka实际上有个offset的概念。

RocketMQ没有内置消息去重的解决方案,最新版本是否支持还需确认。

十二、消息消费

RocketMQ没有真正意义的push,都是pull,虽然有push类,但实际底层实现采用的是长轮询机制,即拉取方式

为什么说要主动拉去消息而不是使用事件的监听

事件驱动方式是建立好长连接,由事件(发送数据)的方式来实时推送。

如果broker主动推送消息的话有可能push速度快,消费速度慢的情况,那么就会造成消息在consumer端堆积过多,同时又不能被其他consumer消费的情况。而pull的方式可以根据当前自身情况来pull,不会造成过多的压力而造成瓶颈。所以采取了pull的方式。

CONSUMED_BUT_FILTERED:表示消息已经投递,但是已经被过滤掉了。例如producer发的是topicA,tagA,但是consumer订阅的却是topicA,tagB。CONSUMED_BUT_FILTERED是怎么发生的呢?

这个就要明白RocketMQ中的一个概念,消息消费要满足订阅关系一致性,即对同一个topic而言,同一个consumerGroup集群中,所有消费者订阅的topic和tag必须保持严格一致,不然就会造成消息丢失,此时消息的消费状态就是CONSUMED_BUT_FILTERED,相当于被同一集群下的某个消费者节点接收了,但是却直接丢弃了,没有走消费逻辑。

NOT_CONSUME_YET:能找到具体的消息,表明生产者成功将消息发送到了broker;同时表明消费者也在线,但消费者却迟迟没有消费。这说明消息发到了消费者所注册的分片queue之外的queue。

十三、消息可用性

当我们选择好了集群模式之后,那么我们需要关心的就是怎么去存储和复制这个数据,RocketMQ对消息的刷盘提供了同步和异步的策略来满足我们的,当我们选择同步刷盘之后,如果刷盘超时会给返回FLUSH_DISK_TIMEOUT,如果是异步刷盘不会返回刷盘相关信息,选择同步刷盘可以尽最大程度满足我们的消息不会丢失。

除了存储有选择之后,我们的主从同步提供了同步和异步两种模式来进行复制,当然选择同步可以提升可用性,但是消息的发送RT时间会下降10%左右。

RocketMQ采用的是混合型的存储结构,即为Broker单个实例下所有的队列共用一个日志数据文件(即为CommitLog)来存储。

而Kafka采用的是独立型的存储结构,每个队列一个文件。

十四、顺序消费

上业务用消息中间件的时候,是否需要保证消息的顺序性?
如果不需要保证消息顺序,为什么不需要?假如我有一个场景要保证消息的顺序,应该如何保证?

首先多个queue只能保证单个queue里的顺序,queue是典型的FIFO,天然顺序。多个queue同时消费是无法绝对保证消息的有序性的。所以总结如下:

同一topic,同一个QUEUE,发消息的时候一个线程去发送消息,消费的时候 一个线程去消费一个queue里的消息。

如何保证消费发到一个queue中

Rocket MQ给我们提供了MessageQueueSelector接口,可以自己重写里面的接口,实现自己的算法,举个最简单的例子:判断i % 2 == 0,那就都放到queue1里,否则放到queue2里。

十五、RocketMQ刷盘实现

Broker 在消息的存取时直接操作的是内存(内存映射文件),这可以提供系统的吞吐量,但是无法避免机器掉电时数据丢失,所以需要持久化到磁盘中。

刷盘的最终实现都是使用NIO中的 MappedByteBuffer.force() 将映射区的数据写入到磁盘,如果是同步刷盘的话,在Broker把消息写到CommitLog映射区后,就会等待写入完成。

十六、为什么不实用Zookeeper作为注册中心

  1. 因为对于我们熟知的CAP原理,同时最多只能够满足两个点,zookeeper满足的是CP,也就是说zookeeper并不能保证服务的可用性。zookeeper在进行选举的时候,整个选举的时间太长,期间整个集群都处于不可用的状态,而这对于一个注册中心来说肯定是不能接受的,作为服务发现来说就应该是为可用性而设计。
  2. 基于性能的考虑,NameServer本身的实现非常轻量,而且可以通过增加机器的方式水平扩展,增加集群的抗压能力,而zookeeper的写是不可扩展的,而zookeeper要解决这个问题只能通过划分领域,划分多个zookeeper集群来解决,首先操作起来太复杂,其次这样还是又违反了CAP中的A的设计,导致服务之间是不连通的。
  3. 持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每一个写请求,会在每个 ZooKeeper 节点上保持写一个事务日志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的一致性和持久性,而对于一个简单的服务发现的场景来说,这其实没有太大的必要,这个实现方案太重了。而且本身存储的数据应该是高度定制化的。
  4. 消息发送应该弱依赖注册中心,而RocketMQ的设计理念也正是基于此,生产者在第一次发送消息的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可用,短时间内对于生产者和消费者并不会产生太大影响。

十七、Broker是如何保存数据

RocketMQ主要的存储文件包括commitlog文件、consumequeue文件、indexfile文件。

Broker在收到消息之后,会把消息保存到commitlog的文件当中,而同时在分布式的存储当中,每个broker都会保存一部分topic的数据,同时,每个topic对应的messagequeue下都会生成consumequeue文件用于保存commitlog的物理位置偏移量offset,indexfile中会保存key和offset的对应关系。

十八、Broker注册

对于Broker来说会向所有的NameServer 上注册自己的信息,而不是某一个,是全部。

十九、Master 和Salve 之间是如何进行数据的同步

是根据raft协议进行同步:

  1. 在broker收到消息后,会被标记为uncommitted状态
  2. 然后会把消息发送给所有的slave
  3. slave在收到消息之后返回ack响应给master
  4. master在收到超过半数的ack之后,把消息标记为committed
  5. 发送committed消息给所有slave,slave也修改状态为committed

二十、事务与半事务如何具体实现

事务消息就是MQ提供的类似XA的分布式事务能力,通过事务消息可以达到分布式事务的最终一致性。

半事务消息就是MQ收到了生产者的消息,但是没有收到二次确认,不能投递的消息。

实现原理如下:

生产者先发送一条半事务消息到MQ

MQ收到消息后返回ack确认

生产者开始执行本地事务

如果事务执行成功发送commit到MQ,失败发送rollback

如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查

生产者查询事务执行最终状态

根据查询事务状态再次提交二次确认

最终,如果MQ收到二次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。

  1. CLIENT_SERVICE_NOT_OK
  • exception information
The xxx service state not OK, maybe started once
  • reason
    1)Starting multiple Producer/Consumer instances in the same JVM using the same Producer/Consumer Group may cause the client to fail to start.
  • solution
    1)Make sure only one Producer/Consumer instance is started for a given Producer/Consumer Group JVM.
posted @ 2022-11-30 10:19  JaxYoun  阅读(388)  评论(0编辑  收藏  举报