RocketMQ消息发送原理与实践

RocketMQ消息发送方式

RocketMQ支持3种消息发送方式: 同步(sync)、异步(async)、单向(oneway)。
同步: 发送者向MQ执行发送消息API时,同步等待,直到消息服务器返回发送结果。
异步: 发送者向MQ执行发送消息API时,指定消息发送成功后的回调函数,然后调用消息发送API后,立即返回,消息发送者线程不阻塞,直到运行结束,消息发送成功或失败的回调任务在一个新的线程中执行。
单向: 消息发送者向MQ执行发送消息API时,直接返回,不等待消息服务器的结果,也不注册回调函数,简单地说,就是只管发,不在乎消息是否成功存储在消息服务器上。

RocketMQ消息

RocketMQ消息封装类是org.apache.rocketmq.common.message.Message。

private String topic;
private int flag;
private Map<String, String> properties;
private byte[] body;

public Message(String topic, byte[] body)
public Message(String topic, String tags, String keys, int flag, byte[] body, boolean waitStoreMsgOK)
public Message(String topic, String tags, byte[] body)
public Message(String topic, String tags, String keys, byte[] body)
public void setKeys(String keys)
public void putUserProperty(final String name, final String value)
public String getUserProperty(final String name)
public int getDelayTimeLevel()
public void setDelayTimeLevel(int level)

Message的基础属性主要包括消息所属主题topic、消息Flag(RocketMQ不做处理)、扩展属性、消息体。
RocketMQ定义的MessageFlag如下:

MessageSysFlag

public final static int COMPRESSED_FLAG = 0x1;
public final static int MULTI_TAGS_FLAG = 0x1 << 1;
public final static int TRANSACTION_NOT_TYPE = 0;
public final static int TRANSACTION_PREPARED_TYPE = 0x1 << 2;
public final static int TRANSACTION_COMMIT_TYPE = 0x2 << 2;
public final static int TRANSACTION_ROLLBACK_TYPE = 0x3 << 2;

public static int getTransactionValue(final int flag)
public static int resetTransactionValue(final int flag, final int type)
public static int clearCompressedFlag(final int flag)

Message全属性构造函数:

public Message(String topic, String tags, String keys, int flag, byte[] body, boolean waitStoreMsgOK) {
    this.topic = topic;
    this.flag = flag;
    this.body = body;

    if (tags != null && tags.length() > 0)
        this.setTags(tags);

    if (keys != null && keys.length() > 0)
        this.setKeys(keys);

    this.setWaitStoreMsgOK(waitStoreMsgOK);
}

public void setKeys(String keys) {
    this.putProperty(MessageConst.PROPERTY_KEYS, keys);
}

public void setWaitStoreMsgOK(boolean waitStoreMsgOK) {
    this.putProperty(MessageConst.PROPERTY_WAIT_STORE_MSG_OK, Boolean.toString(waitStoreMsgOK));
}

Message扩展属性主要包含下面几个。
tag: 消息TAG,用于消息过滤。
keys: Message索引键,多个用空格隔开,RocketMQ可以根据这些key快速检索到消息。
waitStoreMsgOK: 消息发送时是否等消息存储完成后再返回。
delayTimeLevel: 消息延迟级别,用于定时消息或消息重试。
这些扩展信息属性存储在Message的properties。

生产者启动流程

消息生产者的代码都在client模块中,相对于RocketMQ来说,它就是客户端,也是消息的提供者,我们在应用系统中初始化生产者的一个实例既可使用它来发消息。

DefaultMQProducer消息发送者

DefaultMQProducer是默认的消息生产者实现类,它实现MQAdmin的接口。
下面介绍DefaultMQProducer的主要方法。
1、void createTopic(String key,String newTopic,int queueNum,int topicSysFlag)创建主题。
key: 目前未实际作用,可以与newTopic相同。
newTopic: 主题名称。
queueNum: 队列数量。
topicSysFlag: 主题系统标签,默认为0。
2、public long searchOffset(MessageQueue mq, long timestamp)
根据时间戳从队列中查找其偏移量。
3、public long maxOffset(MessageQueue mq)
查找该消息队列中最大的物理偏移量。
4、public long minOffset(MessageQueue mq)
查找该消息队列中最小的物理偏移量。
5、public MessageExt viewMessage(String offsetMsgId)
根据消息偏移量查找消息。
6、public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end)
根据条件查询消息。
topic: 消息主题。
key: 消息索引字段。
maxNum: 本次最多取出消息条数。
begin: 开始时间。
end: 结束时间。
7、public MessageExt viewMessage(String topic,String msgId)
根据主题与消息ID查找消息。
8、public List<MessageQueue> fetchPublishMessageQueues(String topic)
查找主题下所有的消息队列。
9、public SendResult send(Message msg)
同步发送消息,具体发送到主题中的哪个消息对联由负载均衡算法决定。
10、public SendResult send(Message msg,long timeout)
同步发送消息,如果发送超过timeout则抛出超时异常。
11、public void send(Message msg,SendCallback sendCallback)
异步发送消息,sendCallback参数是消息发送成功后的回调方法。
12、public void send(Message msg, SendCallback sendCallback, long timeout)
异步发送消息,如果发送超过timeout指定的值,则抛出超时异常。
13、public void sendOneway(Message msg)
单向消息发送,就是不在乎发送结果,消息发送出去后该方法立即返回。
14、public SendResult send(Message msg, MessageQueue mq)
同步方式发送消息,发送到指定消息队列。
15、public void send(Message msg, MessageQueue mq, SendCallback sendCallback)
异步方式发送消息,发送到指定消息队列。
16、public void sendOneway(Message msg,MessageQueue mq)
单向方式发送消息,发送到指定的消息队列。
17、public SendResult send(Message msg, MessageQueueSelector selector, Object arg)
消息发送,指定消息选择算法,覆盖消息生产者默认的消息队列负载。
18、public SendResult send(Collection<Message> msgs, MessageQueue messageQueue,long timeout)
同步批量发送。

DefaultMQProducer核心属性

private String producerGroup;
private String createTopicKey = MixAll.DEFAULT_TOPIC;
private volatile int defaultTopicQueueNums = 4;
private int sendMsgTimeout = 3000;
private int compressMsgBodyOverHowmuch = 1024 * 4;
private int retryTimesWhenSendFailed = 2;
private int retryTimesWhenSendAsyncFailed = 2;
private boolean retryAnotherBrokerWhenNotStoreOK = false;
private int maxMessageSize = 1024 * 1024 * 4; // 4M

producerGroup: 生产者所属组,消息服务器在回查事务状态时会随机选择该组中任何一个生产者发起事务回查请求。
createTopicKey: 默认topicKey。
defaultTopicQueueNums: 默认主题在每一个Broker队列数量。
sendMsgTimeout: 发送消息默认超时时间,默认3s。
compressMsgBodyOverHowmuch: 消息体超过该值则启用压缩,默认4K。
retryTimesWhenSendFailed: 同步方式发送消息重试次数,默认为2,总共执行3次。
retryTimesWhenSendAsyncFailed: 异步方式发送消息重试次数,默认为2。
retryAnotherBrokerWhenNotStoreOK: 消息重试时选择另外一个Broker时,是否不等储存结果就返回,默认为false。
maxMessageSize: 允许发送的最大消息长度,默认为4M,该值最大值为2^32-1。

消息生产者启动流程

我们可以从DefaultMQProducerImpl的start方法来追踪,具体细节如下。

Step1: 检查producerGroup是否符合要求;并改变生产者的instanceName为进程ID。

DefaultMQProducerImpl#start

this.checkConfig();
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
    this.defaultMQProducer.changeInstanceNameToPID();
}

Step2: 创建MQClientInstance实例。整个JVM实例中只存在一个MQClientManager实例,维护一个MQClientInstance缓存表private ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable =new ConcurrentHashMap<String, MQClientInstance>(),也就是同一个clientId只会创建一个MQClientInstance。

DefaultMQProducerImpl#start

this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);

MQClientManager#getAndCreateMQClientInstance

public MQClientInstance getAndCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
      String clientId = clientConfig.buildMQClientId();
      MQClientInstance instance = this.factoryTable.get(clientId);
      if (null == instance) {
          instance =
              new MQClientInstance(clientConfig.cloneClientConfig(),
                  this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
          MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
          if (prev != null) {
              instance = prev;
              log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
          } else {
              log.info("Created new MQClientInstance for clientId:[{}]", clientId);
          }
      }

      return instance;
}

org.apache.rocketmq.client.ClientConfig#buildMQClientId

// 创建clientId的方法
public String buildMQClientId() {
    StringBuilder sb = new StringBuilder();
    sb.append(this.getClientIP());

    sb.append("@");
    sb.append(this.getInstanceName());
    if (!UtilAll.isBlank(this.unitName)) {
        sb.append("@");
        sb.append(this.unitName);
    }

    return sb.toString();
}

clientId为客户端IP+instance+(unitName可选),如果在同一台服务器部署两个应用程序,应用程序岂不是clientId相同,会造成混乱?

为了避免这个问题,如果instance为默认值DEFAULT的话,RocketMQ会自动将instance设置为进程ID,这样避免了不同进程的相互影响,但同一个JVM中的不同消费者和不同生产者在启动时获取到的MQClientInstance实例都是同一个。根据后面的介绍,MQClientInstance封装了RocketMQ网络处理API,是消息生产者(Producer)、消息消费者(Consumer)与NameServer、Broker打交道的网络通道。

Step3: 向MQClientInstance注册,将当前生产者加入到MQClientInstance管理中,方便后续调用网络请求、进行心跳检测等。

boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
if (!registerOK) {
    this.serviceState = ServiceState.CREATE_JUST;
    throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
       + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),null);
}

Step4: 启动MQClientInstance,如果MQClientInstance已经启动,则本次启动不会真正执行。

消息发送基本流程

消息发送流程主要的步骤: 验证消息、查找路由、消息发送(包含异常处理机制)。
同步消息发送入口,代码如下

// DefaultMQProducer#send
public SendResult send(
    Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    return this.defaultMQProducerImpl.send(msg);
}
// DefaultMQProducerImpl#send
public SendResult send(
    Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}

默认消息发送以同步方式发送,默认超时时间为3s。

消息长度验证

消息发送之前,首先确保生产者处于运行状态,然后验证消息是否符合相应的规范,具体的规范要求是主题名称、消息体不能为空、消息长度不能等于0且默认不能超过允许发送消息的最大长度4M(maxMessageSize = 1024 * 1024 * 4)。

查找主题路由信息

消息发送之前,首先需要获取主题的路由信息,只有获取了这些信息我们才能知道消息发送到具体的Broker节点。
DefaultMQProducerImpl#tryToFindTopicPublishInfo

private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        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;
    }
}

tryToFindTopicPublishInfo是查找主题的路由信息的方法。如果生产者中缓存了topic的路由信息,如果该路由信息中包含了消息队列,则直接返回该路由信息,如果没有缓存或没有消息队列,则向NameServer查询该Topic的路由信息。如果最终未找到路由信息,则抛出异常:无法找到主题相关路由信息异常。
先看一下TopicPublishInfo

public class TopicPublishInfo {
    private boolean orderTopic = false;
    private boolean haveTopicRouterInfo = false;
    private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>();
    private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();
    private TopicRouteData topicRouteData;
}
public class TopicRouteData extends RemotingSerializable {
    private String orderTopicConf;
    private List<QueueData> queueDatas;
    private List<BrokerData> brokerDatas;
    private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}

TopicPublishInfo的属性:
orderTopic: 是否是顺序消息
List<MessageQueue> messageQueueList: 该主题队列的消息队列
sendWhichQueue : 每选择一次消息队列,该值会自增1,如果Integer.MAX_VALUE,则重置为0,用于选择消息队列。
List<QueueData> queueDatas: topic队列元数据。
List<BrokerData> brokerDatas: topic分布的broker元数据。
HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable: broker上过滤服务器地址列表。

第一次发送消息时,本地没有缓存topic的路由信息,查询NameServer尝试获取,如果路由信息未找到,再次尝试用默认主题DefaultMQProducerImpl#createTopicKey去查询,如果BrokerConfig#autoCreateTopicEnable为true时,NameServer将返回路由信息,如果autoCreateTopicEnable为false将抛出无法找到路由异常。代码MQClientInstance#updateTopicRouteInfoFromNameServer这个方法的功能是消息生产者更新和维护路由缓存,具体代码如下。
Step1: 如果isDefault为true,则使用默认主题去查询,如果查询到路由信息,则替换路由信息中读写队列个数,为消息生产者默认的队列个数(defaultTopicQueueNums);如果isDefault为false,则使用参数topic去查询;如果未查询到路由信息,则返回false,表示路由信息未变化。

MQClientInstance#updateTopicRouteInfoFromNameServer

TopicRouteData topicRouteData;
if (isDefault && defaultMQProducer != null) {
    topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
        1000 * 3);
    if (topicRouteData != null) {
        for (QueueData data : topicRouteData.getQueueDatas()) {
            int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
            data.setReadQueueNums(queueNums);
            data.setWriteQueueNums(queueNums);
        }
    }
} else {
    topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
}

Step2: 如果路由信息未找到,与本地缓存中的路由信息进行对比,判断路由信息是否发生了改变,如果未发生变化,则直接返回false。

MQClientInstance#updateTopicRouteInfoFromNameServer

if (topicRouteData != null) {
    TopicRouteData old = this.topicRouteTable.get(topic);
    boolean changed = topicRouteDataIsChange(old, topicRouteData);
   if (!changed) {
       changed = this.isNeedUpdateTopicRouteInfo(topic);
    } else {
        log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
   }

Step3: 更新MQClientInstance Broker地址缓存表。

MQClientInstance#updateTopicRouteInfoFromNameServer

    // Update Pub info
    {
        TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
        publishInfo.setHaveTopicRouterInfo(true);
        Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
        while (it.hasNext()) {
            Entry<String, MQProducerInner> entry = it.next();
            MQProducerInner impl = entry.getValue();
            if (impl != null) {
                impl.updateTopicPublishInfo(topic, publishInfo);
            }
        }
    }

Step4: 根据topicRouteData中的List<QueueData>转换成topicPublishInfo的List<MessageQueue>列表。其具体实现在topicRouteData2TopicPublishInfo,然后会更新该MQClientInstance所管辖的所有消息发送关于topic的路由信息。

MQClientInstance#topicRouteData2TopicPublishInfo

    List<QueueData> qds = route.getQueueDatas();
    Collections.sort(qds);
    for (QueueData qd : qds) {
        if (PermName.isWriteable(qd.getPerm())) {
            BrokerData brokerData = null;
            for (BrokerData bd : route.getBrokerDatas()) {
                if (bd.getBrokerName().equals(qd.getBrokerName())) {
                    brokerData = bd;
                    break;
                }
            }

            if (null == brokerData) {
                continue;
            }

            if (!brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) {
                continue;
            }

            for (int i = 0; i < qd.getWriteQueueNums(); i++) {
                MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
                info.getMessageQueueList().add(mq);
            }
        }
    }

循环遍历路由信息的QueueData信息,如果队列没有写权限,则继续遍历下一个QueueData;根据brokerName找到brokerData信息,找不到或没有找到Master节点,则遍历下一个QueueData;根据写队列个数,根据topic+序号创建MessageQueue,填充topicPublishInfo的List<MessageQueue>。完成消息发送的路由查找。

选择消息队列

根据路由信息选择消息队列,返回的消息队列按照broker、序号排序。举例说明,如果topicA在broker-a,broker-b上分别创建了4个队列,那么返回的消息队列:[{"brokerName":"broker-a","queueId":0},{"brokerName":"broker-a","queueId":1},{"brokerName":"broker-a","queueId":2},{"brokerName":"broker-a","queueId":3},{"brokerName":"broker-b","queueId":0},{"brokerName":"broker-a","queueId":1},{"brokerName":"broker-a","queueId":2},{"brokerName":"broker-a","queueId":3}],那么RocketMQ如何选择消息队列呢?

首先消息发送端采用重试机制,由retryTimesWhenSendFailed指定同步方式重试次数,异步重试机制在收到消息发送结构后执行回调之前进行重试。由retryTimesWhenSendAsyncFailed指定,接下来就是循环执行,选择消息队列、发送消息,发送成功则返回,收到异常则重试。选择消息队列有两种方式。
1、sendLatencyFaultEnable = false,默认不启用Broker故障延迟机制。
2、sendLatencyFaultEnable = true,启用Broker故障延迟机制。

1.默认机制
sendLatencyFaultEnable = false,调用TopicPublishInfo#selectOneMessageQueue

TopicPublishInfo#selectOneMessageQueue

    public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
        if (lastBrokerName == null) {
            return selectOneMessageQueue();
        } else {
            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);
                if (!mq.getBrokerName().equals(lastBrokerName)) {
                    return mq;
                }
            }
            return selectOneMessageQueue();
        }
    }

首先在一次消息发送过程中,可能会多次执行算则消息队列这个方法,lastBrokerName就是上一次选择的执行发送消息失败的Broker。第一次执行消息队列选择时,lastBrokerName为null,此时直接用sendWhichQueue自增再获取值,与当前路由表中消息队列个数取模,返回该位置的MessageQueue(selectOneMessageQueue()方法),如果消息发送再失败的话,下次进行消息队列选择时规避上次MessageQueue所在的Broker,否则还是很有可能再次失败。

该算法在一次消息发送过程中能成功规避故障的Broker,但如果Broker宕机,由于路由算法中的消息队列是按Broker排序的,如果上一次根据路由算法选择的是宕机的Broker的第一个队列,name随后的下次选择是宕机Broker的第二个队列,消息发送很有可能会失败,再次引发重试,带来不必要的性能损耗,那么有什么方法在一次消息发送失败后,暂时将该Broker排除在消息队列选择范围外呢?或许有朋友会问,Broker不可用后,路由信息中为什么还会有包含该Broker的路由信息呢?其实这不难解释:首先,NameServer检测Broker是否可用是有延迟的,最短一次心跳检测间隔(10s);其次,NameServer不会检测到Broker当即后马上推送消息给消息生产者,而是消息生产者每个30s更新一次路由信息,所以消息生产者最快感知Broker最新的路由信息也需要30s。如果引入一种机制,在Broker宕机期间,如果一次消息发送失败后,可以将该Broker暂时排除在消息队列的选择范围中。

2.Broker故障延迟机制

MQFaultStrategy#selectOneMessageQueue

    public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
        if (this.sendLatencyFaultEnable) {
            try {
                int index = tpInfo.getSendWhichQueue().getAndIncrement();
                for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                    int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                    if (pos < 0)
                        pos = 0;
                    MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                    if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                        if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                            return mq;
                    }
                }

                final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
                int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
                if (writeQueueNums > 0) {
                    final MessageQueue mq = tpInfo.selectOneMessageQueue();
                    if (notBestBroker != null) {
                        mq.setBrokerName(notBestBroker);
                        mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                    }
                    return mq;
                } else {
                    latencyFaultTolerance.remove(notBestBroker);
                }
            } catch (Exception e) {
                log.error("Error occurred when selecting message queue", e);
            }

            return tpInfo.selectOneMessageQueue();
        }

        return tpInfo.selectOneMessageQueue(lastBrokerName);
    }

首先对上述代码进行解读。
1、根据对消息队列进行轮询获取一个消息队列。
2、验证该消息队列是否可用,latencyFaultTolerance.isAvailable(mq.getBrokerName())是关键。
3、 如果返回的MessageQueue可用,移除lantencyFaultTolerance关于该topic条目,表明该Broker故障已经恢复。
Broker故障延迟机制核心类如下:

public interface LatencyFaultTolerance<T> {
    void updateFaultItem(final T name, final long currentLatency, final long notAvailableDuration);
    boolean isAvailable(final T name);
    void remove(final T name);
    T pickOneAtLeast();
}
public class LatencyFaultToleranceImpl implements LatencyFaultTolerance<String> {
    private final ConcurrentHashMap<String, FaultItem> faultItemTable = new ConcurrentHashMap<String, FaultItem>(16);
    private final ThreadLocalIndex whichItemWorst = new ThreadLocalIndex();
}
class FaultItem implements Comparable<FaultItem> {
    private final String name;
    private volatile long currentLatency;
    private volatile long startTimestamp;
}
public class MQFaultStrategy {
    private final LatencyFaultTolerance<String> latencyFaultTolerance = new LatencyFaultToleranceImpl();
    private boolean sendLatencyFaultEnable = false;
    private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
    private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
}

LatencyFaultTolerance: 延迟机制接口规范。
1、void updateFaultItem(final T name, final long currentLatency, final long notAvailableDuration);
更新失败条目 。
name: brokerName
currentLatenmcy: 消息发送故障延迟时间。
notAvailableDuration: 不可用持续时长,在这个时间内,Broker将被规避。
2、boolean isAvailable(final T name);
判断Broker是否可用。
name: broker名称。
3、void remove(final T name)
移除Fault条目,意味着Broker重新参与路由计算。
4、T pickOneAtLeast()
尝试从规避的Broker中选择一个可用的Broker,如果没有找到,将返回null。
FaultItem: 失败条目(规避规则条目)。
final String name 条目唯一键,这里为brokerName。
private volatile long currentLatency 本次消息发送延迟。
private volatile long startTimeStamp 故障规避开始时间。
MQFaultStrategy: 消息失败策略,延迟实现的门面类。
long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
latencyMax,根据currentLatency本次消息发送延迟,从latencyMax尾部向前汇总爱到第一个比currentLatency小的索引index,如果没有找到,返回0。然后根据这个索引从notAvailableDuration数组中取出对应的时间,在这个时长内,Broker将设置为不可用。

DefaultMQProducerImpl#sendDefaultImpl

beginTimestampPrev = System.currentTimeMillis();
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout);
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);

上述代码如果发送过程中抛出了异常,调用DefaultMQProducerImpl#updateFaultItem,该方法则直接调用MQFaultStrategy#updateFaultItem方法,关注一下各个参数的含义。
第一个参数: broker名称。
第二个参数: 本次消息发送延迟时间 currentLatency。
第三个参数: isolation,是否隔离,该参数的含义如果为true,则使用默认时长30s来计算Broker故障规避时长,如果为false,则使用本次消息发送延迟时间来计算Broker故障规避时长。

MQFaultStrategy#updateFaultItem

    public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
        if (this.sendLatencyFaultEnable) {
            long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
            this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
        }
    }

    private long computeNotAvailableDuration(final long currentLatency) {
        for (int i = latencyMax.length - 1; i >= 0; i--) {
            if (currentLatency >= latencyMax[i])
                return this.notAvailableDuration[i];
        }

        return 0;
    }

如果isolation为true,则使用30s作为computeNotAvailableDuration方法的参数;如果isolation为false,则使用本次消息发送时延作为computeNotAvailableDuration方法的参数,那computeNotAvailableDuration的作用是计算因本次消息发送故障需要将Broker规避的时长,也就是接下来多久的时间内该Broker将不参与消息发送队列负载。具体算法: 从latencyMax数组尾部开始寻找,找到第一个比currentLatency小的下标,然后从notAvailableDuration数组中获取需要规避的时长,该方法最终调用LatencyFaultTolerance的updateFaultItem。

LatencyFaultToleranceImpl#updateFaultItem

public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
    FaultItem old = this.faultItemTable.get(name);
    if (null == old) {
        final FaultItem faultItem = new FaultItem(name);
        faultItem.setCurrentLatency(currentLatency);
        faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);

        old = this.faultItemTable.putIfAbsent(name, faultItem);
        if (old != null) {
            old.setCurrentLatency(currentLatency);
            old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
        }
    } else {
        old.setCurrentLatency(currentLatency);
        old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
    }
}

根据broker名称从缓存表中获取FaultItem,如果找到则更新FaultItem,否则创建FaultItem。这里有两个关键点。
1、currentLatency、startTimeStamp被volatile修饰。
2、startTimeStamp为当前系统时间加上需要规避的时长。startTimeStamp是判断broker当前是否可用的直接依据,请看FaultItem#isAvailable方法。

public boolean isAvailable() {
    return (System.currentTimeMillis() - startTimestamp) >= 0;
}

消息发送

消息发送API核心入口: DefaultMQProducerImpl#sendKernelImpl

private SendResult sendKernelImpl(final Message msg,
    final MessageQueue mq,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback,
    final TopicPublishInfo topicPublishInfo,
    final long timeout)

消息发送参数详解。
1、Message msg: 待发送消息。
2、MessageQueue mq: 消息将发送到该消息队列上。
3、CommunicationMode communicationMode: 消息发送模式,SYNC、ASYNC、ONEWAY。
4、SendCallback sendCallback: 异步消息回调函数。
5、TopicPublishInfo topicPublishInfo: 主题路由信息。
6、long timeout: 消息发送超时时间。

Step1: 根据MessageQueue获取Broker的网络地址。如果MQClientInstance的brokerAddrTable未缓存该Broker的信息,则从NameServer主动更新一下topic的路由信息。如果路由更新后还是找不到Broker信息,则抛出MQClientException,提示Broker不存在。

DefaultMQProducerImpl#sendKernelImpl

String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
if (null == brokerAddr) {
    tryToFindTopicPublishInfo(mq.getTopic());
    brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
}

Step2: 为消息分配全局唯一ID,如果消息体默认超过4K(compressMsgBodyOverHowmuch),会对消息体采用zip压缩,并设置消息的系统标记为MessageSysFlag.COMPRESSED_FLAG。如果是事务Prrepared消息,则设置消息的系统标记为MessageSysFlag.TRANSACTION_PREPARED_TYPE。

DefaultMQProducerImpl#sendKernelImpl

//for MessageBatch,ID has been set in the generating process
if (!(msg instanceof MessageBatch)) {
    MessageClientIDSetter.setUniqID(msg);
}

int sysFlag = 0;
if (this.tryToCompressMessage(msg)) {
    sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
}

final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
    sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
}

Step3: 如果注册了消息发送钩子函数,则执行消息发送之前的增强逻辑。通过DefaultMQProducerImpl#registerSendMessageHook注册钩子处理类,并且可以注册多个。

DefaultMQProducerImpl#sendKernelImpl

if (this.hasSendMessageHook()) {
    context = new SendMessageContext();
    context.setProducer(this);
    context.setProducerGroup(this.defaultMQProducer.getProducerGroup());
    context.setCommunicationMode(communicationMode);
    context.setBornHost(this.defaultMQProducer.getClientIP());
    context.setBrokerAddr(brokerAddr);
    context.setMessage(msg);
    context.setMq(mq);
    String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
    if (isTrans != null && isTrans.equals("true")) {
        context.setMsgType(MessageType.Trans_Msg_Half);
    }

    if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) {
        context.setMsgType(MessageType.Delay_Msg);
    }
    this.executeSendMessageHookBefore(context);
}

SendMessageHook

public interface SendMessageHook {
    String hookName();
    void sendMessageBefore(final SendMessageContext context);
    void sendMessageAfter(final SendMessageContext context);
}

Step4: 构建消息发送请求包。主要包含如下重要信息: 生产者组、主题名称、默认创建主题Key、该主题在单个Broker默认队列数、队列ID(队列序号)、消息系统标题标记(MessageSysFlag)、消息发送时间、消息标记(RocketMQ对消息中的flag不做任何处理,供应用程序使用)、消息扩展属性、消息重试次数、是否是批量消息等。

DefaultMQProducerImpl#sendKernelImpl

SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
requestHeader.setTopic(msg.getTopic());
requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
requestHeader.setQueueId(mq.getQueueId());
requestHeader.setSysFlag(sysFlag);
requestHeader.setBornTimestamp(System.currentTimeMillis());
requestHeader.setFlag(msg.getFlag());
requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
requestHeader.setReconsumeTimes(0);
requestHeader.setUnitMode(this.isUnitMode());
requestHeader.setBatch(msg instanceof MessageBatch);
if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
    String reconsumeTimes = MessageAccessor.getReconsumeTime(msg);
    if (reconsumeTimes != null) {
        requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes));
        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME);
    }

    String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg);
    if (maxReconsumeTimes != null) {
        requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes));
        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES);
    }
}

Step5: 根据消息发送方式,同步、异步、单向方式进行网络传输。

MQClientAPIImpl#sendMessage

    public SendResult sendMessage(
        final String addr,
        final String brokerName,
        final Message msg,
        final SendMessageRequestHeader requestHeader,
        final long timeoutMillis,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final TopicPublishInfo topicPublishInfo,
        final MQClientInstance instance,
        final int retryTimesWhenSendFailed,
        final SendMessageContext context,
        final DefaultMQProducerImpl producer
    ) throws RemotingException, MQBrokerException, InterruptedException {
        RemotingCommand request = null;
        if (sendSmartMsg || msg instanceof MessageBatch) {
            SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader);
            request = RemotingCommand.createRequestCommand(msg instanceof MessageBatch ? RequestCode.SEND_BATCH_MESSAGE : RequestCode.SEND_MESSAGE_V2, requestHeaderV2);
        } else {
            request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
        }

        request.setBody(msg.getBody());

        switch (communicationMode) {
            case ONEWAY:
                this.remotingClient.invokeOneway(addr, request, timeoutMillis);
                return null;
            case ASYNC:
                final AtomicInteger times = new AtomicInteger();
                this.sendMessageAsync(addr, brokerName, msg, timeoutMillis, request, sendCallback, topicPublishInfo, instance,
                    retryTimesWhenSendFailed, times, context, producer);
                return null;
            case SYNC:
                return this.sendMessageSync(addr, brokerName, msg, timeoutMillis, request);
            default:
                assert false;
                break;
        }

        return null;
    }

Step6: 如果注册了消息发送钩子函数,执行after逻辑。注意,就算消息发送过程中发生RemotingExceptuion、MQBrokerException、InterruptedException时该方法也会执行。

DefaultMQProducerImpl#sendKernelImpl

if (this.hasSendMessageHook()) {
    context.setSendResult(sendResult);
    this.executeSendMessageHookAfter(context);
}

1.同步发送

MQ客户端发送消息的入口是MQClientAPIImpl#sendMessage。请求命令是RequestCode.SEND_MESSAGE,我们可以找到该命令的处理类:org.apache.rocketmq.broker.processor.SendMessageProcessor。入口方法在org.apache.rocketmq.broker.processor.SendMessageProcessor#sendMessage。
Step1: 检查消息发送是否合理,这里完成了以下几件事情。
1、检查该Broker是否有写权限。
2、检查该Topic是否可以进行消息发送。主要针对默认主题,默认主题不能发送消息,仅仅供路由查找。
3、在NameServer端存储主题的配置信息,默认路径: ${ROCKETMQ_HOME}/store/config/topic.json。下面是主题存储信息。
order: 是否是顺序消息;
perm: 权限码;
readQueueNums: 读队列数量;
writerQueueNums: 写队列数量;
topicName: 主题名称;
topicSysFlag: topic Flag;
topicFilterType: 主题过滤方式。
4、检查队列,如果队列不合法,返回错误码。

AbstractSendMessageProcessor#msgCheck

    protected RemotingCommand msgCheck(final ChannelHandlerContext ctx,
        final SendMessageRequestHeader requestHeader, final RemotingCommand response) {
        if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission())
            && this.brokerController.getTopicConfigManager().isOrderTopic(requestHeader.getTopic())) {
            response.setCode(ResponseCode.NO_PERMISSION);
            response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
                + "] sending message is forbidden");
            return response;
        }
        if (!this.brokerController.getTopicConfigManager().isTopicCanSendMessage(requestHeader.getTopic())) {
            String errorMsg = "the topic[" + requestHeader.getTopic() + "] is conflict with system reserved words.";
            log.warn(errorMsg);
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark(errorMsg);
            return response;
        }

        TopicConfig topicConfig =
            this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());
        if (null == topicConfig) {
            int topicSysFlag = 0;
            if (requestHeader.isUnitMode()) {
                if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
                } else {
                    topicSysFlag = TopicSysFlag.buildSysFlag(true, false);
                }
            }

            log.warn("the topic {} not exist, producer: {}", requestHeader.getTopic(), ctx.channel().remoteAddress());
            topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageMethod(
                requestHeader.getTopic(),
                requestHeader.getDefaultTopic(),
                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
                requestHeader.getDefaultTopicQueueNums(), topicSysFlag);

            if (null == topicConfig) {
                if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    topicConfig =
                        this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
                            requestHeader.getTopic(), 1, PermName.PERM_WRITE | PermName.PERM_READ,
                            topicSysFlag);
                }
            }

            if (null == topicConfig) {
                response.setCode(ResponseCode.TOPIC_NOT_EXIST);
                response.setRemark("topic[" + requestHeader.getTopic() + "] not exist, apply first please!"
                    + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
                return response;
            }
        }

        int queueIdInt = requestHeader.getQueueId();
        int idValid = Math.max(topicConfig.getWriteQueueNums(), topicConfig.getReadQueueNums());
        if (queueIdInt >= idValid) {
            String errorInfo = String.format("request queueId[%d] is illegal, %s Producer: %s",
                queueIdInt,
                topicConfig.toString(),
                RemotingHelper.parseChannelRemoteAddr(ctx.channel()));

            log.warn(errorInfo);
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark(errorInfo);

            return response;
        }
        return response;
    }

Step2: 如果消息重试次数超过允许的最大重试次数,消息将进入到DLQ延迟队列。延迟队列主题: %DLQ%+消费组名。

Step3: 调用DefaultMessageStore#putMessage进行消息存储。

2.异步发送

消息异步发送是指消息生产者调用发送的API后,无需阻塞等待消息服务器返回本次消息发送结果,只需要提供一个回调函数,供消息发送客户端在收到响应结果回调。异步方式相比同步方式,消息发送端的发送性能会显著提高,但为了保护消息服务器的负载压力,RocketMQ对消息发送的异步消息进行了并发控制,通过参数clientAsyncSemaphoreValue来控制,默认为65535。异步消息发送虽然也可以通过DefaultMQProducer#retryTimesWhenSendAsyncFailed属性来控制消息重试次数,但是重试的调用入口是在收到服务器响应包时进行的,如果出现网络异常、网络超时等将不会重试。

3.单向发送

单向发送是值消息生产者调用消息发送的API后,无需等待消息服务器返回本次消息发送结果,并且无需提供回调函数,表示消息发送鸭羹就不关心本次消息是否成功,其实现原理与异步消息发送相同,只是消息发送客户端在收到响应结果后什么都不做而已,并且没有重试机制。

发布了102 篇原创文章 · 获赞 10 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/u012921921/article/details/104331412