当前位置:首页 > 随笔 > 正文内容

RocketMQ 消息发送流程与 Broker 故障延迟机制解析

访客 随笔 2026年10月15日 2

消息发送的容错思路

Producer 向 Topic 发送消息时,RocketMQ 客户端需要在多个 Broker 的队列之间进行选择。如果某个 Broker 因为网络抖动、GC 暂停或自身负载过高而响应变慢,反复向其发送消息会加剧问题。为此,RocketMQ 在消息发送链路中提供了两种主要容错手段:

  • 发送失败后的重试机制;
  • Broker 故障延迟机制,即把近期表现不佳的 Broker 暂时从候选队列中剔除,等待一段"隔离时间"后再重新尝试。

隔离时间的长度与发送耗时正相关:发送越慢,被"冷落"的时间越长。下面重点分析故障延迟机制的实现以及消息从客户端到 Broker 的完整发送链路。

故障延迟机制的核心实现

故障延迟机制由 LatencyFaultTolerance 接口及其实现类负责。每个 Broker 对应一条故障记录,保存当前延迟和恢复可用的时间戳。

class BrokerFaultRecord implements Comparable<BrokerFaultRecord> {
    private final String brokerName;
    private long currentLatency;
    private long availableTime;

    public BrokerFaultRecord(String brokerName) {
        this.brokerName = brokerName;
    }

    public boolean isAvailable() {
        return System.currentTimeMillis() >= this.availableTime;
    }

    @Override
    public int compareTo(BrokerFaultRecord other) {
        return Long.compare(this.availableTime, other.availableTime);
    }

    // getter / setter ...
}

管理类负责更新记录、判断可用性以及在多条故障记录中挑选相对"较轻"的 Broker。

public class FaultToleranceManager implements LatencyFaultTolerance<String> {
    private final ConcurrentHashMap<String, BrokerFaultRecord> records =
            new ConcurrentHashMap<>();
    private final AtomicInteger worstSelector = new AtomicInteger(0);

    @Override
    public void updateFaultItem(String brokerName, long currentLatency,
                                long notAvailableDuration) {
        long now = System.currentTimeMillis();
        BrokerFaultRecord old = records.get(brokerName);
        if (old == null) {
            BrokerFaultRecord record = new BrokerFaultRecord(brokerName);
            record.setCurrentLatency(currentLatency);
            record.setAvailableTime(now + notAvailableDuration);
            old = records.putIfAbsent(brokerName, record);
            if (old != null) {
                old.setCurrentLatency(currentLatency);
                old.setAvailableTime(now + notAvailableDuration);
            }
        } else {
            old.setCurrentLatency(currentLatency);
            old.setAvailableTime(now + notAvailableDuration);
        }
    }

    @Override
    public boolean isAvailable(String brokerName) {
        BrokerFaultRecord record = records.get(brokerName);
        return record == null || record.isAvailable();
    }

    @Override
    public String pickOneAtLeast() {
        List<BrokerFaultRecord> list = new ArrayList<>(records.values());
        if (list.isEmpty()) {
            return null;
        }
        Collections.shuffle(list);
        Collections.sort(list);

        int half = list.size() / 2;
        if (half <= 0) {
            return list.get(0).getBrokerName();
        }
        int index = worstSelector.getAndIncrement() % half;
        return list.get(index).getBrokerName();
    }

    @Override
    public void remove(String brokerName) {
        records.remove(brokerName);
    }
}

pickOneAtLeast 先对记录做一次随机化再排序,随后在前一半恢复时间较短的 Broker 中轮询选择。这样既优先使用"问题较小"的 Broker,又通过轮询避免多个 Producer 线程扎堆。

队列选择逻辑

Producer 真正发送前,会先调用队列选择策略。下面是一段重构后的队列选择代码,变量名和结构均做了调整,但逻辑保持不变。

public MessageQueue selectOneMessageQueue(TopicPublishInfo topicInfo, String lastBroker) {
    if (!this.sendLatencyFaultEnable) {
        return topicInfo.selectOneMessageQueue(lastBroker);
    }

    int startIndex = topicInfo.getSendWhichQueue().getAndIncrement();
    List<MessageQueue> queueList = topicInfo.getMessageQueueList();

    for (int i = 0; i < queueList.size(); i++) {
        int pos = Math.abs(startIndex + i) % queueList.size();
        if (pos < 0) {
            pos = 0;
        }
        MessageQueue candidate = queueList.get(pos);
        if (!this.faultTolerance.isAvailable(candidate.getBrokerName())) {
            continue;
        }
        if (lastBroker == null || lastBroker.equals(candidate.getBrokerName())) {
            return candidate;
        }
    }

    String fallbackBroker = this.faultTolerance.pickOneAtLeast();
    int writeQueueNums = topicInfo.getQueueIdByBroker(fallbackBroker);
    if (writeQueueNums > 0) {
        MessageQueue fallback = topicInfo.selectOneMessageQueue();
        fallback.setBrokerName(fallbackBroker);
        fallback.setQueueId(
            topicInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums
        );
        return fallback;
    }

    if (fallbackBroker != null) {
        this.faultTolerance.remove(fallbackBroker);
    }
    return topicInfo.selectOneMessageQueue();
}

这段逻辑的含义是:先轮询所有队列,跳过被标记为不可用的 Broker;如果所有队列都不合适,则从故障记录中挑选一个相对可靠的 Broker,临时构造一个目标队列;如果连回退对象也无法使用,则清空该记录并回到默认选择策略。

发送延迟的计算

发送结束后,Producer 会根据本次发送耗时更新故障记录。下面给出经过重构的延迟计算与更新逻辑。

public class SendFaultStrategy {
    private final LatencyFaultTolerance<String> faultTolerance;
    private final long[] latencyThresholds =
            {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
    private final long[] isolateDurations =
            {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};

    public void updateFaultItem(String brokerName, long sendCost, boolean isolation) {
        if (!this.sendLatencyFaultEnable) {
            return;
        }
        long actualLatency = isolation ? 30000 : sendCost;
        long duration = computeNotAvailableDuration(actualLatency);
        this.faultTolerance.updateFaultItem(brokerName, actualLatency, duration);
    }

    private long computeNotAvailableDuration(long latency) {
        for (int i = latencyThresholds.length - 1; i >= 0; i--) {
            if (latency >= latencyThresholds[i]) {
                return isolateDurations[i];
            }
        }
        return 0L;
    }
}

当发送耗时达到 550 ms 时,Broker 会被隔离 30 秒;耗时越高,隔离时间越长。若 isolation 为 true,则按 30 秒计算延迟,用于明确判定 Broker 不可用的场景。

消息发送的核心流程

以异步发送为例,客户端代码通常如下:

DefaultMQProducer producer = new DefaultMQProducer("AsyncProducerGroup");
producer.setRetryTimesWhenSendAsyncFailed(0);
producer.start();

for (int i = 0; i < messageCount; i++) {
    byte[] body = ("hello-" + i).getBytes(StandardCharsets.UTF_8);
    Message msg = new Message("TopicDemo", "TagA", "Key" + i, body);
    final int seq = i;
    producer.send(msg, new SendCallback() {
        @Override
        public void onSuccess(SendResult sendResult) {
            System.out.printf("%d OK %s%n", seq, sendResult.getMsgId());
        }

        @Override
        public void onException(Throwable e) {
            System.out.printf("%d Exception %s%n", seq, e.getMessage());
        }
    });
}

最终调用链会进入 DefaultMQProducerImpl#sendKernelImpl。该方法负责定位 Broker 地址、生成消息唯一 ID、压缩消息体、执行发送钩子,并构造 SendMessageRequestHeader,随后根据同步/异步/单向模式交给 MQClientAPIImpl 发送。下面是一段高度抽象后的核心流程代码:

private SendResult sendKernelImpl(Message msg, MessageQueue mq,
                                  CommunicationMode mode, SendCallback callback,
                                  TopicPublishInfo topicInfo, long timeout) {
    String brokerAddr = locateBrokerAddress(mq.getBrokerName());
    brokerAddr = MixAll.brokerVIPChannel(
            this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr);

    prepareMessage(msg);
    SendMessageRequestHeader header = buildRequestHeader(msg, mq);

    switch (mode) {
        case ASYNC:
            return remotingClient.sendMessage(
                    brokerAddr, mq.getBrokerName(), msg, header,
                    timeout, mode, callback, topicInfo, ...);
        case ONEWAY:
        case SYNC:
            return remotingClient.sendMessage(
                    brokerAddr, mq.getBrokerName(), msg, header,
                    timeout, mode, ...);
        default:
            throw new UnsupportedOperationException();
    }
}

private String locateBrokerAddress(String brokerName) {
    String addr = this.mQClientFactory.findBrokerAddressInPublish(brokerName);
    if (addr == null) {
        tryToFindTopicPublishInfo(brokerName);
        addr = this.mQClientFactory.findBrokerAddressInPublish(brokerName);
    }
    return addr;
}

private SendMessageRequestHeader buildRequestHeader(Message msg, MessageQueue mq) {
    SendMessageRequestHeader header = new SendMessageRequestHeader();
    header.setTopic(msg.getTopic());
    header.setQueueId(mq.getQueueId());
    header.setProducerGroup(this.defaultMQProducer.getProducerGroup());
    header.setBornTimestamp(System.currentTimeMillis());
    header.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
    // ...
    return header;
}

这里的 remotingClient 实际为 NettyRemotingClient,它把请求封装为 RequestCode.SEND_MESSAGE 命令发送到 Broker。

Broker 接收与存储

Broker 端由 SendMessageProcessor 处理 SEND_MESSAGE 请求。其主要流程是先做权限与 Topic 校验,再构造内部消息对象,最后调用存储引擎写入。

public class SendMessageProcessor {
    private RemotingCommand handleSendMessage(ChannelHandlerContext ctx,
                                              RemotingCommand request,
                                              SendMessageRequestHeader header) {
        RemotingCommand response = createResponse(request);

        preCheck(ctx, header, response);
        if (response.getCode() != ResponseCode.SUCCESS) {
            return response;
        }

        TopicConfig topicConfig =
                this.brokerController.getTopicConfigManager()
                        .selectTopicConfig(header.getTopic());
        int queueId = resolveQueueId(header.getQueueId(), topicConfig);

        MessageExtBrokerInner inner = buildInnerMessage(ctx, request, header, queueId);

        if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()
                && isTransactionMessage(inner)) {
            response.setCode(ResponseCode.NO_PERMISSION);
            return response;
        }

        PutMessageResult putResult =
                this.brokerController.getMessageStore().putMessage(inner);

        return assembleSendResult(putResult, response, inner, queueId);
    }
}

其中 preCheck 包括:

  • 检查 Broker 写权限;
  • 检查 Topic 是否允许发送、是否存在,不存在时自动创建;
  • 检查队列编号是否合法。

校验通过后,消息被转换为 MessageExtBrokerInner,随后进入 DefaultMessageStore#putMessage 持久化。写入成功后,Broker 返回 SendResult,Producer 根据结果决定是否触发重试或更新故障延迟记录。

完整链路

将以上步骤串联起来,一条消息从客户端到 Broker 的完整流程如下:

  1. 调用 DefaultMQProducer#send;
  2. DefaultMQProducerImpl 选择目标队列,期间利用故障延迟机制剔除不可用 Broker;
  3. 调用 sendKernelImpl 组装请求头,通过 NettyRemotingClient 发送命令;
  4. Broker 端的 SendMessageProcessor 校验请求并调用存储引擎写入消息;
  5. Producer 收到结果后,根据发送耗时更新故障延迟记录。
返回列表

上一篇:使用Kruskal算法求解最小生成树

没有最新的文章了...

相关文章

可以按小时收费的VPS

很多 VPS 提供商都支持 按小时计费(hourly billing),想短期试用 / 临时搭建节点、测试网络、短期项目等场景非常合适。下面是当前最主流且靠谱的按小时 VPS 选项,分别按不同需求场景整理: 1. Vultr(全球节点,包括日本) 按小时计费 可选机房:东京 / 大阪 / 洛杉矶 / 法兰克福 / 伦敦 … 支持 PayPal(部分情况),但更常用信用卡/PayPal+卡价格参考$...

在 iPhone 上下载国外App

地区/国家限制App Store 会根据 Apple ID 的国家或地区限制应用下载。如果你的 Apple ID 绑定的是中国大陆,就可能无法下载 OpenAI 官方的 ChatGPT 应用,因为它在大陆 App Store 不上架。解决办法:换成美国、加拿大、香港等地区的 Apple ID。或者在现有 Apple ID 上更改地区。注册一个国外 Apple ID(推荐)比如注册 美国区 Appl...

Node.js 中的异步编程:回调与 Promise

Node.js 是一个基于 JavaScript 构建的单线程、非阻塞运行环境,它通过异步编程机制来高效处理多个操作。在执行如文件读取、API 请求或数据库查询等任务时,Node.js 不会等待这些操作完成,而是使用回调函数和 Promise 来避免阻塞主线程。 回调方式实现异步 那么当异步操作完成后,Node.js 如何知道接下来要做什么呢?这就要用到 回调函数(callback)。 回调本质上...

Selenium自动化测试入门指南

Selenium自动化测试入门指南

什么是自动化测试? 自动化测试是指利用软件工具自动执行测试用例,模拟用户操作,如打开网页、点击链接、输入文本等,并验证结果是否符合预期。 其主要优点包括: 大幅减少人工成本 测试速度快 可以在非工作时间运行 支持持续集成和交付 然而,它也存在一些局限性,例如开发成本较高、不适合快速变化的项目、依赖稳定的UI界面等。 自动化测试的应用条件 适合引入自动化测试的情况包括: 手动测试耗时且需要大量...

MariaDB Galera集群故障快速恢复指南

OpenStack控制节点采用三节点MariaDB Galera集群架构。当数据库集群因故障重启时,有时会出现Galera集群无法正常启动的问题。虽然有多种方法可以恢复数据库服务,但如何实现快速启动同时确保数据完整性呢? 通过分析日志发现,MariaDB Galera集群节点宕机时会在日志中输出以下信息: [Note] WSREP: 新集群视图:全局状态: 874d8e7e-5980-11e8-8...

Android 中 EventBus 的通信机制与实现原理深度解析

EventBus 核心设计思想 EventBus 是一个基于观察者模式的事件总线框架,广泛应用于 Android 平台以实现组件解耦。它通过中心化的消息分发机制,使不同层级、不同线程的对象能够以"发布-订阅"方式通信,避免了传统接口回调或广播带来的强依赖问题。 核心角色说明 事件(Event):任意 Java 对象,作为数据载体,如网络状态变更通知、用户登录信息等。 发布者(Publi...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。