メッセージキューの選択を送信するためのRocketMQのプロデューサーのソースコード分析

1.説明

2つのタイプがあります。1つはメッセージを直接送信することです。クライアント内のキューを選択するためのアルゴリズムがあり、外部からの変更は許可されていません。カスタムキュー選択アルゴリズムもあります(3つのアルゴリズムが組み込まれています。気に入らない場合は、アルゴリズムをカスタマイズして実現できます)。

public class org.apache.rocketmq.client.producer.DefaultMQProducer {
    // 只发送消息,queue的选择由默认的算法来实现
    @Override
    public SendResult send(Collection<Message> msgs) {}
    
    // 自定义选择queue的算法进行发消息
    @Override
    public SendResult send(Collection<Message> msgs, MessageQueue messageQueue) {}
}

第二に、ソースコード

1、send(msg、mq)

1.1、使用シナリオ

デフォルトのキュー選択アルゴリズムが必要ない場合もありますが、カスタマイズする必要があります。一般的に、最も一般的に使用されるシナリオはシーケンシャルメッセージです。シーケンシャルメッセージの送信では、通常、特定の特性セットを持つメッセージが同じキューで送信されることを指定します。順序が保証されるように。、単一のキューが順序付けられているため。

シーケンスメッセージがわからない場合は、前回のシーケンスメッセージの記事をご覧ください。
RocketMQシーケンシャルメッセージの原理とコードを紹介します

1.2、主成分分析

3つの組み込みアルゴリズムがあり、それらはすべて共通のインターフェースを実装しています。

org.apache.rocketmq.client.producer.MessageQueueSelector

  • SelectMessageQueueByRandom

  • SelectMessageQueueByHash

  • SelectMessageQueueByMachineRoom

  • ロジックをカスタマイズする場合は、インターフェイスを直接実装して、selectメソッドをオーバーライドできます。

非常に典型的な戦略モード、異なるアルゴリズムには異なる実装クラスがあり、トップレベルのインターフェースがあります。

1.2.1、SelectMessageQueueByRandom

public class SelectMessageQueueByRandom implements MessageQueueSelector {
    private Random random = new Random(System.currentTimeMillis());
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        // mqs.size():队列的个数。假设队列个数是4,那么这个value就是0-3之间随机。
        int value = random.nextInt(mqs.size());
        return mqs.get(value);
    }
}

とても簡単なのは純粋なランダム性です。

mqs.size():キューの数。キューの数が4であると仮定すると、この値は0〜3の間でランダムになります。

1.2.2、SelectMessageQueueByHash

public class SelectMessageQueueByHash implements MessageQueueSelector {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int value = arg.hashCode();
        // 防止出现负数,取个绝对值,这也是我们平时开发中需要注意到的点
        if (value < 0) {
            value = Math.abs(value);
        }
        // 直接取余队列个数。
        value = value % mqs.size();
        return mqs.get(value);
    }
}

とても簡単で、純粋に残りを取ります。

mqs.size():キューの数。キューの数が4で、値のハッシュコードが3、3%4 = 3であるとすると、添え字は0から始まるため、これが最後のキュー、つまり4番目のキューになります。

1.2.3、SelectMessageQueueByMachineRoom

public class SelectMessageQueueByMachineRoom implements MessageQueueSelector {
    private Set<String> consumeridcs;
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        return null;
    }
    public Set<String> getConsumeridcs() {
        return consumeridcs;
    }
    public void setConsumeridcs(Set<String> consumeridcs) {
        this.consumeridcs = consumeridcs;
    }
}

鳥の用途がわからないので、nullを返すだけです。したがって、カスタム要件がある場合は、直接カスタマイズするだけで、卵の用途はわかりません。

1.2.4、カスタムアルゴリズム

public class MySelectMessageQueue implements MessageQueueSelector {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        return mqs.get(0);
    }
}

最初のキューであるキュー0を常に選択してください。例を挙げると、それは本当にあなたのビジネスニーズに依存します。

1.3、コールチェーン

org.apache.rocketmq.client.producer.DefaultMQProducer#send(Message msg, MessageQueueSelector selector, Object arg)
->
org.apache.rocketmq.client.producer.DefaultMQProducer#send(Message msg, MessageQueueSelector selector, Object arg)
->
org.apache.rocketmq.client.producer.DefaultMQProducer#send(Message msg, MessageQueueSelector selector, Object arg, long timeout)
->
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendSelectImpl(xxx)
->
mq = mQClientFactory.getClientConfig().queueWithNamespace(selector.select(messageQueueList, userMessage, arg));
->
selector.select(messageQueueList, userMessage, arg)
->
org.apache.rocketmq.client.producer.MessageQueueSelector#select(final List<MessageQueue> mqs, final Message msg, final Object arg)

2、send(msg)

2.1、使用シナリオ

通常、これは特別な要件がないシナリオで使用されます。彼のデフォルトのキュー選択アルゴリズムは非常に優れているため、さまざまな最適化シナリオが考えられています。

2.2。主成分分析

// {@link org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl}
// 这是发送消息核心原理,不清楚的看我之前发消息源码分析的文章

// 选择消息要发送的队列
MessageQueue mq = null;
for (int times = 0; times < 3; times++) {
    // 首次肯定是null
    String lastBrokerName = null == mq ? null : mq.getBrokerName();
    // 调用下面的方法进行选择queue
    MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
    if (mqSelected != null) {
        // 给mq赋值,如果首次失败了,那么下次重试的时候(也就是下次for的时候),mq就有值了。
        mq = mqSelected;
        ......
        // 很关键,能解答下面会提到的两个问题:
        // 1.faultItemTable是什么时候放进去的?
        // 2.isAvailable() 为什么只是判断一个时间就可以知道Broker是否可用?   
        this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);    
    }
}

キューの正面玄関を選択します

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
    // 默认为false,代表不启用broker故障延迟
    if (this.sendLatencyFaultEnable) {
        try {
            // 随机数且+1
            int index = tpInfo.getSendWhichQueue().getAndIncrement();
            // 遍历
            for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                // 先(随机数 +1) % queue.size()
                int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                if (pos < 0) {
                    pos = 0;
                }
                MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                // 看找到的这个queue所属的broker是不是可用的
                if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                    // 非失败重试,直接返回到的队列
                    // 失败重试的情况,如果和选择的队列是上次重试是一样的,则返回
                    
                    // 也就是说如果你这个queue所在的broker可用,
                    // 且不是重试进来的或失败重试的情况,如果和选择的队列是上次重试是一样的,那你就是天选之子了。
                    if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName)) {
                        return mq;
                    }
                }
            }
            
			// 如果所有队列都不可用,那么选择一个相对好的broker,不考虑可用性的消息队列
            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);
        }
		// 随机选择一个queue
        return tpInfo.selectOneMessageQueue();
    }
	// 当sendLatencyFaultEnable=false的时候选择queue的方法,默认就是false。
    return tpInfo.selectOneMessageQueue(lastBrokerName);
}

2.2.1。ブローカー障害遅延を有効にしないでください

sendLatencyFaultEnableはデフォルトでfalseであるため、sendLatencyFaultEnable = falseの場合は最初にロジックを確認してください

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
    // 第一次就是null,第二次(也就是重试的时候)就不是null了。
    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();
    }
}

次に、キューを初めて選択するロジックを引き続き確認します。

public MessageQueue selectOneMessageQueue() {
    // 当前线程有个ThreadLocal变量,存放了一个随机数 {@link org.apache.rocketmq.client.common.ThreadLocalIndex#getAndIncrement}
    // 然后取出随机数根据队列长度取模且将随机数+1
    int index = this.sendWhichQueue.getAndIncrement();
    int pos = Math.abs(index) % this.messageQueueList.size();
    if (pos < 0) {
        pos = 0;
    }
    return this.messageQueueList.get(pos);
}

まあ、それは実際には少しランダムを意味します。ただし、ハイライトは、キューの長さを法として乱数を取り出し、乱数+1を追加すると、この+1が点灯することです(getAndIncrement cas +1)。

メッセージの送信に初めて失敗した場合、lastBrokerNameは、現在の選択に失敗したブローカーを格納します(mq = mqSelected)。再試行後、lastBrokerNameには値があります。これは、最後に選択されたボーカーが送信に失敗したことを意味します。 sendWhichQueueローカルスレッド変数+1。最後のブローカーでなくなるまで選択メッセージキューをトラバースします。これは、最後に失敗したブローカーのロジックを回避するためです。

例:今回の乱数は1で、キューの長さは4、1%4 = 1であり、この時点で失敗して再試行に入ります。その後、再試行の前、つまり前のステップ1%4の後、彼は1を置きます++操作が実行された後、それは2になるので、今回再試行すると、それは2%4 = 2であり、失敗したばかりのブローカーを直接除外します。

次に、2番目の再試行選択キューのロジックを引き続き確認します。

// +1
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;
    }
}
// 没找到能用的queue的话继续走默认的那个
return selectOneMessageQueue();

とても簡単で、前回失敗しませんでした、私に入ってからもう一度やり直しましたか?私も非常に単純です。乱数+1を取り出して、キューの長さを調整します。ブローカーが前回失敗したかどうかを確認します。ブローカーが彼の子供である場合は、フィルターで除外し、キューをトラバースして検索します。使用できる次のもの。

2.2.2、ブローカー障害遅延を有効にする

それは次の場合のロジックです

if (this.sendLatencyFaultEnable) {
    ....
}

上記のコメントを見てください。非常に明確です。最初に(随机数 +1) % queue.size()、キューが属するブローカーが使用可能かどうかを確認します。使用可能な場合、再試行または失敗しません。選択したキューが最後の場合再試行テストは同じです。その後、戻るだけで完了です。では、ブローカーが利用可能かどうかをどのように確認しますか?

// {@link org.apache.rocketmq.client.latency.LatencyFaultToleranceImpl#isAvailable(String)}
public boolean isAvailable(final String name) {
    final FaultItem faultItem = this.faultItemTable.get(name);
    if (faultItem != null) {
        return faultItem.isAvailable();
    }
    return true;
}

// {@link org.apache.rocketmq.client.latency.LatencyFaultToleranceImpl.FaultItem#isAvailable()}
public boolean isAvailable() {
    return (System.currentTimeMillis() - startTimestamp) >= 0;
}

疑問に思う:

  • faultItemTableはいつ入れられましたか?
  • isAvailable()一定期間の判断だけでブローカーが利用可能かどうかを知ることができるのはなぜですか?

これには、メッセージが上記で送信された後に呼び出されるメソッドが必要です。

// {@link org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#updateFaultItem}
// 发送开始时间
beginTimestampPrev = System.currentTimeMillis();
// 进行发送
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout);
// 发送结束时间
endTimestamp = System.currentTimeMillis();
// 更新broker的延迟情况
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);

詳細なロジックは次のとおりです。

// {@link org.apache.rocketmq.client.latency.MQFaultStrategy#updateFaultItem}
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
    if (this.sendLatencyFaultEnable) {
        // 首次isolation传入的是false,currentLatency是发送消息所耗费的时间,如下
        // this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
        long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
        this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
    }
}

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

// 根据延迟时间对比MQFaultStrategy中的延迟级别数组latencyMax 不可用时长数组notAvailableDuration 来将该broker加进faultItemTable中。
private long computeNotAvailableDuration(final long currentLatency) {
    for (int i = latencyMax.length - 1; i >= 0; i--) {
        // 假设currentLatency花费了10ms,那么latencyMax里的数据显然不符合下面的所有判断,所以直接return 0;
        if (currentLatency >= latencyMax[i])
            return this.notAvailableDuration[i];
    }
    return 0;
}

// {@link org.apache.rocketmq.client.latency.LatencyFaultToleranceImpl#updateFaultItem()}
@Override
// 其实主要就是给startTimestamp赋值为当前时间+computeNotAvailableDuration(isolation ? 30000 : currentLatency);的结果,给isAvailable()所用
// 也就是说只有notAvailableDuration == 0的时候,isAvailable()才会返回true。
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);
        // 给startTimestamp赋值为当前时间+computeNotAvailableDuration(isolation ? 30000 : currentLatency);的结果,给isAvailable()所用
        faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);

        old = this.faultItemTable.putIfAbsent(name, faultItem);
        if (old != null) {
            old.setCurrentLatency(currentLatency);
            // 给startTimestamp赋值为当前时间+computeNotAvailableDuration(isolation ? 30000 : currentLatency);的结果,给isAvailable()所用
            old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
        }
    } else {
        old.setCurrentLatency(currentLatency);
        // 给startTimestamp赋值为当前时间+computeNotAvailableDuration(isolation ? 30000 : currentLatency);的结果,给isAvailable()所用
        old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
    }
}

次の2行のコードについて詳しく説明します。

private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
レイテンシーマックス notAvailableDuration
50L 0L
100L 0L
550L 30000L
1000L 60000L
2000L 120000L
3000L 180000L
15000L 600000L

これは

  • currentLatencyが50以上100未満の場合、notAvailableDurationは0です。
  • currentLatencyが100以上550未満の場合、notAvailableDurationは0です。
  • currentLatencyが550以上1000未満の場合、notAvailableDurationは300000です。
  • …などなど

別の例を挙げましょう:

分離がtrueに渡されると仮定すると、

long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);

次に、notAvailableDurationが600000Lで渡されます。isAvailableメソッドと組み合わせると、おおよそのプロセスは次のようになります。

RocketMQは、各ブローカーの利用可能時間(現在の時間+ notAvailableDuration)を予測します。現在の時間がこの時間よりも大きい場合、ブローカーが利用可能であることを意味し、notAvailableDurationにはlatencyMaxの間隔に対応する6つのレベルがあります。 、および着信currentLatencyに従って予測します。ブローカーはいつ利用可能になりますか。

だからこれをもう一度見てください

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

どの間隔に該当するかを確認する実行時間に応じて、0〜100の時間内にnotAvailableDurationが0になり、すべて利用可能になります。この値を超えると、利用可能時間が増加し始め、考慮されます。最適な解決策ではありません。あきらめてください。

2.3、コールチェーン

org.apache.rocketmq.client.producer.DefaultMQProducer#send(org.apache.rocketmq.common.message.Message)
->
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#send(org.apache.rocketmq.common.message.Message)
->
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#send(org.apache.rocketmq.common.message.Message, long)
->
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl(xxx)
->
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
->
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#selectOneMessageQueue(xxx) 
org.apache.rocketmq.client.latency.MQFaultStrategy#selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName)    

2.4。まとめ

  • フォールトトレランスが有効になっていない場合は、キューをポーリングして送信します。失敗した場合は、失敗したブローカーをフィルタリングして再試行します。
  • フォールトトレランス戦略が有効になっている場合、RocketMQの予測メカニズムを使用して、ブローカーが利用可能かどうかを予測します。
  • 前回失敗したブローカーが利用可能な場合、ブローカーのキューは引き続き選択されます
  • 上記の状況が失敗した場合は、送信するものをランダムに選択してください
  • メッセージを送信するとき、それは呼び出しの時間とエラーが報告されるかどうかを記録し、時間に基づいてブローカーの利用可能な時間を予測します

3、まとめ

1.質問

彼には2つのオーバーロードされたsend()メソッドがあり、1つはアルゴリズムセレクターをサポートし、もう1つはアルゴリズム選択をサポートしていません。キューアルゴリズム選択は典型的な戦略モードです。なぜsend(message)別々のクラスに抽出していない内蔵のキュー選択アルゴリズムがあり、その後、このクラスを実装org.apache.rocketmq.client.producer.MessageQueueSelectorインタフェースを?たとえばSelectMessageQueueByBest、次のように呼ばれます。

public class org.apache.rocketmq.client.producer.DefaultMQProducer {
    // 只发送消息,queue的选择由默认的算法来实现
    @Override
    public SendResult send(Collection<Message> msgs) {
        this.send(msgs, new SelectMessageQueueByBest().select(xxx));
    }
    
    // 自定义选择queue的算法进行发消息
    @Override
    public SendResult send(Collection<Message> msgs, MessageQueue messageQueue) {}
}

私の推測では、このアルゴリズムは複雑すぎて、他のタイプとの相互作用が多すぎて、パラメーターが他の3つの組み込みのものと異なる可能性があるため、それらをまとめることはできませんでしたが、それでも一緒に標準化されました。彼らは同じことをしましたが、アルゴリズムは異なり、それは典型的な戦略モードです。

2.インタビュー

Q:メッセージを送信するときにキューを選択するためのアルゴリズムは何ですか?

回答:メッセージを直接送信するタイプとキューを選択できないタイプの2つがあります。キュー選択アルゴリズムは次のとおりです。

  • フォールトトレランスが有効になっていない場合は、キューをポーリングして送信します。失敗した場合は、失敗したブローカーをフィルタリングして再試行します。
  • フォールトトレランス戦略が有効になっている場合、RocketMQの予測メカニズムを使用して、ブローカーが利用可能かどうかを予測します。
  • 前回失敗したブローカーが利用可能な場合、ブローカーのキューは引き続き選択されます
  • 上記の状況が失敗した場合は、送信するものをランダムに選択してください
  • メッセージを送信するとき、それは呼び出しの時間とエラーが報告されるかどうかを記録し、時間に基づいてブローカーの利用可能な時間を予測します

もう1つは、メッセージを送信するときにアルゴリズムを選択し、インターフェイスのカスタムアルゴリズムを実装することです。

  • SelectMessageQueueByRandom:ランダム
  • SelectMessageQueueByHash:ハッシュ
  • SelectMessageQueueByMachineRoom
  • インターフェイスのカスタマイズを実装する

おすすめ

転載: blog.csdn.net/qq_33762302/article/details/114783849