【RocketMQ 生产者和消费者】- broker 处理生产者发送的消息


本文章基于 RocketMQ 4.9.3

1. 前言

上面两篇文章讲述了生产者发送消息的整个流程,包括消息重试,消息重传,这篇文章就来看下 broker 是如何处理生产者的消息的。


2. SendMessageProcessor 处理 SEND_MESSAGE 请求

broker 启动的时候通过 registerProcessor 方法注册了对应 code 的请求处理器,SEND_MESSAGE 对应的处理器是 SendMessageProcessor。
在这里插入图片描述
且 SendMessageProcessor 中处理请求的方法是 processRequest,这篇文章想来看下处理的具体逻辑,下一篇文章再来看 broker 是如何处理通过 Netty 接收到的数据的。


3. processRequest 处理客户端请求消息

@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx,
                                      RemotingCommand request) throws RemotingCommandException {
    RemotingCommand response = null;
    try {
        // 这里会去阻塞获取请求的结果
        response = asyncProcessRequest(ctx, request).get();
    } catch (InterruptedException | ExecutionException e) {
        log.error("process SendMessage error, request : " + request.toString(), e);
    }
    // 返回结果
    return response;
}

可以看到这里会通过 asyncProcessRequest 方法去处理请求,然后返回一个 CompletableFuture 对象,再通过 get 方法阻塞获取处理结果。


4. asyncProcessRequest 异步处理请求

/**
 * 处理异步请求
 * @param ctx
 * @param request
 * @return
 * @throws RemotingCommandException
 */
public CompletableFuture<RemotingCommand> asyncProcessRequest(ChannelHandlerContext ctx,
                                                              RemotingCommand request) throws RemotingCommandException {
    final SendMessageContext mqtraceContext;
    // 根据不同的请求码来走不同的处理逻辑
    switch (request.getCode()) {
        // 消费者消费失败会发送消息回退请求来重试或者发送到死信队列
        case RequestCode.CONSUMER_SEND_MSG_BACK:
            return this.asyncConsumerSendMsgBack(ctx, request);
        default:
            // 这里是生产者发送消息会走的逻辑,在这里统一处理
            // 首先就是解析出请求头
            SendMessageRequestHeader requestHeader = parseRequestHeader(request);
            if (requestHeader == null) {
                // 如果请求头为空,那么返回结果就设置为 NULL
                return CompletableFuture.completedFuture(null);
            }
            // 发送消息的轨迹上下文
            mqtraceContext = buildMsgContext(ctx, requestHeader);
            // 执行发送前的钩子方法,也就是调用 SendMessageHook#sendMessageBefore
            this.executeSendMessageHookBefore(ctx, request, mqtraceContext);
            if (requestHeader.isBatch()) {
                // 这里是处理批量发送的消息的逻辑
                return this.asyncSendBatchMessage(ctx, request, mqtraceContext, requestHeader);
            } else {
                // 这里是处理单条消息的逻辑
                return this.asyncSendMessage(ctx, request, mqtraceContext, requestHeader);
            }
    }
}

asyncProcessRequest 方法在处理普通消息发送的时候走的是 default 的逻辑,也就是解析出请求头之后,通过 asyncSendMessage 处理单条消息的发送,asyncSendBatchMessage 是批量消息发送的处理逻辑,我们这里先看处理单条的就行了,也是用的比较多的。


5. asyncSendMessage 处理单条消息的发送

/**
 * 处理单条消息
 * @param ctx
 * @param request
 * @param mqtraceContext
 * @param requestHeader
 * @return
 */
private CompletableFuture<RemotingCommand> asyncSendMessage(ChannelHandlerContext ctx, RemotingCommand request,
                                                            SendMessageContext mqtraceContext,
                                                            SendMessageRequestHeader requestHeader) {
    /**
     * 1.创建响应结果,RocketMQ 的请求和响应都是 RemotingCommand 类型
     */
    final RemotingCommand response = preSend(ctx, request, requestHeader);

    // 获取下响应头
    final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader)response.readCustomHeader();

    if (response.getCode() != -1) {
        // 如果不等于 -1, 说明在 preSend 的过程中出错了, 直接返回结果
        return CompletableFuture.completedFuture(response);
    }

    // 请求体
    final byte[] body = request.getBody();

    // 请求的队列 ID
    int queueIdInt = requestHeader.getQueueId();
    // 获取 topic 配置
    TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());

    if (queueIdInt < 0) {
        // 如果队列 id 小于 0, 那么随机选择一个队列 ID
        queueIdInt = randomQueueId(topicConfig.getWriteQueueNums());
    }

    // 创建消息发送对象, MessageExtBrokerInner 就是 asyncPutMessage 添加消息要传入的参数
    MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
    // 设置 topic
    msgInner.setTopic(requestHeader.getTopic());
    // 设置队列 ID
    msgInner.setQueueId(queueIdInt);

    /**
     * 2.处理重试和死信队列的信息, 消费者如果连续 3 次消费消息失败, 就会把这条消息发送到死信队列里面让用户自己去处理到底要不要重新发到
     *   原始队列去重新消费
     */
    if (!handleRetryAndDLQ(requestHeader, response, request, msgInner, topicConfig)) {
        return CompletableFuture.completedFuture(response);
    }

    // 设置消息体
    msgInner.setBody(body);
    // 设置消息 flag
    msgInner.setFlag(requestHeader.getFlag());
    // 设置消息属性
    Map<String, String> origProps = MessageDecoder.string2messageProperties(requestHeader.getProperties());
    MessageAccessor.setProperties(msgInner, origProps);
    // 设置消息的生成时间
    msgInner.setBornTimestamp(requestHeader.getBornTimestamp());
    // 设置消息的生产者地址
    msgInner.setBornHost(ctx.channel().remoteAddress());
    // 设置当前 broker 的地址作为消息的存储地址
    msgInner.setStoreHost(this.getStoreHost());
    // 设置消息重试次数
    msgInner.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes());
    // 设置 broker 集群的名称到消息的 CLUSTER 属性中
    String clusterName = this.brokerController.getBrokerConfig().getBrokerClusterName();
    MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_CLUSTER, clusterName);
    // 设置消息的 WAIT 属性, 这个属性的意思是消息发送时是否等消息存储完成后再返回
    if (origProps.containsKey(MessageConst.PROPERTY_WAIT_STORE_MSG_OK)) {
        // There is no need to store "WAIT=true", remove it from propertiesString to save 9 bytes for each message.
        // It works for most case. In some cases msgInner.setPropertiesString invoked later and replace it.
        // 这里的意思是在下面消息存储的 asyncPutMessage 方法中会通过 isWaitStoreMsgOK 去判断是否等待存储完成之后再返回, 这个方法
        // 会从 properties 属性里面获取 WAIT 属性来判断, 这个 properties 是在 上面 MessageAccessor.setProperties(msgInner, origProps)
        // 设置进去的, 所以如果 origProps 里面已经存在 WAIT 属性了, 那么 msgInner 的 propertiesString 里面就没必要存储了, 因为
        // 判断这个 WAIT 主要是根据 origProps 里面的值来判断的, 跟 propertiesString 属性没关系
        String waitStoreMsgOKValue = origProps.remove(MessageConst.PROPERTY_WAIT_STORE_MSG_OK);
        // 重新设置消息属性, 这样能节省 9 字节空间, 为什么是 9 字节呢?就是 "WAIT=TRUE"
        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
        // Reput to properties, since msgInner.isWaitStoreMsgOK() will be invoked later
        // 然后重新添加回 origProps 这个原始的属性里面, 因为后面 msgInner.isWaitStoreMsgOK() 判断的时候会用到
        origProps.put(MessageConst.PROPERTY_WAIT_STORE_MSG_OK, waitStoreMsgOKValue);
    } else {
        // 这里就是没有设置 WAIT 属性
        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
    }

    // 处理事务消息的逻辑
    CompletableFuture<PutMessageResult> putMessageResult = null;
    String transFlag = origProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
    if (transFlag != null && Boolean.parseBoolean(transFlag)) {
        // 如果是事务消息, 判断当前 broker 会不会存储事务消息
        if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
            // 如果 broker 不存储事务消息, 也返回错误码 NO_PERMISSION
            response.setCode(ResponseCode.NO_PERMISSION);
            // 设置提示
            response.setRemark(
                    "the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
                            + "] sending transaction message is forbidden");
            return CompletableFuture.completedFuture(response);
        }
        // 以异步的方式添加 Prepare 状态的消息
        putMessageResult = this.brokerController.getTransactionalMessageService().asyncPrepareMessage(msgInner);
    } else {
        // 不是事务消息, 那就是调用 asyncPutMessage 来存储, 当然了添加消息还是异步添加, 通过 CompletableFuture 来进行异步回调
        // 这样就能节省出资源来处理其他请求了
        putMessageResult = this.brokerController.getMessageStore().asyncPutMessage(msgInner);
    }
    // 处理消息存放的结果
    return handlePutMessageResultFuture(putMessageResult, response, request, msgInner, responseHeader, mqtraceContext, ctx, queueIdInt);
}

上面就是处理逻辑,首先通过 preSend 创建出一个响应对象 RemotingCommand,然后判断下如果响应 code 不等于 -1, 说明在 preSend 的过程中出错了, 直接返回结果。这里 -1 是 preSend 创建出来就设置上的,算是初始化值,但是 broker 可以通过配置项配置什么时候开始接收生产者的消息,也就是通过 startAcceptSendRequestTimeStamp 来配置,如果当前时间比 startAcceptSendRequestTimeStamp 要小,这种情况下会设置 code 为 ResponseCode.SYSTEM_ERROR,然后这里就会直接返回结果了。

接下来获取下 topic 配置信息以及队列信息,如果队列 id < 0,就随机选择一个队列来发送,消息需要设置到 broker 的 CommitLog 中,因此需要构建一个 msgInner 对象,并且设置一堆属性,其中要注意一下 WAIT,这个属性的意思是消息发送时是否等消息存储完成后再返回,需要注意一下这个 if 判断语句。

// 设置消息的 WAIT 属性, 这个属性的意思是消息发送时是否等消息存储完成后再返回
if (origProps.containsKey(MessageConst.PROPERTY_WAIT_STORE_MSG_OK)) {
    String waitStoreMsgOKValue = origProps.remove(MessageConst.PROPERTY_WAIT_STORE_MSG_OK);
    msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
    origProps.put(MessageConst.PROPERTY_WAIT_STORE_MSG_OK, waitStoreMsgOKValue);
} else {
    // 这里就是没有设置 WAIT 属性
    msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
}

可以看到,当 origProps 设置了 WAIT 属性的时候,会将 WAIT 属性从 origProps 里面给删掉,这是因为 origProps 在前面设置到了 msgInner 中。
在这里插入图片描述
这个属性用到的地方是 asyncPutMessage 的 isWaitStoreMsgOK,这个方法会从 origProps 里面通过 WAIT 属性来判断是否需要同步,跟 propertiesString 属性没有关系,而且这个 WAIT 属性也只是用在添加消息的时候判断,所以如果设置了 WAIT 属性,那么 msgInner 的 propertiesString 其实可以不需要这个 WAIT 属性,propertiesString 是 origProps 里面的属性拼接来的,需要存到 CommitLog 中,所以这里把 WAIT 先从 origProps 里面删掉,然后设置 propertiesString 之后再将 WAIT 添加回 origProps 中,这也最终存储到 CommitLog 中的属性就少了 WAIT=TRUE,这样算下来就刚好是 9 个字节的大小,也就是节省 9 个字节的 CommitLog 空间,至于最后的 asyncPutMessage 方法可以看下面这篇文章,有详细介绍。


6. handlePutMessageResultFuture 处理消息存放结果

/**
 * 处理消息存放的返回结果
 * @param putMessageResult      异步存储的返回结果
 * @param response              远程响应命令对象
 * @param request               远程请求命令对象
 * @param msgInner              存储的消息
 * @param responseHeader        响应头
 * @param sendMessageContext    消息发送上下文
 * @param ctx                   连接通道
 * @param queueIdInt            队列 ID
 * @return
 */
private CompletableFuture<RemotingCommand> handlePutMessageResultFuture(CompletableFuture<PutMessageResult> putMessageResult,
                                                                        RemotingCommand response,
                                                                        RemotingCommand request,
                                                                        MessageExt msgInner,
                                                                        SendMessageResponseHeader responseHeader,
                                                                        SendMessageContext sendMessageContext,
                                                                        ChannelHandlerContext ctx,
                                                                        int queueIdInt) {
    return putMessageResult.thenApply((r) ->
        // 处理异步存储的返回结果, 然后封装成响应结果
        handlePutMessageResult(r, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt)
    );
}

当获取到返回结果之后,通过 handlePutMessageResult 来处理异步返回结果,下面来看下这个方法的逻辑。

/**
 * 处理异步添加消息的返回结果
 * @param putMessageResult
 * @param response
 * @param request
 * @param msg
 * @param responseHeader
 * @param sendMessageContext
 * @param ctx
 * @param queueIdInt
 * @return
 */
private RemotingCommand handlePutMessageResult(PutMessageResult putMessageResult, RemotingCommand response,
                                               RemotingCommand request, MessageExt msg,
                                               SendMessageResponseHeader responseHeader, SendMessageContext sendMessageContext, ChannelHandlerContext ctx,
                                               int queueIdInt) {
    if (putMessageResult == null) {
        // 如果返回结果为空,
        response.setCode(ResponseCode.SYSTEM_ERROR);
        // 设置提示
        response.setRemark("store putMessage return null");
        return response;
    }
    boolean sendOK = false;

    // 不同的状态码做不同处理
    switch (putMessageResult.getPutMessageStatus()) {
        // Success
        case PUT_OK:
            sendOK = true;
            response.setCode(ResponseCode.SUCCESS);
            break;
        case FLUSH_DISK_TIMEOUT:
            response.setCode(ResponseCode.FLUSH_DISK_TIMEOUT);
            sendOK = true;
            break;
        case FLUSH_SLAVE_TIMEOUT:
            response.setCode(ResponseCode.FLUSH_SLAVE_TIMEOUT);
            sendOK = true;
            break;
        case SLAVE_NOT_AVAILABLE:
            response.setCode(ResponseCode.SLAVE_NOT_AVAILABLE);
            sendOK = true;
            break;

        // Failed
        case CREATE_MAPEDFILE_FAILED:
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark("create mapped file failed, server is busy or broken.");
            break;
        case MESSAGE_ILLEGAL:
        case PROPERTIES_SIZE_EXCEEDED:
            response.setCode(ResponseCode.MESSAGE_ILLEGAL);
            response.setRemark(
                "the message is illegal, maybe msg body or properties length not matched. msg body length limit 128k, msg properties length limit 32k.");
            break;
        case SERVICE_NOT_AVAILABLE:
            response.setCode(ResponseCode.SERVICE_NOT_AVAILABLE);
            response.setRemark(
                "service not available now. It may be caused by one of the following reasons: " +
                    "the broker's disk is full [" + diskUtil() + "], messages are put to the slave, message store has been shut down, etc.");
            break;
        case OS_PAGECACHE_BUSY:
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark("[PC_SYNCHRONIZED]broker busy, start flow control for a while");
            break;
        case LMQ_CONSUME_QUEUE_NUM_EXCEEDED:
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark("[LMQ_CONSUME_QUEUE_NUM_EXCEEDED]broker config enableLmq and enableMultiDispatch, lmq consumeQueue num exceed maxLmqConsumeQueueNum config num, default limit 2w.");
            break;
        case UNKNOWN_ERROR:
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark("UNKNOWN_ERROR");
            break;
        default:
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark("UNKNOWN_ERROR DEFAULT");
            break;
    }

    String owner = request.getExtFields().get(BrokerStatsManager.COMMERCIAL_OWNER);
    // 这里发送成功的意思是消息确实是存储到 broker 的内存中了, 但是像那些 FLUSH_SLAVE_TIMEOUT、FLUSH_DISK_TIMEOUT 都属于刷盘超
    // 时的或者其他错误,不影响消息的存储
    if (sendOK) {

        // 如果发送的消息的 topic 是 SCHEDULE_TOPIC_XXXX
        if (TopicValidator.RMQ_SYS_SCHEDULE_TOPIC.equals(msg.getTopic())) {
            // 增加消息写入的条数和这个队列写入的消息数 + 1
            this.brokerController.getBrokerStatsManager().incQueuePutNums(msg.getTopic(), msg.getQueueId(), putMessageResult.getAppendMessageResult().getMsgNum(), 1);
            // 队列写入的消息字节字节也增加
            this.brokerController.getBrokerStatsManager().incQueuePutSize(msg.getTopic(), msg.getQueueId(), putMessageResult.getAppendMessageResult().getWroteBytes());
        }

        // topic 消息数 + 1
        this.brokerController.getBrokerStatsManager().incTopicPutNums(msg.getTopic(), putMessageResult.getAppendMessageResult().getMsgNum(), 1);
        // topic 新增的消息字节数
        this.brokerController.getBrokerStatsManager().incTopicPutSize(msg.getTopic(),
            putMessageResult.getAppendMessageResult().getWroteBytes());
        // broker 存储的消息数
        this.brokerController.getBrokerStatsManager().incBrokerPutNums(putMessageResult.getAppendMessageResult().getMsgNum());

        response.setRemark(null);

        // 设置消息 ID、队列 ID、队列偏移量(ConsumeQueue offset)
        responseHeader.setMsgId(putMessageResult.getAppendMessageResult().getMsgId());
        responseHeader.setQueueId(queueIdInt);
        responseHeader.setQueueOffset(putMessageResult.getAppendMessageResult().getLogicsOffset());

        // 将响应结果发送给生产者
        doResponse(ctx, request, response);

        // 设置 sendMessageContext 消息发送上下文
        if (hasSendMessageHook()) {
            sendMessageContext.setMsgId(responseHeader.getMsgId());
            sendMessageContext.setQueueId(responseHeader.getQueueId());
            sendMessageContext.setQueueOffset(responseHeader.getQueueOffset());

            int commercialBaseCount = brokerController.getBrokerConfig().getCommercialBaseCount();
            int wroteSize = putMessageResult.getAppendMessageResult().getWroteBytes();
            int incValue = (int)Math.ceil(wroteSize / BrokerStatsManager.SIZE_PER_COUNT) * commercialBaseCount;

            sendMessageContext.setCommercialSendStats(BrokerStatsManager.StatsType.SEND_SUCCESS);
            sendMessageContext.setCommercialSendTimes(incValue);
            sendMessageContext.setCommercialSendSize(wroteSize);
            sendMessageContext.setCommercialOwner(owner);
        }
        return null;
    } else {
        // 发送失败, 同样也处理消息发送上下文
        if (hasSendMessageHook()) {
            int wroteSize = request.getBody().length;
            int incValue = (int)Math.ceil(wroteSize / BrokerStatsManager.SIZE_PER_COUNT);

            sendMessageContext.setCommercialSendStats(BrokerStatsManager.StatsType.SEND_FAILURE);
            sendMessageContext.setCommercialSendTimes(incValue);
            sendMessageContext.setCommercialSendSize(wroteSize);
            sendMessageContext.setCommercialOwner(owner);
        }
    }
    // 返回结果
    return response;
}

这里就直接看下注释就行,这个方法主要就是做了两件事,根据不同请求结果设置 response 里面的 code 和 remark,然后如果存入 CommitLog 成功了,统计下数据,比如 topic 消息数、topic 新增的消息字节数、broker 存储的消息数,最后将响应结果通过 writeAndFlush 发送给生产者。

7. 小结

这篇文章我们看了下 broker 是如何处理单条消息的,其中的处理逻辑都是在 SendMessageProcessor 中完成,那么 broker 是如何通过请求 Code 获取到 SendMessageProcessor 来处理发送请求的,这个就留到下一篇文章再说。





如有错误,欢迎指出!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值