我们知道RocketMQ的NameServer并非强一致而是最终一致性的,也就是客户端隔一段时间定时去获取Broker信息,如果Broker一段时间内出现了故障,客户端并不能马上感应到,那RocketMQ如何做到消息发送的高可用呢?大致可以从下面三个方面来展开:
- 重试机制
- 依次更换队列
- 规避己故障的Broker
一,重试机制
在发送消息过程中有这样一段代码:
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
for (; times < timesTotal; times++) {
...
}
如果是同步发送的方式默认会重试3次,重试次数也可以通过retryTimesWhenSendFailed
进行配置。
二,依次更换队列
在selectOneMessageQueue方法在里,会从sendWhichQueue取得上一次使用过的队列的索引,这个sendWhichQueue是一个ThreadLocalIndex类型,里面有一个ThreadLocal,队列索引就是存储在这个ThreadLocal里,取得索引后会有一个自增的动作,然后再根据新的索引获取新的队列。举个例子:有一个名为broker-a的broker,里面有队列q1,q2,q3,q4,如果上一次使用的是队列q1那么索引自增后会选择q2作为新的发送队列。通过这样的方式会让这4个队列的负载尽量保持一致。代码如下:
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;
//获取一个队列,队列里记录着所属broker
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
//判断 broker 是否失效
if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
//第一次获取会是空;获取的broker是有效的且还是上一次获取使用过的
if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
return mq;
}
}
三,规避己故障的Broker
如果上次使用的Broker出现了故障或延时超过了一定的范围,RocketMQ是如何规避的呢?
每次发送完消息后RocketMQ都使用ConcurrentHashMap记录该broker与发送消息所花时间的对应关系,在下次发送消息之前会根据这个时间判断一下队列所在Broker是否可以使用,如果是可用的并且也是上次使用的Broker则直接使用,这里我们重点看一下获取的Broker不可使用的情况。我们假设有broker-a,broker-b两个broker,每个队列里都有q1,q2,q3,q4这四个队列:
在发送之前 sendWhichQueue 该值为 broker-a 的 q1,如果由于此时 broker-a 的突发流量异常大导致消息发送失败,会触发重试,按照轮循机制,下一个选择的队列为 broker-a 的 q2 队列,此次消息发送大概率还是会失败,即尽管会重试 2 次,但都是发送给同一个 Broker 处理,此过程会显得不那么靠谱,即大概率还是会失败,那这样重试的其实是没多大意义的。
RocketMQ 为了解决该问题,引入了故障规避机制,在消息重试的时候,会尽量规避上一次发送的 Broker,回到上述示例,当消息发往 broker-a q1 队列时返回发送失败,那重试的时候会先排除 broker-a 中所有队列,即这次会选择 broker-b q1 队列,增大消息发送的成功率。RocketMQ 提供了两种规避策略,该参数由 sendLatencyFaultEnable 控制,用户可干预,表示是否开启延迟规避机制,默认为不开启。规避broker-a的代码如下:
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);
final int half = tmpList.size() / 2;
if (half <= 0) {
return tmpList.get(0).getName();
} else {
final int i = this.whichItemWorst.getAndIncrement() % half;
return tmpList.get(i).getName();
}
}
return null;
}
开启延迟规避机制,一旦消息发送失败会将 broker-a “悲观”地认为在接下来的一段时间内该 Broker 不可用,在为未来某一段时间内所有的客户端不会向该 Broker 发送消息。这个延迟时间就是通过 notAvailableDuration、latencyMax 共同计算的,就首先先计算本次消息发送失败所耗的时延,然后对应 latencyMax 中哪个区间,即计算在 latencyMax 的下标,然后返回 notAvailableDuration 同一个下标对应的延迟值.例如,如果上次请求的latency超过550Lms,就退避3000Lms;超过1000L,就退避60000L;
private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
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;
}
关于是否开启故障规避需要谨慎使用,一般来说Broker的繁忙基本都是瞬时的,很快就可以恢复。如果我们开启了规避比如3分钟内不向broker-a发送消息,会导致消息在其他Broker激增,进一步会导致部分消费端无法消费到消息,增大其他消费者的处理压力,导致整体消费性能的下降。所以没有特殊场景建议不要开启broker的故障规避策略。
RocketMQ就是通过失败重试,依次更换队列还有规避有故障的broker这三个方面来保证消息发送的高可用的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。