阅读须知
- 文章中使用/* */注释的方法会做深入分析
正文
整个消息发送负载均衡和容错处理的内容都和消息发送流程有关系,所以我们分析时需要与消息发送流程结合起来一起看。消息发送时,客户端需要获取 topic 的路由信息,这样客户端才知道要发送到哪个 Broker 实例。我们在分析 RocketMQ 路由信息管理流程时提到过,客户端启动时,会开启定时任务来定时更新 topic 路由信息,消息发送时就会获取这部分信息,我们来分析一下 RocketMQ 消息发送时获取 topic 路由信息流程:
DefaultMQProducerImpl:
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
// 获取缓存中获取 topic 发布信息
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
/* 如果 topic 发布信息为空或者消息队列为空,尝试从 NameServer 拉取最新的 topic 路由信息 */
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
}
if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
return topicPublishInfo;
} else {
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
}
}
我们来简单介绍一下 TopicPublishInfo 中都包含哪些信息:
- orderTopic:是否是顺序消息
- messageQueueList:topic 对应的消息队列信息
- sendWhichQueue:每选择一次消息队列,该值会自增1,增至 Integer.MAX_VALUE 后,下一次自增重置为0,用于选择消息队列
- topicRouteData:topic 路由元数据
继续看下 TopicRouteData 中都包含哪些信息:
- queueData:topic 队列元数据
- brokerDatas:topic 分布的 broker 元数据
- filterServerTable:Broker 上 FilterServer 地址列表
继续分析路由信息拉取流程:
MQClientInstance:
public boolean updateTopicRouteInfoFromNameServer(final String topic) {
return updateTopicRouteInfoFromNameServer(topic, false, null);
}
MQClientInstance:
public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
DefaultMQProducer defaultMQProducer) {
try {
if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
try {
TopicRouteData topicRouteData;
if (isDefault && defaultMQProducer != null) {
// 如果 isDefault 为 true,则使用默认 topic 去查询,默认 topic 为 TBW102
topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),
1000 * 3);
if (topicRouteData != null) {
for (QueueData data : topicRouteData.getQueueDatas()) {
int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
data.setReadQueueNums(queueNums);
data.setWriteQueueNums(queueNums);
}
}
} else {
/* 使用参数 topic 查询路由信息 */
topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
}
if (topicRouteData != null) {
TopicRouteData old = this.topicRouteTable.get(topic);
// 判断 topic 路由信息是否发生改变
boolean changed = topicRouteDataIsChange(old, topicRouteData);
if (!changed) {
changed = this.isNeedUpdateTopicRouteInfo(topic);
} else {
log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
}
if (changed) {
TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
for (BrokerData bd : topicRouteData.getBrokerDatas()) {
this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
}
{
/* 将 TopicRouteData 转换为 TopicPublishInfo */
TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
publishInfo.setHaveTopicRouterInfo(true);
Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, MQProducerInner> entry = it.next();
MQProducerInner impl = entry.getValue();
if (impl != null) {
// 更新 producer 的 topic 发布信息缓存
impl.updateTopicPublishInfo(topic, publishInfo);
}
}
}
{
Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, MQConsumerInner> entry = it.next();
MQConsumerInner impl = entry.getValue();
if (impl != null) {
// 更新 consumer 的 topic 订阅信息
impl.updateTopicSubscribeInfo(topic, subscribeInfo);
}
}
}
log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData);
this.topicRouteTable.put(topic, cloneTopicRouteData);
return true;
}
} else {
log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}. [{}]", topic, this.clientId);
}
} catch (MQClientException e) {
if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
log.warn("updateTopicRouteInfoFromNameServer Exception", e);
}
} catch (RemotingException e) {
log.error("updateTopicRouteInfoFromNameServer Exception", e);
throw new IllegalStateException(e);
} finally {
this.lockNamesrv.unlock();
}
} else {
log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms. [{}]", LOCK_TIMEOUT_MILLIS, this.clientId);
}
} catch (InterruptedException e) {
log.warn("updateTopicRouteInfoFromNameServer Exception", e);
}
return false;
}
MQClientAPIImpl:
public TopicRouteData getDefaultTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis)
throws RemotingException, MQClientException, InterruptedException {
return getTopicRouteInfoFromNameServer(topic, timeoutMillis, false);
}
public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis)
throws RemotingException, MQClientException, InterruptedException {
return getTopicRouteInfoFromNameServer(topic, timeoutMillis, true);
}
public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis,
boolean allowTopicNotExist) throws MQClientException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException {
GetRouteInfoRequestHeader requestHeader = new GetRouteInfoRequestHeader();
requestHeader.setTopic(topic);
// 构建 GET_ROUTEINFO_BY_TOPIC 命令
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC, requestHeader);
// 调用远程服务
RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis);
assert response != null;
switch (response.getCode()) {
case ResponseCode.TOPIC_NOT_EXIST: {
if (allowTopicNotExist) {
log.warn("get Topic [{}] RouteInfoFromNameServer is not exist value", topic);
}
break;
}
case ResponseCode.SUCCESS: {
byte[] body = response.getBody();
if (body != null) {
return TopicRouteData.decode(body, TopicRouteData.class);
}
}
default:
break;
}
throw new MQClientException(response.getCode(), response.getRemark());
}
NameServer 对于 GET_ROUTEINFO_BY_TOPIC 命令的处理我们在分析路由信息管理流程时分析过,这里不再赘述。
MQClientInstance:
public static TopicPublishInfo topicRouteData2TopicPublishInfo(final String topic, final TopicRouteData route) {
TopicPublishInfo info = new TopicPublishInfo();
info.setTopicRouteData(route);
if (route.getOrderTopicConf() != null && route.getOrderTopicConf().length() > 0) {
// 顺序消息的 Broker 配置解析
String[] brokers = route.getOrderTopicConf().split(";");
for (String broker : brokers) {
String[] item = broker.split(":");
int nums = Integer.parseInt(item[1]);
for (int i = 0; i < nums; i++) {
MessageQueue mq = new MessageQueue(topic, item[0], i);
info.getMessageQueueList().add(mq);
}
}
// 设置 topic 发布信息为顺序消息
info.setOrderTopic(true);
} else {
List<QueueData> qds = route.getQueueDatas();
Collections.sort(qds);
for (QueueData qd : qds) {
// 遍历队列数据找到有写权限的队列
if (PermName.isWriteable(qd.getPerm())) {
BrokerData brokerData = null;
// 遍历找到队列所属的 Broker 信息
for (BrokerData bd : route.getBrokerDatas()) {
if (bd.getBrokerName().equals(qd.getBrokerName())) {
brokerData = bd;
break;
}
}
// Broker 信息为 null,继续遍历下一个队列
if (null == brokerData) {
continue;
}
// 如果 Broker 信息不包含 Master 地址,继续遍历下一个队列
if (!brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) {
continue;
}
for (int i = 0; i < qd.getWriteQueueNums(); i++) {
// 构建 MessageQueue 对象,自增序列作为 queueId 标识
MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
info.getMessageQueueList().add(mq);
}
}
}
// 设置 topic 发布信息为非顺序消息
info.setOrderTopic(false);
}
return info;
}
到这里就完成了消息发送的路由信息查找。路由信息查找过后,接下来自然需要从查找的路由信息中选择出来本次消息发送要发送到的队列:
DefaultMQProducerImpl:
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
return this.mqFaultStrategy.selectOneMessageQueue(tpInfo, lastBrokerName);
}
MQFaultStrategy:
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
// sendLatencyFaultEnable 用于配置是否启用 Broker 故障延迟机制,默认不启用
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()))
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);
}
return tpInfo.selectOneMessageQueue();
}
// 这里的 lastBrokerName 是上次发送失败的 BrokerName,首次发送时为 null
/* 默认策略选择消息队列 */
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
TopicPublishInfo:
public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
if (lastBrokerName == null) {
/* 首次发送,直接选择一个消息队列进行发送 */
return selectOneMessageQueue();
} else {
for (int i = 0; i < this.messageQueueList.size(); i++) {
int index = this.sendWhichQueue.getAndIncrement();
int pos = Math.abs(index) % this.messageQueueList.size();
if (pos < 0)
pos = 0;
MessageQueue mq = this.messageQueueList.get(pos);
// 规避掉上次发送失败的 Broker
if (!mq.getBrokerName().equals(lastBrokerName)) {
return mq;
}
}
return selectOneMessageQueue();
}
}
TopicPublishInfo:
public MessageQueue selectOneMessageQueue() {
int index = this.sendWhichQueue.getAndIncrement();
// 每次对 index 自增,然后与队列的大小取模,其实就是一个轮询算法
int pos = Math.abs(index) % this.messageQueueList.size();
if (pos < 0)
pos = 0;
return this.messageQueueList.get(pos);
}
到这里已经完成了本次发送的消息队列选择,这里有个问题我们需要注意下,我们看到本次消息发送时跳过了上次发送失败的 Broker,但是下一次消息发送还是会尝试将消息发送到这个失败的 Broker,短时间内的再次尝试很有可能还是失败的(这个要看具体的发送失败原因),这样不就造成了不必要的性能损耗了么?
我们之前在分析路由管理时看到,Broker 发生故障时,NameServer 检测 Broker 是否可用是通过定时心跳机制完成的,这中间的有延迟的,客户端从 NameServer 拉取路由信息是通过定时任务完成的,同样也存在延迟,所以当 Broker 故障时,客户端可能没有办法实时感知到,这样有可能多次消息发送都会发送到故障的 Broker 上,导致不必要的损耗,这样的情况是有可能发生的。
RocketMQ 当然也考虑到了这种情况,处理的办法就是我们前面提到的故障延迟机制,在开启故障延迟机制时,选择完消息队列后会调用 latencyFaultTolerance.isAvailable
方法验证消息队列是否可用,LatencyFaultTolerance 是故障延迟处理的核心类,默认实现为 LatencyFaultToleranceImpl,它内部维护了一个 faultItemTable 变量,用来保存 Broker 与故障条目(FaultItem)的映射,在消息发送过程(DefaultMQProducerImpl#sendDefaultImpl
)中出现异常或者正常发送成功,都会更新这个映射,我们来看实现:
DefaultMQProducerImpl:
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
this.mqFaultStrategy.updateFaultItem(brokerName, currentLatency, isolation);
}
currentLatency 是使用消息发送结束时间减去消息发送开始时间计算出来的值。
isolation 表示是否隔离,如果值为 true,则使用默认时长30s来计算 Broker 故障规避时长,如果值为 false,则使用本次消息发送延迟时间来计算 Broker 故障规避时长。
MQFaultStrategy:
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
if (this.sendLatencyFaultEnable) {
/* 计算不可用时长 */
long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
/* 更新 FaultItem */
this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
}
}
MQFaultStrategy:
private long computeNotAvailableDuration(final long currentLatency) {
// latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
// notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
for (int i = latencyMax.length - 1; i >= 0; i--) {
if (currentLatency >= latencyMax[i])
return this.notAvailableDuration[i];
}
return 0;
}
MQFaultStrategy 预定义了一些不可用时长,computeNotAvailableDuration 方法的目的就是通过传入的消息发送延迟时间来匹配一个不可用时长。
LatencyFaultToleranceImpl:
public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
FaultItem old = this.faultItemTable.get(name);
// 判断缓存映射中是否存在 Broker 对应的 FaultItem,从而确定新建或更新 FaultItem
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);
}
}
FaultItem 的 startTimeStamp 属性代表此 FaultItem 的可用开始时间,上面我们看到在开启故障延迟机制时,选择完消息队列后会调用 latencyFaultTolerance.isAvailable
方法验证消息队列是否可用,startTimeStamp 属性就是完成这个判断的关键要素:
LatencyFaultToleranceImpl:
public boolean isAvailable(final String name) {
final FaultItem faultItem = this.faultItemTable.get(name);
if (faultItem != null) {
/* 根据 brokerName 获取到 FaultItem 判断是否可用 */
return faultItem.isAvailable();
}
// 如果没有在 FaultItem 映射表中,当然直接返回可用
return true;
}
LatencyFaultToleranceImpl.FaultItem:
public boolean isAvailable() {
// 如果当前时间大于等于 startTimestamp,证明此 FaultItem 已经到了可用时间,返回可用
return (System.currentTimeMillis() - startTimestamp) >= 0;
}
如果在选择消息队列时,所有 FaultItem 映射中的 Broker 都判断不可用,这时该怎么办呢?总不能阻断本次消息发送,所以这时只能强行从所有的 FaultItem 中选择一个 Broker:
LatencyFaultToleranceImpl:
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()) {
// 打乱 FaultItem 顺序,个人理解这里打乱顺序的原因是为了避免多个 FaultItem 的 currentLatency 和 startTimestamp 都相等时会导致每次排序的结果都是一样的
// 这样就可能会出现每次选择的 FaultItem 都是一样的,打乱后会增加 FaultItem 选择的随机性
Collections.shuffle(tmpList);
// 重新排序 FaultItem
Collections.sort(tmpList);
final int half = tmpList.size() / 2;
if (half <= 0) {
// 只有一个则直接这个 FaultItem
return tmpList.get(0).getName();
} else {
// 这个算法的目的是在排序好的 FaultItem 列表中轮询前一半的元素,避免获取到 startTimestamp 较大的 FaultItem
final int i = this.whichItemWorst.getAndIncrement() % half;
return tmpList.get(i).getName();
}
}
return null;
}
这里涉及到 FaultItem 排序,所以我们需要看下 FaultItem 的排序规则:
LatencyFaultToleranceImpl.FaultItem:
public int compareTo(final FaultItem other) {
// 可用的排在前面
if (this.isAvailable() != other.isAvailable()) {
if (this.isAvailable())
return -1;
if (other.isAvailable())
return 1;
}
// currentLatency 小的排在前面
if (this.currentLatency < other.currentLatency)
return -1;
else if (this.currentLatency > other.currentLatency) {
return 1;
}
// startTimestamp 小的排在前面
if (this.startTimestamp < other.startTimestamp)
return -1;
else if (this.startTimestamp > other.startTimestamp) {
return 1;
}
return 0;
}
到这里 RocketMQ 消息发送负载均衡与容错处理了就分析完了。