RocketMQ消息拉取-负载均衡算法

122 阅读3分钟

RocketMQ消息拉取-负载均衡算法

负载均衡

默认-平衡hash队列算法

Average Hashing queue algorithm

org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely

/**
 * Allocating by consumer id
 *
 * @param consumerGroup current consumer group
 * @param currentCID current consumer id
 * @param mqAll message queue set in current topic
 * @param cidAll consumer set in current consumer group
 * @return The allocate result of given strategy
 */
public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
    List<String> cidAll) {

    List<MessageQueue> result = new ArrayList<MessageQueue>();
    if (!check(consumerGroup, currentCID, mqAll, cidAll)) {
        return result;
    }

    // 当前消费者在消费者组数组坐标, 如5个队列 % 3个消费者,当前第3个(2),当前第2个(1),当前第一个(0)
    int index = cidAll.indexOf(currentCID);
    // 当前MQ主题的消息队列多少 % 消费者的多少,如5个队列 % 3个消费者 = 2
    int mod = mqAll.size() % cidAll.size();
    // 平均一个消费者要消费队列的大小
    // 队列数比消费者数少,则平均大小就是1,有些消费者会分配不到
    // 队列数比消费者数多
    //    当前消费者整除,是除数,如5个队列 % 3个消费者,当前第3个(3/3 = 1);
    //    当前消费者不整除,是除数+1,如5个队列 % 3个消费者,当前第2个(5/3 + 1 = 2), 第1个(5/3 + 1 = 2)
    int averageSize =
        mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
            + 1 : mqAll.size() / cidAll.size());
    // 当前消费者的mq坐标, 当前消费者不整除
    //    下标小于余数,当前消费坐标 * 平均大小,如5个队列,3个消费者,当前第2个(1 * 2  = 2),当前第1个(0 * 2 = 0)
    //    下标大于等于余数,当前消费者整除,是当前消费坐标 * 平均大小,如5个队列,3个消费者,当前第3个(2 * 1 + 2 = 4),
    int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
    // 环大小 = min(平均大小,队列大小-开始坐标),如5个队列,3个消费者,当前第3个(min(1,5-4) = 1), 当前第2个(min(2,5-2)=2,当前第1个(min(2,5-0) = 2)
    int range = Math.min(averageSize, mqAll.size() - startIndex);
    for (int i = 0; i < range; i++) {
        // 当前第3个(4),当前第2个(2,3),当前第1个(0,1)
        result.add(mqAll.get((startIndex + i) % mqAll.size()));
    }
    return result;
}

核心思想

1.通过取模算出每个消费者负责的队列数量;

2.通过当前在消费者组的坐标,和余数大小比较,比余数小的坐标,按照平均大小倍数连续平分,剩余部分就是大于余数部分的坐标

image.png

循环平衡hash队列算法

org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragelyByCircle

public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll, List<String> cidAll) {

    List<MessageQueue> result = new ArrayList<MessageQueue>();
    if (!check(consumerGroup, currentCID, mqAll, cidAll)) {
        return result;
    }

    // 消费者在消息队列,当前坐标, 3个消费者0,1,2
    int index = cidAll.indexOf(currentCID);
    for (int i = index; i < mqAll.size(); i++) {
        // Mq队列 / 消费者组 的余数 == 当前消费者 / 消费者组 的余数,就是匹配关系
        if (i % cidAll.size() == index) {
            result.add(mqAll.get(i));
        }
    }
    return result;
}

核心思想

  1. 计算消费者在消费者组的坐标
  2. 遍历队列,队列 / 消费者组 的余数 == 消费者 / 消费者组 的余数,就是匹配关系

image.png

一致性hash分配算法

org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueConsistentHash

public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
    List<String> cidAll) {

    List<MessageQueue> result = new ArrayList<MessageQueue>();
    if (!check(consumerGroup, currentCID, mqAll, cidAll)) {
        return result;
    }

    // 将所有消费者维护成物理节点列表
    Collection<ClientNode> cidNodes = new ArrayList<ClientNode>();
    for (String cid : cidAll) {
        cidNodes.add(new ClientNode(cid));
    }

    // 将所有物理节点同虚拟节点个数维护成一致性hash环
    final ConsistentHashRouter<ClientNode> router; //for building hash ring
    if (customHashFunction != null) {
        router = new ConsistentHashRouter<ClientNode>(cidNodes, virtualNodeCnt, customHashFunction);
    } else {
        router = new ConsistentHashRouter<ClientNode>(cidNodes, virtualNodeCnt);
    }

    // 遍历队列, 获取相应的虚拟节点的物理节点
    List<MessageQueue> results = new ArrayList<MessageQueue>();
    for (MessageQueue mq : mqAll) {
        ClientNode clientNode = router.routeNode(mq.toString());
        // 物理节点匹配就添加
        if (clientNode != null && currentCID.equals(clientNode.getKey())) {
            results.add(mq);
        }
    }

    return results;

}

一致性hash表核心实现

org.apache.rocketmq.common.consistenthash.ConsistentHashRouter#addNode

// 虚拟节点环
private final SortedMap<Long, VirtualNode<T>> ring = new TreeMap<Long, VirtualNode<T>>();
    
// 增加节点
public void addNode(T pNode, int vNodeCount) {
    if (vNodeCount < 0)
        throw new IllegalArgumentException("illegal virtual node counts :" + vNodeCount);
    
    // 获取已存在的节点备份数量
    int existingReplicas = getExistingReplicas(pNode);
   
    // 塞入红黑树
    for (int i = 0; i < vNodeCount; i++) {
        VirtualNode<T> vNode = new VirtualNode<T>(pNode, i + existingReplicas);
        ring.put(hashFunction.hash(vNode.getKey()), vNode);
    }
}

org.apache.rocketmq.common.consistenthash.ConsistentHashRouter#getExistingReplicas

// 获取已存在的节点备份数量
public int getExistingReplicas(T pNode) {
    int replicas = 0;
    for (VirtualNode<T> vNode : ring.values()) {
        if (vNode.isVirtualNodeOf(pNode)) {
            replicas++;
        }
    }
    return replicas;
}

org.apache.rocketmq.common.consistenthash.ConsistentHashRouter#routeNode

// 路由节点
public T routeNode(String objectKey) {
    if (ring.isEmpty()) {
        return null;
    }
    Long hashVal = hashFunction.hash(objectKey);
    SortedMap<Long, VirtualNode<T>> tailMap = ring.tailMap(hashVal);
    Long nodeHashVal = !tailMap.isEmpty() ? tailMap.firstKey() : ring.firstKey();
    return ring.get(nodeHashVal).getPhysicalNode();
}

核心思想

1.消费者包装为虚拟节点插入hash环后

2.将队列的hash值进行环路由,找到最近的消费者虚拟节点,就是相应的对应关系

image.png

机房分配hash队列算法

org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueByMachineRoom

Computer room Hashing queue algorithm, such as Alipay logic room

public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
    List<String> cidAll) {

    List<MessageQueue> result = new ArrayList<MessageQueue>();
    if (!check(consumerGroup, currentCID, mqAll, cidAll)) {
        return result;
    }
    int currentIndex = cidAll.indexOf(currentCID);
    if (currentIndex < 0) {
        return result;
    }
    List<MessageQueue> premqAll = new ArrayList<MessageQueue>();
    for (MessageQueue mq : mqAll) {
        String[] temp = mq.getBrokerName().split("@");
        if (temp.length == 2 && consumeridcs.contains(temp[0])) {
            premqAll.add(mq);
        }
    }

    int mod = premqAll.size() / cidAll.size();
    int rem = premqAll.size() % cidAll.size();
    int startIndex = mod * currentIndex;
    int endIndex = startIndex + mod;
    for (int i = startIndex; i < endIndex; i++) {
        result.add(premqAll.get(i));
    }
    if (rem > currentIndex) {
        result.add(premqAll.get(currentIndex + mod * cidAll.size()));
    }
    return result;
}

参考

jishuin.proginn.com/p/763bfbd79…