Comment rocketMQ utilise-t-il MQFaultStrategy pour éviter les erreurs de retard ?

arrière-plan

Dans un cluster RocketMq, lorsque les files d'attente sont réparties entre différents serveurs de courtage, lors de la tentative d'envoi d'un message à l'une des files d'attente, si l'envoi prend trop de temps ou échoue, RocketMQ essaiera de réessayer d'envoyer. Si vous souhaitez y réfléchir, le même message ne parvient pas à être envoyé pour la première fois ou prend trop de temps, ce qui peut être dû à des fluctuations du réseau ou à l'arrêt du courtier concerné. Si vous réessayez dans peu de temps, il est très probablement que la même situation se reproduira .

RocketMQ nous fournit la fonction de basculement automatique des files d'attente après des échecs retardés, et prédira le temps d'échec et récupérera automatiquement en fonction du nombre d'échecs et des niveaux d'échec. Cette fonction est facultative et désactivée par défaut. Elle peut être activée via la configuration suivante .

DefaultMQProducer producer = new DefaultMQProducer("producerGroup");
producer.setSendLatencyFaultEnable(true);

Remarque : Cette fonction ne prend effet que lorsqu'aucune file d'attente n'est spécifiée

Interprétation du code source

Nous localisons le code source lié à la décision de la file d'attente et réessayonsorg.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl

private SendResult sendDefaultImpl(
    Message msg,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback,
    final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    
    
    this.makeSureStateOK();
    // 参数校验
    Validators.checkMessage(msg, this.defaultMQProducer);
    final long invokeID = random.nextLong();
    long beginTimestampFirst = System.currentTimeMillis();
    long beginTimestampPrev = beginTimestampFirst;
    long endTimestamp = beginTimestampFirst;
    // 获得topic发布的信息
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    // 这里.ok()是判断是否有可用的queue,只有当queue不为空时才能将消息投递出去
    if (topicPublishInfo != null && topicPublishInfo.ok()) {
    
    
        boolean callTimeout = false;
        MessageQueue mq = null;
        Exception exception = null;
        SendResult sendResult = null;
        // 同步执行需要设置一个最大重试次数
        int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
        int times = 0;
        String[] brokersSent = new String[timesTotal];
        for (; times < timesTotal; times++) {
    
    
            String lastBrokerName = null == mq ? null : mq.getBrokerName();
            // 选择投递的queue,会自动规避最近故障的queue
            MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
            if (mqSelected != null) {
    
    
                mq = mqSelected;
                brokersSent[times] = mq.getBrokerName();
                try {
    
    
                    beginTimestampPrev = System.currentTimeMillis();
                    if (times > 0) {
    
    
                        // 为了防止namespace状态发生变更,重试期间利用namespace重新解析topic名称
                        msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()));
                    }
                    long costTime = beginTimestampPrev - beginTimestampFirst;
                    if (timeout < costTime) {
    
    
                        // 如果超时则break停止投递
                        callTimeout = true;
                        break;
                    }

                    // 开始投递消息
                    sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                    endTimestamp = System.currentTimeMillis();
                    // 更新发送超时记录,用于规避再次故障
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                    switch (communicationMode) {
    
    
                        case ASYNC:
                            return null;
                        case ONEWAY:
                            return null;
                        case SYNC:
                            if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
    
    
                                if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
    
    
                                    // 失败则尝试投递其他broker
                                    continue;
                                }
                            }
                            return sendResult;
                        default:
                            break;
                    }
                } catch (RemotingException e) {
    
    
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                    log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                    log.warn(msg.toString());
                    exception = e;
                    continue;
                } catch (MQClientException e) {
    
    
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                    log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                    log.warn(msg.toString());
                    exception = e;
                    continue;
                } catch (MQBrokerException e) {
    
    
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                    log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                    log.warn(msg.toString());
                    exception = e;
                    if (this.defaultMQProducer.getRetryResponseCodes().contains(e.getResponseCode())) {
    
    
                        continue;
                    } else {
    
    
                        if (sendResult != null) {
    
    
                            return sendResult;
                        }

                        throw e;
                    }
                } catch (InterruptedException e) {
    
    
                    endTimestamp = System.currentTimeMillis();
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                    log.warn(String.format("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
                    log.warn(msg.toString());

                    log.warn("sendKernelImpl exception", e);
                    log.warn(msg.toString());
                    throw e;
                }
            } else {
    
    
                break;
            }
        }

        // 是否有响应数据,有则直接响应结果
        if (sendResult != null) {
    
    
            return sendResult;
        }
        // 下面就是异常类的包装和抛出操作

        String info = String.format("Send [%d] times, still failed, cost [%d]ms, Topic: %s, BrokersSent: %s",
            times,
            System.currentTimeMillis() - beginTimestampFirst,
            msg.getTopic(),
            Arrays.toString(brokersSent));

        info += FAQUrl.suggestTodo(FAQUrl.SEND_MSG_FAILED);

        MQClientException mqClientException = new MQClientException(info, exception);
        if (callTimeout) {
    
    
            throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
        }

        if (exception instanceof MQBrokerException) {
    
    
            mqClientException.setResponseCode(((MQBrokerException) exception).getResponseCode());
        } else if (exception instanceof RemotingConnectException) {
    
    
            mqClientException.setResponseCode(ClientErrorCode.CONNECT_BROKER_EXCEPTION);
        } else if (exception instanceof RemotingTimeoutException) {
    
    
            mqClientException.setResponseCode(ClientErrorCode.ACCESS_BROKER_TIMEOUT);
        } else if (exception instanceof MQClientException) {
    
    
            mqClientException.setResponseCode(ClientErrorCode.BROKER_NOT_EXIST_EXCEPTION);
        }

        // 将包装好的异常结果抛出
        throw mqClientException;
    }

    // 校验NameServer服务器是否正常
    validateNameServerSetting();

    // 抛出topic异常信息
    throw new MQClientException("No route info of this topic: " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO),
        null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION);
}

Faites attention à this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);cette ligne de code. Ce code sera appelé immédiatement après que le message aura été envoyé au courtier, et cette méthode sera appelée si l'exception est interceptée. Nous avons dit précédemment que le courtier sera temporairement marqué comme indisponible s'il prend trop long ou ne parvient pas à envoyer. Voyons voir comment il est implémenté en bas.

Suivez le code pour localiserorg.apache.rocketmq.client.latency.MQFaultStrategy#updateFaultItem

    public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
    
    
        // 配置如果开启则生效
        if (this.sendLatencyFaultEnable) {
    
    
            // 如果是个隔离异常则标记执行持续时长为30秒,并根据执行时长计算broker不可用时长
            long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
            // 记录broker不可用的时长信息
            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;
    }

La méthode ci-dessus a trois paramètres d'entrée

  • brokerName Le nom du courtier
  • currentLatency à partir de la latence actuelle
  • Que l'isolement soit isolé ou non, le lieu marquera directement le temps de retard de l'état d'isolement à 30 secondes, c'est-à-dire que lorsqu'il est vrai, le temps d'exécution est considéré comme étant de 30 secondes

computeNotAvailableDurationDeux tableaux sont utilisés dans la méthode, regardons ces deux tableaux

private long[] latencyMax = {
    
    50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
private long[] notAvailableDuration = {
    
    0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};

Le latencyMax ci-dessus indique le temps d'exécution, et notAvailableDuration indique la période d'indisponibilité du courtier, et leurs bits d'index correspondent les uns aux autres. Cette méthode consiste à parcourir la position d'index en sens inverse. En supposant que le temps de transmission du message actuel est de 600 ms, et que le l'indice latencyMax correspondant est 2, puis l'indice notAvailableDuration Egalement 2, la durée d'indisponibilité de ce courtier est de 30 000 ms.

Voyons comment les courtiers indisponibles sont maintenus et localisons-les logiquementorg.apache.rocketmq.client.latency.LatencyFaultToleranceImpl#updateFaultItem

@Override
public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
    
    
    // 查看broker是否被标记过
    FaultItem old = this.faultItemTable.get(name);
    if (null == old) {
    
    
        // 没有则进行一次标记
        final FaultItem faultItem = new FaultItem(name);
        // 记录本次的耗时
        faultItem.setCurrentLatency(currentLatency);
        // 当前时间+不可用时间=截至时间
        faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);

        // 再次尝试放入map中,为了防止并发情况下key已存在,则使用putIfAbsent
        old = this.faultItemTable.putIfAbsent(name, faultItem);
        if (old != null) {
    
    
            // 放入时已存在则更新存在的对象
            old.setCurrentLatency(currentLatency);
            old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
        }
    } else {
    
    
        // broker被标记过则直接更新
        old.setCurrentLatency(currentLatency);
        old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
    }
}

Le courtier marqué le maintient à l'aide de ConcurrentHashMap 延迟对象, qui contient le temps écoulé et le temps jusqu'à ce qu'il soit indisponible

A ce stade, nous connaissons le principe d'indisponibilité anormale. Examinons ensuite le code lié à la prise de décision automatique de la file d'attente. Nous repéronsorg.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl

Faites attention à MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);cette ligne de code, en raison de la logique de nouvelle tentative, il y a lastBrokerName, qui signifie le courtier utilisé lors du dernier appel, et topicPublishInfo, qui signifie les informations liées au sujet à fournir.

Suivez la logique pour entrer dans la méthode de prise de décision de la file d'attente principaleorg.apache.rocketmq.client.latency.MQFaultStrategy#selectOneMessageQueue

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
    
    
    // 是否开启延迟故障功能
    if (this.sendLatencyFaultEnable) {
    
    
        try {
    
    
            // 使用threadlocal维护索引位置,做到线程隔离
            int index = tpInfo.getSendWhichQueue().incrementAndGet();
            // 遍历所有可用queue
            for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
    
    
                // 索引位置对queue数量进行取模,保证分布尽量均匀
                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()))
                    return mq;
            }
            // 没有可用的broker走下面逻辑
            // 从疑似故障的broker中强行取一个broker出来
            final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
            // 从broker中取一个queue
            int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
            if (writeQueueNums > 0) {
    
    
                final MessageQueue mq = tpInfo.selectOneMessageQueue();
                if (notBestBroker != null) {
    
    
                    mq.setBrokerName(notBestBroker);
                    mq.setQueueId(tpInfo.getSendWhichQueue().incrementAndGet() % writeQueueNums);
                }
                return mq;
            } else {
    
    
                latencyFaultTolerance.remove(notBestBroker);
            }
        } catch (Exception e) {
    
    
            log.error("Error occurred when selecting message queue", e);
        }

        return tpInfo.selectOneMessageQueue();
    }
    // 重最后一个broker中取出一个queue
    return tpInfo.selectOneMessageQueue(lastBrokerName);
}

Voici latencyFaultTolerance.isAvailable(mq.getBrokerName())l'utilisation de l'objet retardé stocké dans le ConcurrentHashMap précédent pour déterminer s'il est disponible en comparant avec l'heure actuelle

public boolean isAvailable() {
    
    
    // startTimestamp是 上次调度故障的时间+故障恢复时间
    return (System.currentTimeMillis() - startTimestamp) >= 0;
}

Que se passe-t-il si tous les courtiers ici ont été marqués comme défectueux et que le temps de récupération n'a pas encore été atteint ?

Le code ci-dessus final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();en retirera un de force du courtier défectueux, et nous le localiseronsorg.apache.rocketmq.client.latency.LatencyFaultToleranceImpl#pickOneAtLeast

public String pickOneAtLeast() {
    
    
    final Enumeration<FaultItem> elements = this.faultItemTable.elements();
    List<FaultItem> tmpList = new LinkedList<FaultItem>();
    while (elements.hasMoreElements()) {
    
    
        final FaultItem faultItem = elements.nextElement();
        tmpList.add(faultItem);
    }

    if (!tmpList.isEmpty()) {
    
    
        // 这里属于无效操作,忽略就好,官方已在最新版本修复
        Collections.shuffle(tmpList);

        // 进行排序
        Collections.sort(tmpList);

        // 这段逻辑表示只从延迟最低的一半broker中选择一个
        final int half = tmpList.size() / 2;
        if (half <= 0) {
    
    
            return tmpList.get(0).getName();
        } else {
    
    
            final int i = this.whichItemWorst.incrementAndGet() % half;
            return tmpList.get(i).getName();
        }
    }

    return null;
}

Attention à la logique ci-dessus, il ne s'agit pas d'en prendre un au hasard, d'abord il sera trié en fonction du retard du dernier ordonnancement, puis il sera divisé par deux, et un seul courtier sera pris modulo du plus rapide la moitié des courtiers.

Ignorez simplement ce qui précède Collections.shuffle(tmpList);, le responsable a déclaré que cela serait corrigé https://github.com/apache/rocketmq/pull/3945

On peut voir que RocketMQ a fait beaucoup de considérations à la fin de l'envoi du message juste pour la fusion des pannes et le basculement.Bien sûr, le code décrit dans ce chapitre n'est applicable que lorsque la file d'attente n'est pas spécifiée manuellement et que la fonction de panne de retard d'envoi est activé.

Je suppose que tu aimes

Origine blog.csdn.net/qq_21046665/article/details/125892156
conseillé
Classement