一、引言

RocketMQ是一款优秀的分布式消息中间件,在各方面的性能都比目前已有的消息队列要好,RocketMQ默认采用长轮询的拉模式, 单机支持千万级别的消息堆积,可以非常好的应用在海量消息系统中。

RocketMQ主要由 Producer、Broker、Consumer、Namesvr 等组件组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,Namesvr负责存储元数据,各组件的主要功能如下:

  • 消息生产者(Producer):负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到Broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。

  • 消息消费者(Consumer):负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。

  • 代理服务器(Broker Server):消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。

  • 名字服务(Name Server):名称服务充当路由消息的提供者。生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。多个Namesrv实例组成集群,但相互独立,没有信息交换。

  • 生产者组(Producer Group):同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。

  • 消费者组(Consumer Group):同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。

RocketMQ整体消息处理逻辑上以Topic维度进行生产消费、物理上会存储到具体的Broker上的某个MessageQueue当中,正因为一个Topic会存在多个Broker节点上的多个MessageQueue,所以自然而然就产生了消息生产消费的负载均衡需求。

本篇文章分析的核心在于介绍RocketMQ的消息生产者(Producer)和消息消费者(Consumer)在整个消息的生产消费过程中如何实现负载均衡以及其中的实现细节。

二、RocketMQ的整体架构

(图片来自于Apache RocketMQ)

RocketMQ架构上主要分为四部分,如上图所示:

  • Producer:消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。

  • Consumer:消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。

  • NameServer:NameServer是一个非常简单的Topic路由注册中心,支持分布式集群方式部署,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。

  • BrokerServer:Broker主要负责消息的存储、投递和查询以及服务高可用保证,支持分布式集群方式部署。

RocketMQ的Topic的物理分布如上图所示:

Topic作为消息生产和消费的逻辑概念,具体的消息存储分布在不同的Broker当中。

Broker中的Queue是Topic对应消息的物理存储单元。

在RocketMQ的整体设计理念当中,消息的生产消费以Topic维度进行,每个Topic会在RocketMQ的集群中的Broker节点创建对应的MessageQueue。

producer生产消息的过程本质上就是选择Topic在Broker的所有的MessageQueue并按照一定的规则选择其中一个进行消息发送,正常情况的策略是轮询。

consumer消费消息的过程本质上就是一个订阅同一个Topic的consumerGroup下的每个consumer按照一定的规则负责Topic下一部分MessageQueue进行消费。

在RocketMQ整个消息的生命周期内,不管是生产消息还是消费消息都会涉及到负载均衡的概念,消息的生成过程中主要涉及到Broker选择的负载均衡,消息的消费过程主要涉及多consumer和多Broker之间的负责均衡。

三、producer消息生产过程

producer消息生产过程:

  • producer首先访问namesvr获取路由信息,namesvr存储Topic维度的所有路由信息(包括每个topic在每个Broker的队列分布情况)。

  • producer解析路由信息生成本地的路由信息,解析Topic在Broker队列信息并转化为本地的消息生产的路由信息。

  • producer根据本地路由信息向Broker发送消息,选择本地路由中具体的Broker进行消息发送。

3.1 路由同步过程

  1. public class MQClientInstance {
  2. public boolean updateTopicRouteInfoFromNameServer(final String topic) {
  3. return updateTopicRouteInfoFromNameServer(topic, false, null);
  4. }
  5. public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
  6. DefaultMQProducer defaultMQProducer) {
  7. try {
  8. if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
  9. try {
  10. TopicRouteData topicRouteData;
  11. if (isDefault && defaultMQProducer != null) {
  12. // 省略对应的代码
  13. } else {
  14. // 1、负责查询指定的Topic对应的路由信息
  15. topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
  16. }
  17. if (topicRouteData != null) {
  18. // 2、比较路由数据topicRouteData是否发生变更
  19. TopicRouteData old = this.topicRouteTable.get(topic);
  20. boolean changed = topicRouteDataIsChange(old, topicRouteData);
  21. if (!changed) {
  22. changed = this.isNeedUpdateTopicRouteInfo(topic);
  23. }
  24. // 3、解析路由信息转化为生产者的路由信息和消费者的路由信息
  25. if (changed) {
  26. TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
  27. for (BrokerData bd : topicRouteData.getBrokerDatas()) {
  28. this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
  29. }
  30. // 生成生产者对应的Topic信息
  31. {
  32. TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
  33. publishInfo.setHaveTopicRouterInfo(true);
  34. Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
  35. while (it.hasNext()) {
  36. Entry<String, MQProducerInner> entry = it.next();
  37. MQProducerInner impl = entry.getValue();
  38. if (impl != null) {
  39. impl.updateTopicPublishInfo(topic, publishInfo);
  40. }
  41. }
  42. }
  43. // 保存到本地生产者路由表当中
  44. this.topicRouteTable.put(topic, cloneTopicRouteData);
  45. return true;
  46. }
  47. }
  48. } finally {
  49. this.lockNamesrv.unlock();
  50. }
  51. } else {
  52. }
  53. } catch (InterruptedException e) {
  54. }
  55. return false;
  56. }
  57. }

路由同步过程

  • 路由同步过程是消息生产者发送消息的前置条件,没有路由的同步就无法感知具体发往那个Broker节点。

  • 路由同步主要负责查询指定的Topic对应的路由信息,比较路由数据topicRouteData是否发生变更,最终解析路由信息转化为生产者的路由信息和消费者的路由信息。

  1. public class TopicRouteData extends RemotingSerializable {
  2. private String orderTopicConf;
  3. // 按照broker维度保存的Queue信息
  4. private List<QueueData> queueDatas;
  5. // 按照broker维度保存的broker信息
  6. private List<BrokerData> brokerDatas;
  7. private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
  8. }
  9. public class QueueData implements Comparable<QueueData> {
  10. // broker的名称
  11. private String brokerName;
  12. // 读队列大小
  13. private int readQueueNums;
  14. // 写队列大小
  15. private int writeQueueNums;
  16. // 读写权限
  17. private int perm;
  18. private int topicSynFlag;
  19. }
  20. public class BrokerData implements Comparable<BrokerData> {
  21. // broker所属集群信息
  22. private String cluster;
  23. // broker的名称
  24. private String brokerName;
  25. // broker对应的ip地址信息
  26. private HashMap<Long/* brokerId */, String/* broker address */> brokerAddrs;
  27. private final Random random = new Random();
  28. }
  29. --------------------------------------------------------------------------------------------------
  30. public class TopicPublishInfo {
  31. private boolean orderTopic = false;
  32. private boolean haveTopicRouterInfo = false;
  33. // 最细粒度的队列信息
  34. private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>();
  35. private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();
  36. private TopicRouteData topicRouteData;
  37. }
  38. public class MessageQueue implements Comparable<MessageQueue>, Serializable {
  39. private static final long serialVersionUID = 6191200464116433425L;
  40. // Topic信息
  41. private String topic;
  42. // 所属的brokerName信息
  43. private String brokerName;
  44. // Topic下的队列信息Id
  45. private int queueId;
  46. }

路由解析过程:

  • TopicRouteData核心变量QueueData保存每个Broker的队列信息,BrokerData保存Broker的地址信息。

  • TopicPublishInfo核心变量MessageQueue保存最细粒度的队列信息。

  • producer负责将从namesvr获取的TopicRouteData转化为producer本地的TopicPublishInfo。

  1. public class MQClientInstance {
  2. public static TopicPublishInfo topicRouteData2TopicPublishInfo(final String topic, final TopicRouteData route) {
  3. TopicPublishInfo info = new TopicPublishInfo();
  4. info.setTopicRouteData(route);
  5. if (route.getOrderTopicConf() != null && route.getOrderTopicConf().length() > 0) {
  6. // 省略相关代码
  7. } else {
  8. List<QueueData> qds = route.getQueueDatas();
  9. // 按照brokerName进行排序
  10. Collections.sort(qds);
  11. // 遍历所有broker生成队列维度信息
  12. for (QueueData qd : qds) {
  13. // 具备写能力的QueueData能够用于队列生成
  14. if (PermName.isWriteable(qd.getPerm())) {
  15. // 遍历获得指定brokerData进行异常条件过滤
  16. BrokerData brokerData = null;
  17. for (BrokerData bd : route.getBrokerDatas()) {
  18. if (bd.getBrokerName().equals(qd.getBrokerName())) {
  19. brokerData = bd;
  20. break;
  21. }
  22. }
  23. if (null == brokerData) {
  24. continue;
  25. }
  26. if (!brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) {
  27. continue;
  28. }
  29. // 遍历QueueData的写队列的数量大小,生成MessageQueue保存指定TopicPublishInfo
  30. for (int i = 0; i < qd.getWriteQueueNums(); i++) {
  31. MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
  32. info.getMessageQueueList().add(mq);
  33. }
  34. }
  35. }
  36. info.setOrderTopic(false);
  37. }
  38. return info;
  39. }
  40. }

路由生成过程:

  • 路由生成过程主要是根据QueueData的BrokerName和writeQueueNums来生成MessageQueue 对象。

  • MessageQueue是消息发送过程中选择的最细粒度的可发送消息的队列。

  1. {
  2. "TBW102": [{
  3. "brokerName": "broker-a",
  4. "perm": 7,
  5. "readQueueNums": 8,
  6. "topicSynFlag": 0,
  7. "writeQueueNums": 8
  8. }, {
  9. "brokerName": "broker-b",
  10. "perm": 7,
  11. "readQueueNums": 8,
  12. "topicSynFlag": 0,
  13. "writeQueueNums": 8
  14. }]
  15. }

路由解析举例:

  • topic(TBW102)在broker-a和broker-b上存在队列信息,其中读写队列个数都为8。

  • 先按照broker-a、broker-b的名字顺序针对broker信息进行排序。

  • 针对broker-a会生成8个topic为TBW102的MessageQueue对象,queueId分别是0-7。

  • 针对broker-b会生成8个topic为TBW102的MessageQueue对象,queueId分别是0-7。

  • topic(名为TBW102)的TopicPublishInfo整体包含16个MessageQueue对象,其中有8个broker-a的MessageQueue,有8个broker-b的MessageQueue。

  • 消息发送过程中的路由选择就是从这16个MessageQueue对象当中获取一个进行消息发送。

3.2 负载均衡过程

  1. public class DefaultMQProducerImpl implements MQProducerInner {
  2. private SendResult sendDefaultImpl(
  3. Message msg,
  4. final CommunicationMode communicationMode,
  5. final SendCallback sendCallback,
  6. final long timeout
  7. ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
  8. // 1、查询消息发送的TopicPublishInfo信息
  9. TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
  10. if (topicPublishInfo != null && topicPublishInfo.ok()) {
  11. String[] brokersSent = new String[timesTotal];
  12. // 根据重试次数进行消息发送
  13. for (; times < timesTotal; times++) {
  14. // 记录上次发送失败的brokerName
  15. String lastBrokerName = null == mq ? null : mq.getBrokerName();
  16. // 2、从TopicPublishInfo获取发送消息的队列
  17. MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
  18. if (mqSelected != null) {
  19. mq = mqSelected;
  20. brokersSent[times] = mq.getBrokerName();
  21. try {
  22. // 3、执行发送并判断发送结果,如果发送失败根据重试次数选择消息队列进行重新发送
  23. sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
  24. switch (communicationMode) {
  25. case SYNC:
  26. if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
  27. if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
  28. continue;
  29. }
  30. }
  31. return sendResult;
  32. default:
  33. break;
  34. }
  35. } catch (MQBrokerException e) {
  36. // 省略相关代码
  37. } catch (InterruptedException e) {
  38. // 省略相关代码
  39. }
  40. } else {
  41. break;
  42. }
  43. }
  44. if (sendResult != null) {
  45. return sendResult;
  46. }
  47. }
  48. }
  49. }

消息发送过程:

  • 查询Topic对应的路由信息对象TopicPublishInfo。

  • 从TopicPublishInfo中通过selectOneMessageQueue获取发送消息的队列,该队列代表具体落到具体的Broker的queue队列当中。

  • 执行发送并判断发送结果,如果发送失败根据重试次数选择消息队列进行重新发送,重新选择队列会避开上一次发送失败的Broker的队列。

  1. public class TopicPublishInfo {
  2. public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
  3. if (lastBrokerName == null) {
  4. return selectOneMessageQueue();
  5. } else {
  6. // 按照轮询进行选择发送的MessageQueue
  7. for (int i = 0; i < this.messageQueueList.size(); i++) {
  8. int index = this.sendWhichQueue.getAndIncrement();
  9. int pos = Math.abs(index) % this.messageQueueList.size();
  10. if (pos < 0)
  11. pos = 0;
  12. MessageQueue mq = this.messageQueueList.get(pos);
  13. // 避开上一次上一次发送失败的MessageQueue
  14. if (!mq.getBrokerName().equals(lastBrokerName)) {
  15. return mq;
  16. }
  17. }
  18. return selectOneMessageQueue();
  19. }
  20. }
  21. }

路由选择过程:

  • MessageQueue的选择按照轮询进行选择,通过全局维护索引进行累加取模选择发送队列。

  • MessageQueue的选择过程中会避开上一次发送失败Broker对应的MessageQueue。

Producer消息发送示意图

  • 某Topic的队列分布为Broker_A_Queue1、Broker_A_Queue2、Broker_B_Queue1、Broker_B_Queue2、Broker_C_Queue1、Broker_C_Queue2,根据轮询策略依次进行选择。

  • 发送失败的场景下如Broker_A_Queue1发送失败那么就会跳过Broker_A选择Broker_B_Queue1进行发送。

四、consumer消息消费过程

consumer消息消费过程

  • consumer访问namesvr同步topic对应的路由信息。

  • consumer在本地解析远程路由信息并保存到本地。

  • consumer在本地进行Reblance负载均衡确定本节点负责消费的MessageQueue。

  • consumer访问Broker消费指定的MessageQueue的消息。

4.1 路由同步过程

  1. public class MQClientInstance {
  2. // 1、启动定时任务从namesvr定时同步路由信息
  3. private void startScheduledTask() {
  4. this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
  5. @Override
  6. public void run() {
  7. try {
  8. MQClientInstance.this.updateTopicRouteInfoFromNameServer();
  9. } catch (Exception e) {
  10. log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
  11. }
  12. }
  13. }, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);
  14. }
  15. public void updateTopicRouteInfoFromNameServer() {
  16. Set<String> topicList = new HashSet<String>();
  17. // 遍历所有的consumer订阅的Topic并从namesvr获取路由信息
  18. {
  19. Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
  20. while (it.hasNext()) {
  21. Entry<String, MQConsumerInner> entry = it.next();
  22. MQConsumerInner impl = entry.getValue();
  23. if (impl != null) {
  24. Set<SubscriptionData> subList = impl.subscriptions();
  25. if (subList != null) {
  26. for (SubscriptionData subData : subList) {
  27. topicList.add(subData.getTopic());
  28. }
  29. }
  30. }
  31. }
  32. }
  33. for (String topic : topicList) {
  34. this.updateTopicRouteInfoFromNameServer(topic);
  35. }
  36. }
  37. public boolean updateTopicRouteInfoFromNameServer(final String topic, boolean isDefault,
  38. DefaultMQProducer defaultMQProducer) {
  39. try {
  40. if (this.lockNamesrv.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
  41. try {
  42. TopicRouteData topicRouteData;
  43. if (isDefault && defaultMQProducer != null) {
  44. // 省略代码
  45. } else {
  46. topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
  47. }
  48. if (topicRouteData != null) {
  49. TopicRouteData old = this.topicRouteTable.get(topic);
  50. boolean changed = topicRouteDataIsChange(old, topicRouteData);
  51. if (!changed) {
  52. changed = this.isNeedUpdateTopicRouteInfo(topic);
  53. }
  54. if (changed) {
  55. TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
  56. for (BrokerData bd : topicRouteData.getBrokerDatas()) {
  57. this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
  58. }
  59. // 构建consumer侧的路由信息
  60. {
  61. Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
  62. Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
  63. while (it.hasNext()) {
  64. Entry<String, MQConsumerInner> entry = it.next();
  65. MQConsumerInner impl = entry.getValue();
  66. if (impl != null) {
  67. impl.updateTopicSubscribeInfo(topic, subscribeInfo);
  68. }
  69. }
  70. }
  71. this.topicRouteTable.put(topic, cloneTopicRouteData);
  72. return true;
  73. }
  74. }
  75. } finally {
  76. this.lockNamesrv.unlock();
  77. }
  78. }
  79. } catch (InterruptedException e) {
  80. }
  81. return false;
  82. }
  83. }

路由同步过程

  • 路由同步过程是消息消费者消费消息的前置条件,没有路由的同步就无法感知具体待消费的消息的Broker节点。

  • consumer节点通过定时任务定期从namesvr同步该消费节点订阅的topic的路由信息。

  • consumer通过updateTopicSubscribeInfo将同步的路由信息构建成本地的路由信息并用以后续的负责均衡。

4.2 负载均衡过程

  1. public class RebalanceService extends ServiceThread {
  2. private static long waitInterval =
  3. Long.parseLong(System.getProperty(
  4. "rocketmq.client.rebalance.waitInterval", "20000"));
  5. private final MQClientInstance mqClientFactory;
  6. public RebalanceService(MQClientInstance mqClientFactory) {
  7. this.mqClientFactory = mqClientFactory;
  8. }
  9. @Override
  10. public void run() {
  11. while (!this.isStopped()) {
  12. this.waitForRunning(waitInterval);
  13. this.mqClientFactory.doRebalance();
  14. }
  15. }
  16. }

负载均衡过程

  • consumer通过RebalanceService来定期进行重新负载均衡。

  • RebalanceService的核心在于完成MessageQueue和consumer的分配关系。

  1. public abstract class RebalanceImpl {
  2. private void rebalanceByTopic(final String topic, final boolean isOrder) {
  3. switch (messageModel) {
  4. case BROADCASTING: {
  5. // 省略相关代码
  6. break;
  7. }
  8. case CLUSTERING: { // 集群模式下的负载均衡
  9. // 1、获取topic下所有的MessageQueue
  10. Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
  11. // 2、获取topic下该consumerGroup下所有的consumer对象
  12. List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
  13. // 3、开始重新分配进行rebalance
  14. if (mqSet != null && cidAll != null) {
  15. List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
  16. mqAll.addAll(mqSet);
  17. Collections.sort(mqAll);
  18. Collections.sort(cidAll);
  19. AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
  20. List<MessageQueue> allocateResult = null;
  21. try {
  22. // 4、通过分配策略重新进行分配
  23. allocateResult = strategy.allocate(
  24. this.consumerGroup,
  25. this.mQClientFactory.getClientId(),
  26. mqAll,
  27. cidAll);
  28. } catch (Throwable e) {
  29. return;
  30. }
  31. Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
  32. if (allocateResult != null) {
  33. allocateResultSet.addAll(allocateResult);
  34. }
  35. // 5、根据分配结果执行真正的rebalance动作
  36. boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
  37. if (changed) {
  38. this.messageQueueChanged(topic, mqSet, allocateResultSet);
  39. }
  40. }
  41. break;
  42. }
  43. default:
  44. break;
  45. }
  46. }

重新分配流程

  • 获取topic下所有的MessageQueue。

  • 获取topic下该consumerGroup下所有的consumer的cid(如192.168.0.8@15958)。

  • 针对mqAll和cidAll进行排序,mqAll排序顺序按照先BrokerName后BrokerId,cidAll排序按照字符串排序。

  • 通过分配策略

  • AllocateMessageQueueStrategy重新分配。

  • 根据分配结果执行真正的rebalance动作。

  1. public class AllocateMessageQueueAveragely implements AllocateMessageQueueStrategy {
  2. private final InternalLogger log = ClientLogger.getLog();
  3. @Override
  4. public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
  5. List<String> cidAll) {
  6. List<MessageQueue> result = new ArrayList<MessageQueue>();
  7. // 核心逻辑计算开始
  8. // 计算当前cid的下标
  9. int index = cidAll.indexOf(currentCID);
  10. // 计算多余的模值
  11. int mod = mqAll.size() % cidAll.size();
  12. // 计算平均大小
  13. int averageSize =
  14. mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
  15. + 1 : mqAll.size() / cidAll.size());
  16. // 计算起始下标
  17. int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
  18. // 计算范围大小
  19. int range = Math.min(averageSize, mqAll.size() - startIndex);
  20. // 组装结果
  21. for (int i = 0; i < range; i++) {
  22. result.add(mqAll.get((startIndex + i) % mqAll.size()));
  23. }
  24. return result;
  25. }
  26. // 核心逻辑计算结束
  27. @Override
  28. public String getName() {
  29. return "AVG";
  30. }
  31. }
  32. ------------------------------------------------------------------------------------
  33. rocketMq的集群存在3broker,分别是broker_abroker_bbroker_c
  34. rocketMq上存在名为topic_demotopicwriteQueue写队列数量为3,分布在3broker
  35. 排序后的mqAll的大小为9,依次为
  36. [broker_a_0 broker_a_1 broker_a_2 broker_b_0 broker_b_1 broker_b_2 broker_c_0 broker_c_1 broker_c_2]
  37. rocketMq存在包含4consumerconsumer_group,排序后cidAll依次为
  38. [192.168.0.6@15956 192.168.0.7@15957 192.168.0.8@15958 192.168.0.9@15959]
  39. 192.168.0.6@15956 的分配MessageQueue结算过程
  40. index0
  41. mod9%4=1
  42. averageSize9 / 4 + 1 = 3
  43. startIndex0
  44. range3
  45. messageQueue:[broker_a_0broker_a_1broker_a_2]
  46. 192.168.0.6@15957 的分配MessageQueue结算过程
  47. index1
  48. mod9%4=1
  49. averageSize9 / 4 = 2
  50. startIndex3
  51. range2
  52. messageQueue:[broker_b_0broker_b_1]
  53. 192.168.0.6@15958 的分配MessageQueue结算过程
  54. index2
  55. mod9%4=1
  56. averageSize9 / 4 = 2
  57. startIndex5
  58. range2
  59. messageQueue:[broker_b_2broker_c_0]
  60. 192.168.0.6@15959 的分配MessageQueue结算过程
  61. index3
  62. mod9%4=1
  63. averageSize9 / 4 = 2
  64. startIndex7
  65. range2
  66. messageQueue:[broker_c_1broker_c_2]

分配策略分析:

  • 整体分配策略可以参考上图的具体例子,可以更好的理解分配的逻辑。

consumer的分配

  • 同一个consumerGroup下的consumer对象会分配到同一个Topic下不同的MessageQueue。

  • 每个MessageQueue最终会分配到具体的consumer当中。

五、RocketMQ指定机器消费设计思路

日常测试环境当中会存在多台consumer进行消费,但实际开发当中某台consumer新上了功能后希望消息只由该机器进行消费进行逻辑覆盖,这个时候consumerGroup的集群模式就会给我们造成困扰,因为消费负载均衡的原因不确定消息具体由那台consumer进行消费。当然我们可以通过介入consumer的负载均衡机制来实现指定机器消费。

  1. public class AllocateMessageQueueAveragely implements AllocateMessageQueueStrategy {
  2. private final InternalLogger log = ClientLogger.getLog();
  3. @Override
  4. public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
  5. List<String> cidAll) {
  6. List<MessageQueue> result = new ArrayList<MessageQueue>();
  7. // 通过改写这部分逻辑,增加判断是否是指定IP的机器,如果不是直接返回空列表表示该机器不负责消费
  8. if (!cidAll.contains(currentCID)) {
  9. return result;
  10. }
  11. int index = cidAll.indexOf(currentCID);
  12. int mod = mqAll.size() % cidAll.size();
  13. int averageSize =
  14. mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
  15. + 1 : mqAll.size() / cidAll.size());
  16. int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
  17. int range = Math.min(averageSize, mqAll.size() - startIndex);
  18. for (int i = 0; i < range; i++) {
  19. result.add(mqAll.get((startIndex + i) % mqAll.size()));
  20. }
  21. return result;
  22. }
  23. }

consumer负载均衡策略改写

  • 通过改写负载均衡策略AllocateMessageQueueAveragely的allocate机制保证只有指定IP的机器能够进行消费。

  • 通过IP进行判断是基于RocketMQ的cid格式是192.168.0.6@15956,其中前面的IP地址就是对于的消费机器的ip地址,整个方案可行且可以实际落地。

六、小结

本文主要介绍了RocketMQ在生产和消费过程中的负载均衡机制,结合源码和实际案例力求给读者一个易于理解的技术普及,希望能对读者有参考和借鉴价值。囿于文章篇幅,有些方面未涉及,也有很多技术细节未详细阐述,如有疑问欢迎继续交流。

作者:vivo互联网服务器团队-Wang Zhi

深入剖析 RocketMQ 源码 - 负载均衡机制的更多相关文章

  1. 深入剖析RocketMQ源码-NameServer

    一.RocketMQ架构简介 1.1 逻辑部署图 (图片来自网络) 1.2 核心组件说明 通过上图可以看到,RocketMQ的核心组件主要包括4个,分别是NameServer.Broker.Produ ...

  2. 深入剖析 RocketMQ 源码 - 消息存储模块

    一.简介 RocketMQ 是阿里巴巴开源的分布式消息中间件,它借鉴了 Kafka 实现,支持消息订阅与发布.顺序消息.事务消息.定时消息.消息回溯.死信队列等功能.RocketMQ 架构上主要分为四 ...

  3. RocketMQ源码 — 六、 RocketMQ高可用(1)

    高可用究竟指的是什么?请参考:关于高可用的系统 RocketMQ做了以下的事情来保证系统的高可用 多master部署,防止单点故障 消息冗余(主从结构),防止消息丢失 故障恢复(本篇暂不讨论) 那么问 ...

  4. RocketMQ源码分析之从官方示例窥探:RocketMQ事务消息实现基本思想

    摘要: RocketMQ源码分析之从官方示例窥探RocketMQ事务消息实现基本思想. 在阅读本文前,若您对RocketMQ技术感兴趣,请加入RocketMQ技术交流群 RocketMQ4.3.0版本 ...

  5. RocketMQ源码详解 | Consumer篇 · 其一:消息的 Pull 和 Push

    概述 当消息被存储后,消费者就会将其消费. 这句话简要的概述了一条消息的最总去向,也引出了本文将讨论的问题: 消息什么时候才对被消费者可见? 是在 page cache 中吗?还是在落盘后?还是像 K ...

  6. Azure的负载均衡机制

    负载均衡一直是一个比较重要的议题,几乎所有的Azure案例或者场景都不可避免,鉴于经常有客户会问,所以笔者觉得有必要总结一下. Azure提供的负载均衡机制,按照功能,可以分为三种:Azure Loa ...

  7. HDFS 02 - HDFS 的机制:副本机制、机架感知机制、负载均衡机制

    目录 1 - HDFS 的副本机制 2 - HDFS 的机架感知机制 3 - HDFS 的负载均衡机制 参考资料 版权声明 1 - HDFS 的副本机制 HDFS 中的文件,在物理上都是以分块(blo ...

  8. 【RocketMQ源码分析】深入消息存储(1)

    最近在学习RocketMQ相关的东西,在学习之余沉淀几篇笔记. RocketMQ有很多值得关注的设计点,消息发送.消息消费.路由中心NameServer.消息过滤.消息存储.主从同步.事务消息等等. ...

  9. RocketMQ源码详解 | Broker篇 · 其一:线程模型与接收链路

    概述 在上一节 RocketMQ源码详解 | Producer篇 · 其二:消息组成.发送链路 中,我们终于将消息发送出了 Producer,在短暂的 tcp 握手后,很快它就会进入目的 Broker ...

随机推荐

  1. SP3734题解

    题意: 有 \(n\) 列表格,第 \(i\) 列有 \(a_i\) 个格子,问在 \(n\) 列表格中有多少种放置 \(k\) 个棋子的方法使没有棋子在同一列和同一行.(如果中间有一个"格 ...

  2. DHCP协议简析

    推荐这篇文章,原理及抓包都分析的很好: **推荐这篇文章,原理及抓包都分析的很好:** https://blog.csdn.net/andy_93/article/details/78238931 简 ...

  3. 5月7日 python学习总结 MySQL数据库(一)

    一.数据库介绍 1.数据库相关概念 数据库服务器(本质就是一台计算机,该计算机之上安装有数据库管理软件的服务端) 数据库管理系统RDBMS(本质就是一个C/S机构的套接字软件) 库(文件夹)===&g ...

  4. javaweb项目中关于配置文件web.xml的解析

    一..启动tomcat,加载项目中的web.xml文件,创建servercontext上下文对象. 可以通过servercontext对象在应用中获取web.xml文件中的值. web应用加载的顺序与 ...

  5. metinfo 6.0 任意文件读取漏洞

    一. 启动环境 1.双击运行桌面phpstudy.exe软件 2.点击启动按钮,启动服务器环境 二.代码审计 1.双击启动桌面Seay源代码审计系统软件 2.点击新建项目按钮,弹出对画框中选择(C:\ ...

  6. linux的一些sao东西

    1.sys命令的目录 /usr/include/asm-generic

  7. class文件和java文件区别

  8. 使用Spring框架的好处是什么?

    轻量:Spring 是轻量的,基本的版本大约2MB. 控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们. 面向切面的编程(AOP):Spring支持 ...

  9. Java如何声明一个数组?JS如何声明一个数组?如何获取数组长度

    1 Long[] numbers; //一般使用的定义方式,可分为静态和动态两种定义方式,下有说明. 2 Long numbers[]; //跟上面用法一致. 3 Long... numbers; / ...

  10. memcached 是如何做身份验证的?

    没有身份认证机制!memcached 是运行在应用下层的软件(身份验证应该是应用 上层的职责).memcached 的客户端和服务器端之所以是轻量级的,部分原因就 是完全没有实现身份验证机制.这样,m ...