RocketMQ Producer高可靠架构源码剖析

780 阅读6分钟

概述

客户端消息的代码并不是很多,Producer是如何保障高可靠发送的呢? 经典的面试题,如何保证Producer消息不丢失等等?Producer消息的隔离机制?消息的重试机制?Producer负载均衡的策略? image.png

1、Producer 发送消息的四大步骤

四个核心步骤

(1)消息校验

(2)获取topic路由信息TopicPublishInfo

(3)根据topic的路由信息选择一个MessageQueue(明确往哪个broker发送)

(4)发送消息,成功则返回,超时或者失败则启用高可用策略。

1.1 消息校验

producer在发送消息的时候,就是选择合适的队列,通过队列找到合适的broker,将消息发送到broker中。

1.2 获取Topic路由信息

核心内容还是,根据topic获取对应的路由数据,然后从路由数据中找到合适的队列。

MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);

1.3 根据topic负载均衡算法选择一个MessageQueue

  1. 是否开启消息失败延迟隔离机制
  2. 本地变量ThreadLocal 保存上一次发送的消息队列下标,消息发送使用轮询机制获取下一个发送消息队列。同时topic发送有异常延迟,确保选中的消息队列所在broker正常
  3. 当前消息队列是否可用

1.4 发送消息

该方法是消息发送核心方法,已经明确往哪个Broker发送消息了,

  1. 发送消息涉及到消息发送前和发送后做的事情,已经发送完成后的回调消息轨迹就是在这里处理。
  2. 最终构建请求消息体调用remotingClient.invoke()并完成netty的网络请求。

Rocketmq提供三种方式可以发送普通消息:同步、异步、和单向发送。

(1)同步:发送方发送消息后,收到服务端响应后才发送下一条消息

(2)异步:发送一条消息后,不等服务端返回就可以继续发送消息或者后续任务处理。发送方通过回调接口接收服务端响应,并处理响应结果。

(3)OneWay:发送方发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不需要应答。

发送方式对比:发送吞吐量,单向>异步>同步。但单向发送可靠性差存在丢失消息可能,选型根据实际需求确定。

1.5 源码分析

我们从同步消息入手,可以直接看下源码里面单侧,通过单侧DEBUG去了解整体的流程, 这边我们以同步消息为例。

DefaultMQProducerTest#testSendMessageSync_Success

image.png

第一步消息的校验,校验的源码比较好理解就是一些主题的判空

image.png

DefaultMQProducerImpl#sendDefaultImpl 这边我简要说一下核心方法,代码的细节还是需要去走查源码 第2步查找路由信息。

//step2 查找路由, 找元数据 TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());

private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {

    //优先从缓存中获得主题的路由信息
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);

    //路由信息为空,则从NameServer获取路由
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        //从NameServer获取路由表
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);

    }

    if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
        return topicPublishInfo;
    } else {
        //如果未找到当前主题的路由信息,则用默认主题继续查找
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
        return topicPublishInfo;
    }
}

第3步 根据负载均衡算法选择 MessageQueue

// 根据topic获取对应的路由数据,然后从路由数据中找到合适的队列。 MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
        //如果开启了延迟隔离 ,默认是没有的
        if (this.sendLatencyFaultEnable) {
            try {
                //  round-robin: 这个index,每次优择一个队列,tpInfo中的ThreadLocalIndex都会加1
                //  注意,通过线程局部变量,进行了无锁编程, 避免了锁的操作
                int index = tpInfo.getSendWhichQueue().getAndIncrement();
                for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                    //与队列的长度取模,根据最后的pos取一个队列
                    int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                    if (pos < 0)
                        pos = 0;
                    MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                    //判断取到的队列的broker是否隔离中,
                     if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                        // 如果不是隔离中就返回即可
                        if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                            return mq;
                    }
                }

                // 如果所有的队列都是隔离中的话
                // 那么就从 faultItemTable 隔离列表取出一个Broker即可作为次优的 broker
                final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
                // 获取这个broker的可写队列数,如果该Broker没有可写的队列,则返回-1
                int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
                if (writeQueueNums > 0) {
                    // 再次优择一次队列
                    final MessageQueue mq = tpInfo.selectOneMessageQueue();
                    if (notBestBroker != null) {
//                        // 次优的 broker
                        mq.setBrokerName(notBestBroker);
                          // 通过与队列的长度取模确定队列的位置
                        mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                    }
                    return mq;
                } else {
                    //没有可写的队列,直接从隔离列表移除  Broker
                    latencyFaultTolerance.remove(notBestBroker);
                }
            } catch (Exception e) {
                log.error("Error occurred when selecting message queue", e);
            }

            //如果故障列表中也没有可写的队列,则直接从tpInfo中获取一个
            return tpInfo.selectOneMessageQueue();
        }

        // 没有开启延迟隔离,
        // 直接从TopicPublishInfo通过取模的方式获取队列即可
        // 如果LastBrokerName不为空,则需要过滤掉brokerName=lastBrokerName的队列
        return tpInfo.selectOneMessageQueue(lastBrokerName);
    }

第4步消息的发送,消息的通信Producer发送到Broker, 同步消息返回响应。

private SendResult sendMessageSync(
    final String addr,
    final String brokerName,
    final Message msg,
    final long timeoutMillis,
    final RemotingCommand request
) throws RemotingException, MQBrokerException, InterruptedException {
    RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
    assert response != null;
    return this.processSendResponse(brokerName, msg, response);
}

2、Producer 负载均衡机制核心方法

//round-robin 负载均衡
public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
    //1 消息第一次发送,上一个失败的broker名字为null,直接round-round选择
    if (lastBrokerName == null) {
        return selectOneMessageQueue();
    } else {
        //2 消息发送失败重试(上一个失败的broker不为null)优先选择其他Broker上的队列
        int index = this.sendWhichQueue.getAndIncrement();
        for (int i = 0; i < this.messageQueueList.size(); i++) {
            int pos = Math.abs(index++) % this.messageQueueList.size();
            if (pos < 0)
                pos = 0;
            MessageQueue mq = this.messageQueueList.get(pos);
            //选择其他Broker上的队列,与上一次的故障broker隔离
            if (!mq.getBrokerName().equals(lastBrokerName)) {
                return mq;
            }
        }
        //3 没有其他的Broker可选,那么依然round-robin,可能会选择到之前失败的Broker上的队列
        return selectOneMessageQueue();
    }
}

//负载均衡,无锁编程,ThreadLocal空间换时间
public MessageQueue selectOneMessageQueue() {
    int index = this.sendWhichQueue.getAndIncrement();
    int pos = Math.abs(index) % this.messageQueueList.size();
    if (pos < 0)
        pos = 0;
    return this.messageQueueList.get(pos);
}
public class ThreadLocalIndex {
    private final ThreadLocal<Integer> threadLocalIndex = new ThreadLocal<Integer>();
    private final Random random = new Random();

    //获取下一个 index
    public int getAndIncrement() {
        Integer index = this.threadLocalIndex.get();

        //初始值
        if (null == index) {
            index = Math.abs(random.nextInt());
            if (index < 0)
                index = 0;
            this.threadLocalIndex.set(index);
        }

        index = Math.abs(index + 1);
        if (index < 0)
            index = 0;

        this.threadLocalIndex.set(index);
        return index;
    }

    @Override
    public String toString() {
        return "ThreadLocalIndex{" +
            "threadLocalIndex=" + threadLocalIndex.get() +
            '}';
    }
}

3、Producer 隔离机制

MQFaultStrategy 延迟隔离策略类 在RocketMq集群中,queue分布在各个不同的broker服务器中时,当尝试向其中一个queue发送消息时,如果出现耗时过长或者发送失败的情况,RocketMQ则会尝试重试发送。不妨细想一下,同样的消息第一次发送失败或耗时过长,可能是网络波动或者相关broker停止导致,如果短时间再次重试极有可能还是同样的情况。

RocketMQ为我们提供了延迟故障自动切换queue的功能,并且会根据故障次数和失败等级来预判故障时间并自动恢复,该功能是选配,默认关闭,可以通过如下配置开启。

image.png

4、Producer 重试机制

三种消息的类型介绍如下:

  1. 普通消息:消息是无序的,任意发送发送哪一个队列都可以。
  2. 普通有序消息:同一类消息(例如某个用户的消息)总是发送到同一个队列,在异常情况下,也可以发送到其他队列。
  3. 严格有序消息:消息必须被发送到同一个队列,即使在异常情况下,也不允许发送到其他队列。

5、面试题,Producer消息发送的高可靠机制?

隔离机制,重试机制,超时机制,负载均衡 四个方面

参考

rocketMQ是如何利用MQFaultStrategy规避延迟故障的?