Several load balancing methods in Dubbo

Posted by Fly on Sun, 21 Jul 2019 12:30:39 +0200

AbstractLoadBalance

In Dubbo, all load balancing implementation classes inherit from AbstractLoadBalance, which implements the LoadBalance interface and encapsulates some common logic. as follows

  public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        if (invokers != null && invokers.size() != 0) {
            return invokers.size() == 1 ? (Invoker)invokers.get(0) : this.doSelect(invokers, url, invocation);
        } else {
            return null;
        }
    }

AbstractLoadBalance implements the LoadBalance interface

@SPI("random")
public interface LoadBalance {
   @Adaptive({"loadbalance"})
   <T> Invoker<T> select(List<Invoker<T>> var1, URL var2, Invocation var3) throws RpcException;
}

AbstractLoadBalance not only implements basic load balancing logic, but also encapsulates some common logic, such as service provider weight calculation logic. getWeight() and so on.

protected int getWeight(Invoker<?> invoker, Invocation invocation) {
    // Getting weight Weight Configuration Value from url
    int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
    if (weight > 0) {
        // Get the service provider startup timestamp
        long timestamp = invoker.getUrl().getParameter(Constants.REMOTE_TIMESTAMP_KEY, 0L);
        if (timestamp > 0L) {
            // Computing service provider runtime
            int uptime = (int) (System.currentTimeMillis() - timestamp);
            // Get service preheating time, default 10 minutes
            int warmup = invoker.getUrl().getParameter(Constants.WARMUP_KEY, Constants.DEFAULT_WARMUP);
            // If the service runtime is less than the preheating time, the service weight is recalculated, i.e. reduced weight.
            if (uptime > 0 && uptime < warmup) {
                // Recalculating Service Weight
                weight = calculateWarmupWeight(uptime, warmup, weight);
            }
        }
    }
    return weight;
}

static int calculateWarmupWeight(int uptime, int warmup, int weight) {
    // To calculate the weight, the following code is logically similar to (uptime / warmup) * weight.
    // As the uptime of service runtime increases, the weight calculation value w will gradually approach the configuration value weight.
    int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
    return ww < 1 ? 1 : (ww > weight ? weight : ww);
}

This process is mainly used to ensure that when the service runtime is less than the service preheating time, the service is degraded to avoid the service being in a high load state at the beginning of startup. Service preheating is an optimization tool, similar to JVM preheating. The main purpose is to make the service run "low power" for a period of time after start-up, so that its efficiency is gradually improved to the best state.

RandomLoadBalance

The source code is as follows:

public class RandomLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "random";
    private final Random random = new Random();

    public RandomLoadBalance() {
    }

    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size();
        int totalWeight = 0;
        boolean sameWeight = true;

        int offset;
        for(offset = 0; offset < length; ++offset) {
            int weight = this.getWeight((Invoker)invokers.get(offset), invocation);
            totalWeight += weight;
            if (sameWeight && offset > 0 && weight != this.getWeight((Invoker)invokers.get(offset - 1), invocation)) {
                sameWeight = false;
            }
        }

        if (totalWeight > 0 && !sameWeight) {
            offset = this.random.nextInt(totalWeight);
            Iterator i$ = invokers.iterator();

            while(i$.hasNext()) {
                Invoker<T> invoker = (Invoker)i$.next();
                offset -= this.getWeight(invoker, invocation);
                if (offset < 0) {
                    return invoker;
                }
            }
        }

        return (Invoker)invokers.get(this.random.nextInt(length));
    }
}

Among them, the weight of calculation method is used:
The loop subtracts the offset number from the weight value of the service provider, and returns the corresponding Invoker when the offset is less than 0.

  1. For example, we have servers = A, B, C], weights = 5, 3, 2], offset = 7.
  2. For the first cycle, offset - 5 = 2 > 0, offset > 5,
  3. It indicates that it will not fall on the corresponding interval of server A.
  4. In the second cycle, offset - 3 = 1 < 0, that is, 5 < offset < 8,
  5. Indicates that it will fall on the corresponding interval of server B.

After multiple requests, the call request can be "evenly" distributed according to the weight value. Random LoadBalance also has some drawbacks. When the number of calls is small, the random number generated by Random may be more centralized, at which time most requests will fall on the same server. This disadvantage is not very serious and can be ignored in most cases. Random LoadBalance is a simple and efficient load balancing implementation, so Dubbo chose it as its default implementation.

LeastActiveLoadBalance

Least Active Load Balance translates to minimum active load balancing. The smaller the number of active calls, the more efficient the service provider is, and more requests can be processed per unit time. At this point, the request should be assigned priority to the service provider. In the specific implementation, each service provider corresponds to an active number. Initially, all service providers are active at 0. For each request received, the number of active requests is increased by 1, and the number of active requests is reduced by 1 when the request is completed. After the service runs for a period of time, the service providers with good performance process requests faster, so the active number decreases faster. At this time, such service providers can get new service requests first. This is the basic idea of the minimum active number load balancing algorithm. In addition to the minimum active number, Least Active LoadBalance also introduces weight values in its implementation. So, to be exact, Least Active Load Balance is based on the weighted minimum active number algorithm. For example, in a cluster of service providers, there are two service providers with excellent performance. At a certain time, they have the same number of active requests. At this time, Dubbo will allocate requests according to their weights. The greater the weight, the greater the probability of obtaining new requests. If two service providers have the same weight, then choose one at random.

public class LeastActiveLoadBalance extends AbstractLoadBalance {

    public static final String NAME = "leastactive";

    private final Random random = new Random();

    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        int length = invokers.size();
        // Minimum Active Number
        int leastActive = -1;
        // Number of service providers with the same "minimum active number" (hereinafter referred to as Invoker)
        int leastCount = 0; 
        // leastIndexs is used to record the subscript information of Invoker with the same "minimum active number" in the invokers list
        int[] leastIndexs = new int[length];
        int totalWeight = 0;
        // The Invoker weight value of the first minimum active number is used to compare with other Invoker weights with the same minimum active number.
        // To detect whether "all Invoker s with the same minimum number of active weights" are equal
        int firstWeight = 0;
        boolean sameWeight = true;

        // Traversing the invokers list
        for (int i = 0; i < length; i++) {
            Invoker<T> invoker = invokers.get(i);
            // Get the active number corresponding to Invoker
            int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
            // Getting Weight
            int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT);
            // Find a smaller number of activities and start over
            if (leastActive == -1 || active < leastActive) {
            	// Update the minimum active number leastActive with the current active number
                leastActive = active;
                // Update leastCount to 1
                leastCount = 1;
                // Record the current subscription to leastIndexs
                leastIndexs[0] = i;
                totalWeight = weight;
                firstWeight = weight;
                sameWeight = true;

            // The current active number of Invoker is the same as the minimum active number, leastActive. 
            } else if (active == leastActive) {
            	// Record the subscript of the current Invoker in the invokers collection in leastIndexs
                leastIndexs[leastCount++] = i;
                // Cumulative weight
                totalWeight += weight;
                // Check whether the current Invoker weights are equal to the first Weight.
                // Unequal sets sameWeight to false
                if (sameWeight && i > 0
                    && weight != firstWeight) {
                    sameWeight = false;
                }
            }
        }
        
        // When only one Invoker has the minimum active number, it is sufficient to return the Invoker directly.
        if (leastCount == 1) {
            return invokers.get(leastIndexs[0]);
        }

        // Multiple Invoker s have the same minimum active number, but their weights are different.
        if (!sameWeight && totalWeight > 0) {
        	// Random generation of a number between [0, total weight]
            int offsetWeight = random.nextInt(totalWeight);
            // The loop subtracts the weight of Invoker with the smallest active number from the random number.
            // When offset is less than or equal to 0, return the corresponding Invoker
            for (int i = 0; i < leastCount; i++) {
                int leastIndex = leastIndexs[i];
                // Get the weight value and subtract the weight value from the random number.
                offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
                if (offsetWeight <= 0)
                    return invokers.get(leastIndex);
            }
        }
        // If the weight is the same or the weight is 0, an Invoker is returned randomly.
        return invokers.get(leastIndexs[random.nextInt(leastCount)]);
    }
}
  1. Traverse the invokers list to find the Invoker with the smallest active number
  2. If there are multiple Invokers with the same minimum active number, record the subscripts of these Invokers in the invokers set and accumulate their weights to compare whether their weights are equal.
  3. If only one Invoker has the smallest active number, then return the Invoker directly.
  4. If there are multiple Invoker s with the minimum active number and their weights are not equal, the processing is consistent with Random LoadBalance
  5. If there are multiple Invoker s with the minimum active number, but their weights are equal, then randomly return one.

ConsistentHashLoadBalance

Consistent Hash algorithm achieves load balancing. It works like this. First, it generates a hash for the cache node based on ip or other information and projects the hash onto the ring of [0,232-1]. When there is a query or write request, a hash value is generated for the key of the cached item. The first cache node greater than or equal to the hash value is then found and queried or written to the cache item in the node. If the current node hangs, the next query or write to the cache will find another cache node larger than its hash value for the cache item. The general effect is as shown in the figure below, where each cache node occupies a position on the ring. If the hash value of the key of the cached item is less than the hash value of the cached node, the cached item is stored or read in the cached node. For example, the cache items corresponding to the green dots below will be stored in the cache-2 node. Because cache-3 hangs, the cache items that should have been stored in the node will eventually be stored in the cache-4 node.

We replaced the cached nodes in the above figure with Dubbo's service provider and got the following figure:

Nodes of the same color here belong to the same service provider, such as Invoker 1-1, Invoker 1-2,... Invoker 1-160. The purpose of this method is to avoid data skew by introducing virtual nodes to disperse Invoker on the ring. The so-called data skew refers to the situation that a large number of requests fall on the same node because the nodes are not dispersed enough, while other nodes only receive a small number of requests.

public class ConsistentHashLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "consistenthash";
    private final ConcurrentMap<String, ConsistentHashLoadBalance.ConsistentHashSelector<?>> selectors = new ConcurrentHashMap();
    private static final int SERVICE_STRATEGY = 1;
    private static final int METHOD_STRATEGY = 0;
    private static final String PARAM_NAME = "consistenttype";
    private static final ThreadLocal<MessageDigest> MD5_THREADLOCAL = new ThreadLocal<MessageDigest>() {
        protected MessageDigest initialValue() {
            try {
                return MessageDigest.getInstance("MD5");
            } catch (Exception var2) {
                throw new IllegalStateException(var2.getMessage(), var2);
            }
        }
    };

    public ConsistentHashLoadBalance() {
    }

    protected int getConsistentType(URL url, String methodName) {
        return KeyWrapperUtils.getMethodParameter(url, methodName, this.keyWrapper, "consistenttype", 0);
    }

    protected String getHashArguments(URL url, String methodName) {
        return KeyWrapperUtils.getMethodParameter(url, methodName, this.keyWrapper, "hash.arguments", "0");
    }

    protected int getHashNodes(URL url, String methodName) {
        return KeyWrapperUtils.getMethodParameter(url, methodName, this.keyWrapper, "hash.nodes", 160);
    }

    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        Integer level = this.getConsistentType(url, invocation.getMethodName());
        String key;
        if (level == 1) {
            key = ((Invoker)invokers.get(0)).getUrl().getServiceKey();
        } else {
            key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + invocation.getMethodName();
        }

        int identityHashCode = System.identityHashCode(invokers);
        ConsistentHashLoadBalance.ConsistentHashSelector<T> selector = (ConsistentHashLoadBalance.ConsistentHashSelector)this.selectors.get(key);
        if (selector == null || selector.identityHashCode != identityHashCode) {
            this.selectors.put(key, new ConsistentHashLoadBalance.ConsistentHashSelector(this, invokers, invocation.getMethodName(), identityHashCode));
            selector = (ConsistentHashLoadBalance.ConsistentHashSelector)this.selectors.get(key);
        }

        return selector.select(invocation);
    }

    private static final class ConsistentHashSelector<T> {
        private final TreeMap<Long, Invoker<T>> virtualInvokers = new TreeMap();
        public final int identityHashCode;
        private final int[] argumentIndex;

        public ConsistentHashSelector(ConsistentHashLoadBalance loadBalance, List<Invoker<T>> invokers, String methodName, int identityHashCode) {
            this.identityHashCode = identityHashCode;
            URL url = ((Invoker)invokers.get(0)).getUrl();
            String[] index = Constants.COMMA_SPLIT_PATTERN.split(loadBalance.getHashArguments(url, methodName));
            this.argumentIndex = new int[index.length];

            int replicaNumber;
            for(replicaNumber = 0; replicaNumber < index.length; ++replicaNumber) {
                this.argumentIndex[replicaNumber] = Integer.parseInt(index[replicaNumber]);
            }

            replicaNumber = loadBalance.getHashNodes(url, methodName);
            Iterator i$ = invokers.iterator();

            while(i$.hasNext()) {
                Invoker<T> invoker = (Invoker)i$.next();
                String address = invoker.getUrl().getAddress();

                for(int i = 0; i < replicaNumber / 4; ++i) {
                    byte[] digest = this.md5(address + i);

                    for(int h = 0; h < 4; ++h) {
                        long m = this.hash(digest, h);
                        this.virtualInvokers.put(m, invoker);
                    }
                }
            }

        }

        public Invoker<T> select(Invocation invocation) {
            String key = this.toKey(invocation.getArguments());
            byte[] digest = this.md5(key);
            return this.selectForKey(this.hash(digest, 0));
        }

        private String toKey(Object[] args) {
            StringBuilder buf = new StringBuilder();
            int[] arr$ = this.argumentIndex;
            int len$ = arr$.length;

            for(int i$ = 0; i$ < len$; ++i$) {
                int i = arr$[i$];
                if (i >= 0 && i < args.length) {
                    buf.append(args[i]);
                }
            }

            return buf.toString();
        }

        private Invoker<T> selectForKey(long hash) {
            Long key = hash;
            if (!this.virtualInvokers.containsKey(key)) {
                SortedMap<Long, Invoker<T>> tailMap = this.virtualInvokers.tailMap(key);
                if (tailMap.isEmpty()) {
                    key = (Long)this.virtualInvokers.firstKey();
                } else {
                    key = (Long)tailMap.firstKey();
                }
            }

            Invoker<T> invoker = (Invoker)this.virtualInvokers.get(key);
            return invoker;
        }

        private long hash(byte[] digest, int number) {
            return ((long)(digest[3 + number * 4] & 255) << 24 | (long)(digest[2 + number * 4] & 255) << 16 | (long)(digest[1 + number * 4] & 255) << 8 | (long)(digest[number * 4] & 255)) & 4294967295L;
        }

        private byte[] md5(String value) {
            MessageDigest md5 = (MessageDigest)ConsistentHashLoadBalance.MD5_THREADLOCAL.get();
            md5.reset();

            byte[] bytes;
            try {
                bytes = value.getBytes("UTF-8");
            } catch (UnsupportedEncodingException var5) {
                throw new IllegalStateException(var5.getMessage(), var5);
            }

            md5.update(bytes);
            return md5.digest();
        }
    }
}

There are a lot of codes. Generally speaking, we try to initialize the configuration information of the node with ConsistentHashSelector first, then calculate the hash value of the virtual node when calculating the node, TreeMap stores the node information, and then we calculate the parameters md5 and hash to get a hash value every time we look up. Then take this value to TreeMap to find the target Invoker.
Consistent hash algorithm uses many scenarios. It's better to familiarize yourself with this algorithm first, and then look at the code, which is much easier.

Topics: less Dubbo Load Balance jvm