开发者说:RocketMQ 消息发送的高可用设计

640?wx_fmt=jpeg

从上周开始,我们推出了「开发者说」的专栏,发布来自社区开发者自己写的文章。投稿可以是已经发表过的,也可以是未发表过,一经采用,我们将提供纪念品作为感谢,投稿请联系 zjjxg2018(微信号)。


本文作者张乘辉,GitHub ID @objcoding,Java开发工程师, 钟情Java,热爱技术,「Java科代表」公众号作者。


smiley_30.png- 正文开始 -smiley_30.png


从 RocketMQ topic 的创建机制可知,一个 topic 对应有多个消息队列,那么我们在发送消息时,是如何选择消息队列进行发送的?假如这时有 broker 宕机了,RocketMQ 是如何规避故障 broker 的?看完这篇文章,相信你会从文中找到答案。

RocketMQ 在发送消息时,由于 nameserver 检测 broker 是否存活时是有延迟的,在选择消息队列时难免会遇到已经宕机的 broker,又或者因为网络原因发送失败的,因此 RocketMQ 采取了一些高可用设计的方案,主要通过两个手段:重试与 broker 规避。

重试机制


直接定位到 client 端发送消息的方法:

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl:

1int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;	
1for (; times < timesTotal; times++) {	
2    // ...	
3}

在 client 端,发送消息的方式有:同步(SYNC)、异步(ASYNC)、单向(ONEWAY)。

那么可以知道,retryTimesWhenSendFailed 决定同步方法重试次数,默认重试次数为 3 次。

重试机制提高了消息发送的成功率。

选择队列的方式


我在这里提出一个问题:

现在有个由两个 broker 节点组成的集群,有 topic1,默认在每个 broker 上创建 4 个队列,分别是:master-a(q0,q1,q2,q3)、master-b(q0,q1,q2,q3),上一次发送消息到 master-a 的 q0 队列,此时 master-a 宕机了,如果继续发送 topic1 消息,RocketMQ 如果避免再次发送到 master-a?

以上问题引出了 RocketMQ 发送消息时如何选择队列的一些机制,选择队列有两种方式,通过 sendLatencyFaultEnable 的值来控制,默认值为 false,不启动 broker 故障延迟机制,值为 true 时启用 broker 故障延迟机制。

1. 默认机制

sendLatencyFaultEnable=false,消息发送选择队列调用以下方法:

org.apache.rocketmq.client.impl.producer.TopicPublishInfo#selectOneMessageQueue:

 1public MessageQueue selectOneMessageQueue(final String lastBrokerName) {	
 2  if (lastBrokerName == null) {	
 3    return selectOneMessageQueue();	
 4  } else {	
 5    int index = this.sendWhichQueue.getAndIncrement();	
 6    for (int i = 0; i < this.messageQueueList.size(); i++) {	
 7      int pos = Math.abs(index++) % this.messageQueueList.size();	
 8      if (pos < 0)	
 9        pos = 0;	
10      MessageQueue mq = this.messageQueueList.get(pos);	
11      if (!mq.getBrokerName().equals(lastBrokerName)) {	
12        return mq;	
13      }	
14    }	
15    return selectOneMessageQueue();	
16  }	
17}

这里的 lastBrokerName 指的是上一次执行消息发送时选择失败的 broker,在重试机制下,第一次执行消息发送时,lastBrokerName=null,直接选择以下方法:

org.apache.rocketmq.client.impl.producer.TopicPublishInfo#selectOneMessageQueue:

1public MessageQueue selectOneMessageQueue() {	
2  int index = this.sendWhichQueue.getAndIncrement();	
3  int pos = Math.abs(index) % this.messageQueueList.size();	
4  if (pos < 0)	
5    pos = 0;	
6  return this.messageQueueList.get(pos);	
7}

sendWhichQueue 是一个利用 ThreadLocal 本地线程存储自增值的一个类,自增值第一次使用 Random 类随机取值,此后如果消息发送出发重试机制,那么每次自增取值。

此方法直接用 sendWhichQueue 自增获取值,再与消息队列的长度进行取模运算,取模目的是为了循环选择消息队列。

如果此时选择的队列发送消息失败了,此时重试机制在再次选择队列时 lastBrokerName 不为空,回到最开始的那个方法,还是利用 sendWhichQueue 自增获取值,但这里多了一个步骤,与消息队列的长度进行取模运算后,如果此时选择的队列所在的 broker 还是上一次选择失败的 broker,则继续选择下一个队列。

我们再细想一下,如果此时有 broker 宕机了,在默认机制下很可能下一次选择的队列还是在已经宕机的 broker,没有办法规避故障的 broker,因此消息发送很可能会再次失败,重试发送造成了不必要的性能损失。

所以 RocketMQ 采用 Broker 故障延迟机制来规避故障的 broker。

2. Broker故障延迟机制

sendLatencyFaultEnable=true,消息发送选择队列调用以下方法:

org.apache.rocketmq.client.latency.MQFaultStrategy#selectOneMessageQueue:

 1public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {	
 2  // Broker故障延迟机制	
 3  if (this.sendLatencyFaultEnable) {	
 4    try {	
 5      // 自增取值	
 6      int index = tpInfo.getSendWhichQueue().getAndIncrement();	
 7      for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {	
 8        // 队列位置值取模	
 9        int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();	
10        if (pos < 0)	
11          pos = 0;	
12        MessageQueue mq = tpInfo.getMessageQueueList().get(pos);	
13        // 校验队列是否可用	
14        if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {	
15          if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))	
16            return mq;	
17        }	
18      }	
19	
20      // 尝试从失败的broker列表中选择一个可用的broker	
21      final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();	
22      int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);	
23      if (writeQueueNums > 0) {	
24        final MessageQueue mq = tpInfo.selectOneMessageQueue();	
25        if (notBestBroker != null) {	
26          mq.setBrokerName(notBestBroker);	
27          mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);	
28        }	
29        return mq;	
30      } else {	
31        // 从失败条目中移除已经恢复的broker	
32        latencyFaultTolerance.remove(notBestBroker);	
33      }	
34    } catch (Exception e) {	
35      log.error("Error occurred when selecting message queue", e);	
36    }	
37	
38    return tpInfo.selectOneMessageQueue();	
39  }	
40	
41  // 默认机制	
42  return tpInfo.selectOneMessageQueue(lastBrokerName);	
43}

该方法利用 sendWhichQueue 的自增取值的方式轮询选择队列,与默认机制一致,不同的是多了判断是否可用,调用了:

latencyFaultTolerance.isAvailable(mq.getBrokerName());

来判断,其中肯定内涵机关,所以我们需要从延迟机制的几个核心类找突破口。

下面我会从源码的角度详细地分析 rocketmq 是如何实现在一定时间内规避故障 broker 的,从发送消息方法源码看出,在发送完消息,会调用 updateFaultItem 方法:

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl:

1// 3.执行真正的消息发送	
2sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);	
3endTimestamp = System.currentTimeMillis();	
4this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);

发送消息时捕捉到异常同样会调用 updateFaultItem 方法,它是延迟机制的核心方法:

1endTimestamp = System.currentTimeMillis();	
2this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);

其中 endTimestamp - beginTimestampPrev 等于消息发送需要用到的时间,如果成功发送第三个参数传的是 false,发送失败传 true,下面继续看 updateFaultItem 方法的实现源码:

org.apache.rocketmq.client.latency.MQFaultStrategy#updateFaultItem:

1public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {	
2  if (this.sendLatencyFaultEnable) {	
3    long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);	
4    this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);	
5  }	
6}	
1private long computeNotAvailableDuration(final long currentLatency) {	
2  for (int i = latencyMax.length - 1; i >= 0; i--) {	
3    if (currentLatency >= latencyMax[i])	
4      return this.notAvailableDuration[i];	
5  }	
6  return 0;	
7}

其中参数 currentLatency 为本次消息发送的延迟时间,isolation 表示 broker 是否需要规避,所以消息成功发送表示 broker 无需规避,消息发送失败时表示 broker 发生故障了需要规避。

latencyMax 和 notAvailableDuration 是延迟机制算法的核心值,每次发送消息的延迟,它们也决定了失败条目中的 startTimestamp 的值。

从方法可看出,如果 broker 需要隔离,消息发送延迟时间默认为 30s,再利用这个时间从 latencyMax 尾部向前找到比 currentLatency 小的数组下标 index,如果没有找到就返回 0,我们看看 latencyMax 和 notAvailableDuration 这两个数组的默认值:

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

可看出,如果 isolation=true,该 broker 会得到一个 10 分钟规避时长,如果 isolation=false,那么规避时长就得看消息发送的延迟时间是多少了,我们继续往下撸:

org.apache.rocketmq.client.latency.LatencyFaultToleranceImpl#updateFaultItem:

 1public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {	
 2  // 从缓存中获取失败条目	
 3  FaultItem old = this.faultItemTable.get(name);	
 4  if (null == old) {	
 5    // 如果缓存不存在,新建失败条目	
 6    final FaultItem faultItem = new FaultItem(name);	
 7    faultItem.setCurrentLatency(currentLatency);	
 8    // broker开始可用时间=当前时间+规避时长	
 9    faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);	
10	
11    old = this.faultItemTable.putIfAbsent(name, faultItem);	
12    if (old != null) {	
13      // 更新旧的失败条目	
14      old.setCurrentLatency(currentLatency);	
15      old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);	
16    }	
17  } else {	
18    // 更新旧的失败条目	
19    old.setCurrentLatency(currentLatency);	
20    old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);	
21  }	
22}

FaultItem 为存储故障 broker 的类,称为失败条目,每个条目存储了 broker 的名称、消息发送延迟时长、故障规避开始时间。

该方法主要是对失败条目的一些更新操作,如果失败条目已存在,那么更新失败条目,如果失败条目不存在,那么新建失败条目,其中失败条目的 startTimestamp 为当前系统时间加上规避时长,startTimestamp 是判断 broker 是否可用的时间值:

org.apache.rocketmq.client.latency.LatencyFaultToleranceImpl.FaultItem#isAvailable:

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

如果当前系统时间大于故障规避开始时间,说明 broker 可以继续加入轮询的队伍里了。

写在最后


经过一波源码的分析,我相信你已经找到了你想要的答案,这个故障延迟机制真的是一个很好的设计,我们看源码不仅仅要看爽,爽了之后我们还要思考一下这些优秀的设计思想在平时写代码的过程中是否可以借鉴一下?


本文作者:

张乘辉

GitHub ID @objcoding,Java开发工程师, 钟情Java,热爱技术,「Java科代表」公众号作者。


文章缩略图

Photo by Jon Tyson on Unsplash


/ RocketMQ x 金融 /


640?wx_fmt=jpeg


©每周一推

第一时间获得下期分享


640?wx_fmt=gif

Tips:

# 点下“看”❤️

# 然后,公众号对话框内发送“电子秤”,试试手气??

# 本期奖品是来自淘宝心选的厨房电子秤

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值