RocketMQ 调研问题记录

实践&基础知识

首先贴出我参考的实践类的文章

  • springboot的RocketMq实例
  • RocketMQ 入门 —— 基于 Spring Boot 使用 RocketMQ
  • RocketMQ最佳实践
  • RocketMQ最佳实践,就看这一篇!
  • RocketMQ的最佳实践
  • RocketMQ事务消费和顺序消费详解

关于实践以及RocketMQ的基础知识,网上已经不胜繁多,这里不多做赘述,这篇文章想主要讨论的,是我在对接使用过程中思考到的几个问题:

  • RocketMQ nameServer 无状态是什么意思,应该怎么理解?
  • RocketMQ 如何实现的多个队列下的消息发送顺序控制?
  • RocketMQ 如何实现保证了消息的不可重复消费?

问题列表

RocketMQ nameServer 无状态如何理解?

Name Server 是专为 RocketMQ 设计的轻量级名称服务,具有简单、可集群横吐扩展、无状态,节点之间互不通信等特点。整个Rocketmq集群的工作原理如下图所示:


Rocketmq集群架构

我们会发现RocketMQ架构中说道,nameServer是无状态的,这个应该怎么理解呢?
首先是无状态,无状态指的是前后通信并不会互相影响,相对的,每次通信都需要携带完成一次请求的所有信息
(cookie、session就是无状态请求的一种解决方案,详情如下:HTTP是一个无状态的协议。这句话里的无状态是什么意思? - 灵剑的回答 - 知乎)
那就引申下一个问题:

无状态、且节点间不同信的前提下,nameSever如何做到保持数据的最终一致?

引申:为什么要使用NameServer?

NameServer作为一个名称服务,需要提供服务注册、服务剔除、服务发现这些基本功能,但是NameServer节点之间并不通信,在某个时刻各个节点数据可能不一致的情况下,如何保证客户端可以最终拿到正确的数据。下面分别从路由注册、路由剔除,路由发现三个角度进行介绍。

3.1 路由注册

对于Zookeeper、Etcd这样强一致性组件,数据只要写到主节点,内部会通过状态机将数据复制到其他节点,Zookeeper使用的是Zab协议,etcd使用的是raft协议。

但是NameServer节点之间是互不通信的,无法进行数据复制。RocketMQ采取的策略是,在Broker节点在启动的时候,轮训NameServer列表,与每个NameServer节点建立长连接,发起注册请求。NameServer内部会维护一个Broker表,用来动态存储Broker的信息。

同时,Broker节点为了证明自己是存活的,会将最新的信息上报给NameServer,然后每隔30秒向NameServer发送心跳包,心跳包中包含 BrokerId、Broker地址、Broker名称、Broker所属集群名称等等,然后NameServer接收到心跳包后,会更新时间戳,记录这个Broker的最新存活时间

NameServer在处理心跳包的时候,存在多个Broker同时操作一张Broker表,为了防止并发修改Broker表导致不安全,路由注册操作引入了ReadWriteLock读写锁,这个设计亮点允许多个消息生产者并发读,保证了消息发送时的高并发,但是同一时刻NameServer只能处理一个Broker心跳包,多个心跳包串行处理。这也是读写锁的经典使用场景,即读多写少。

3.2 路由剔除

正常情况下,如果Broker关闭,则会与NameServer断开长连接,Netty的通道关闭监听器会监听到连接断开事件,然后会将这个Broker信息剔除掉

异常情况下,NameServer中有一个定时任务,每隔10秒扫描一下Broker表,如果某个Broker的心跳包最新时间戳距离当前时间超多120秒,也会判定Broker失效并将其移除。

特别的,对于一些日常运维工作,例如:Broker升级,RocketMQ提供了一种优雅剔除路由信息的方式。如在升级一个节Master点之前,可以先通过命令行工具禁止这个Broker的写权限,发送消息到这个Broker的请求,都会收到一个NO_PERMISSION响应,客户端会自动重试其他的Broker。当观察到这个broker没有流量后,再将这个broker移除。

3.3 路由发现

路由发现是客户端的行为,这里的客户端主要说的是生产者和消费者。具体来说:

对于生产者,可以发送消息到多个Topic,因此一般是在发送第一条消息时,才会根据Topic获取从NameServer获取路由信息。
对于消费者,订阅的Topic一般是固定的,所在在启动时就会拉取。

那么生产者/消费者在工作的过程中,如果路由信息发生了变化怎么处理呢?
如:Broker集群新增了节点,节点宕机或者Queue的数量发生了变化。细心的读者注意到,前面讲解NameServer在路由注册或者路由剔除过程中,并不会主动推送会客户端的,这意味着,需要由客户端拉取主题的最新路由信息。

事实上,RocketMQ客户端提供了定时拉取Topic最新路由信息的机制,这里我们直接结合源码来讲解。

DefaultMQProducer和DefaultMQConsumer有一个pollNameServerInterval配置项,用于定时从NameServer并获取最新的路由表,默认是30秒,它们底层都依赖一个MQClientInstance类。

MQClientInstance类中有一个updateTopicRouteInfoFromNameServer方法,用于根据指定的拉取时间间隔,周期性的的从NameServer拉取路由信息。 在拉取时,会把当前启动的Producer和Consumer需要使用到的Topic列表放到一个集合中,逐个从NameServer进行更新。以下源码展示了这个过程:

public void updateTopicRouteInfoFromNameServer() {
 
    //1 需要更新路由信息的Topic集合
    Set topicList = new HashSet();
 
    //2 添加消费者需要使用到的Topic到集合中
    {
        Iterator> it = this.consumerTable.entrySet().iterator();
        while (it.hasNext()) {
            Entry entry = it.next();
            MQConsumerInner impl = entry.getValue();
            if (impl != null) {
                Set subList = impl.subscriptions();
                if (subList != null) {
                    for (SubscriptionData subData : subList) {
                        topicList.add(subData.getTopic());
                    }
                }
            }
        }
    }
 
    //3 添加生产者需要使用到的topic到集合中
    {
        Iterator> it = this.producerTable.entrySet().iterator();
        while (it.hasNext()) {
            Entry entry = it.next();
            MQProducerInner impl = entry.getValue();
            if (impl != null) {
                Set lst = impl.getPublishTopicList();
                topicList.addAll(lst);
            }
        }
    }
 
    //4 逐一从NameServer更新每个Topic的路由信息
    for (String topic : topicList) {
        this.updateTopicRouteInfoFromNameServer(topic);
    }
}

然而定时拉取,还不能解决所有的问题。因为客户端默认是每隔30秒会定时请求NameServer并获取最新的路由表,意味着客户端获取路由信息总是会有30秒的延时。这就带来一个严重的问题,客户端无法实时感知Broker服务器的宕机。如果生产者和消费者在这30秒内,依然会向这个宕机的broker发送或消费消息呢?
这个问题,可以通过客户端重试机制来解决:

引申:生产者重试机制

RocketMQ 如何实现的多个队列下的消息发送顺序控制?

内容转自(https://juejin.im/post/5de3c8026fb9a07194761641)
以一个业务场景举例,比如订单的创建、支付、发货、收货。
一个topic下有多个队列,为了保证发送有序,RocketMQ提供了MessageQueueSelector队列选择机制,他有三种实现:

  • 我们可使用Hash取模法,让同一个订单发送到同一个队列中,再使用同步发送,只有同个订单的创建消息发送成功,再发送支付消息。这样,我们保证了发送有序。

  • RocketMQ 的topic内的队列机制,可以保证存储满足FIFO(First Input First Output 简单说就是指先进先出),剩下的只需要消费者顺序消费即可。

  • RocketMQ 仅保证顺序发送,顺序消费由消费者业务保证!这里很好理解,一个订单你发送的时候放到一个队列里面去,你同一个的订单号Hash一下是不是还是一样的结果,那肯定是一个消费者消费,那顺序是不是就保证了?

这一段内容可能更偏理论,更还有另一篇文章讲的也很好:RocketMQ顺序消息

如何实现保证了消息的不可重复消费?

详情戳:RocketMQ不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重。

参考文章

  • 深入理解NameServer
  • 阿里RocketMQ如何解决消息的顺序&重复两大硬伤?

欢迎大家关注我的公众号


半亩房顶

你可能感兴趣的:(RocketMQ 调研问题记录)