RocketMQ生产者负载均衡算法实现
生产者负载均衡官方文档的解释如下:
Producer端在发送消息的时候,会先根据Topic找到指定的TopicPublishInfo,在获取了TopicPublishInfo路由信息后,RocketMQ的客户端在默认方式下selectOneMessageQueue()方法会从TopicPublishInfo中的messageQueueList中选择一个队列(MessageQueue)进行发送消息。具体的容错策略均在MQFaultStrategy这个类中定义。这里有一个sendLatencyFaultEnable开关变量,如果开启,在随机递增取模的基础上,再过滤掉not available的Broker代理。所谓的"latencyFaultTolerance",是指对之前失败的,按一定的时间做退避。例如,如果上次请求的latency超过550Lms,就退避3000Lms;超过1000L,就退避60000L;如果关闭,采用随机递增取模的方式选择一个队列(MessageQueue)来发送消息,latencyFaultTolerance机制是实现消息发送高可用的核心关键所在。
可见生产者开启负载均衡其实是一种容错的策略,开启方式如下:
//开启客户端负载均衡,失败退避策略
producer.setSendLatencyFaultEnable(true);
在上一篇文章分享生产者的源码流程中,在发送流程中有一个这样的方法 private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout)实现了这个生产者的负载均衡,代码如下图:
这里循环次数就是发送消息的最大重试次数,循环中首先调用 selectOneMessageQueue 方法获取发送的队列信息。这个方法具体实现如下:
/**
* @Desc TODO Producer的负载均衡算法:选取MessageQueue
* @Date 2020/4/28
*/
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
if (this.sendLatencyFaultEnable) {
try {
//获取随机递增数字于队列长度取模
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;
//判断获取到的队列处于退避时间内
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
//第一次调用的时候是为null ,后面重试调用不为null
if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
return mq;
}
}
//随机选一个
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);
}
return tpInfo.selectOneMessageQueue();
}
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
首先根据随机递增数与队列数量取模,获取到到一个队列,然后需要调用isAvailable判断队列是否可用,isAvailable方法具体最终实现如下:
public boolean isAvailable() {
return (System.currentTimeMillis() - startTimestamp) >= 0;
}
其实就是判断拿到的延迟策略中持有的startTimestamp字段是否小于当前时间,这个字段表示的是当前队列从这个时间以后才可用。这个字段是由截图中的updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false); 这个行代码更新维护的。这个方法的方法体如下:
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
if (this.sendLatencyFaultEnable) {
long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
}
}
这里就判断是打开了开关,然后计算如果所选队列需要短暂隔离,则是使用30000毫秒作为间隔时间,如果不需要隔离就使用本次发送消息耗费的时间作为暂停的时间间隔。查看最终调用的方法如下:
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);
faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
old = this.faultItemTable.putIfAbsent(name, faultItem);
if (old != null) {
old.setCurrentLatency(currentLatency);
old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
}
} else {
old.setCurrentLatency(currentLatency);
old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
}
}
就是将队列规避时间的可用时间设置为当前时间加上间隔时间以后才可用。从而实现失败重试或者下次选取队列的时候避免拿到之前刚刚使用过的队列。