Soul网关(14) - 负载均衡算法

305 阅读3分钟

话不多说,上来就干

上一篇 divide插件 我们了解分流插件,其中对于上游的过来的流量,进行分流

soul中的负载均衡算法包含三种算法:哈希值/随机/轮询

负载均衡

在负载均衡工具类 LoadBalanceUtils 中,最终通过接口 LoadBalanceselect(...)方法来实现负载均衡

其中参数 upstreamList是在网关管理后台配置的负载均衡一些参数,包括路由,比重,预热等

@SPI
public interface LoadBalance {

    /**
     * this is select one for upstream list.
     *
     * @param upstreamList upstream list
     * @param ip ip
     * @return divide upstream
     */
    DivideUpstream select(List<DivideUpstream> upstreamList, String ip);
}

DivideUpstream

 public class DivideUpstream implements Serializable {

    /**
     * ip
     */
    private String upstreamHost;

    /**
     * 端口
     */
    private String protocol;

    /**
     * 路由
     */
    private String upstreamUrl;

    /**
     * 负载均衡比重
     */
    private int weight;

    /**
     * 策略是否打开
     */
    @Builder.Default
    private boolean status = true;

    /**
     * 开始时间
     */
    private long timestamp;

    /**
     * 预热
     */
    private int warmup;

}

这个接口被@SPI修饰,表明是一个通过服务发现机制来实现的接口,这个接口的具体实现类是在运行时动态加载到内存的,在 META-INF.SOUL包下,有一个spi的接口文件,里面放的就是这个接口的具体实现类,java通过这个文件,可以在运行时,通过动态加载的方式,加载实现类

random=org.dromara.soul.plugin.divide.balance.spi.RandomLoadBalance
roundRobin=org.dromara.soul.plugin.divide.balance.spi.RoundRobinLoadBalance
hash=org.dromara.soul.plugin.divide.balance.spi.HashLoadBalance

使用模板方法,在抽象类中 AbstractLoadBalance定义了模板流程

public abstract class AbstractLoadBalance implements LoadBalance {

    /**
     * Do select divide upstream.
     *
     * @param upstreamList the upstream list
     * @param ip           the ip
     * @return the divide upstream
     */
    protected abstract DivideUpstream doSelect(List<DivideUpstream> upstreamList, String ip);

    @Override
    public DivideUpstream select(final List<DivideUpstream> upstreamList, final String ip) {
        if (CollectionUtils.isEmpty(upstreamList)) {
            return null;
        }
        if (upstreamList.size() == 1) {
            return upstreamList.get(0);
        }
        return doSelect(upstreamList, ip);
    }

    protected int getWeight(final DivideUpstream upstream) {
        if (!upstream.isStatus()) {
            return 0;
        }
        int weight = getWeight(upstream.getTimestamp(), getWarmup(upstream.getWarmup(), Constants.DEFAULT_WARMUP), upstream.getWeight());
        return weight;
    }

    private int getWeight(final long timestamp, final int warmup, final int weight) {
        if (weight > 0 && timestamp > 0) {
            int uptime = (int) (System.currentTimeMillis() - timestamp);
            if (uptime > 0 && uptime < warmup) {
                return calculateWarmupWeight(uptime, warmup, weight);
            }
        }
        return weight;
    }

    private int getWarmup(final int warmup, final int defaultWarmup) {
        if (warmup > 0) {
            return warmup;
        }
        return defaultWarmup;
    }

    private int calculateWarmupWeight(final int uptime, final int warmup, final int weight) {
        int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
        return ww < 1 ? 1 : (ww > weight ? weight : ww);
    }

}

最终还是通过抽象类的实现类,重写doSelect(...) 方法,来实现具体的负载均衡算法

哈希值

直接上代码

    @Override
    public DivideUpstream doSelect(final List<DivideUpstream> upstreamList, final String ip) {
        //构造一个自增长/自排序的map
        final ConcurrentSkipListMap<Long, DivideUpstream> treeMap = new ConcurrentSkipListMap<>();
        for (DivideUpstream address : upstreamList) {
            for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
                long addressHash = hash("SOUL-" + address.getUpstreamUrl() + "-HASH-" + i);
                //将所有路由都md5编码加入到map中
                treeMap.put(addressHash, address);
            }
        }
        //访问ip加密,作为获取部分元素的key
        long hash = hash(String.valueOf(ip));
        // 获取自增map大于 hash 的map集合
        SortedMap<Long, DivideUpstream> lastRing = treeMap.tailMap(hash);
        if (!lastRing.isEmpty()) {
            // 截取的map不为空,返回截取后的map的第一个元素的值
            return lastRing.get(lastRing.firstKey());
        }
        // 截取后的map为空,返回整个原始自增map的第一个元素的值
        return treeMap.firstEntry().getValue();
    }

    private static long hash(final String key) {
        // md5 byte
        MessageDigest md5;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new SoulException("MD5 not supported", e);
        }
        md5.reset();
        byte[] keyBytes;
        keyBytes = key.getBytes(StandardCharsets.UTF_8);

        md5.update(keyBytes);
        // 获取摘要
        byte[] digest = md5.digest();

        // hash code, Truncate to 32-bits
        // 转换32位
        long hashCode = (long) (digest[3] & 0xFF) << 24
                | ((long) (digest[2] & 0xFF) << 16)
                | ((long) (digest[1] & 0xFF) << 8)
                | (digest[0] & 0xFF);
        return hashCode & 0xffffffffL;
    }

随机

直接上代码

   public DivideUpstream doSelect(final List<DivideUpstream> upstreamList, final String ip) {
        // 计算总的比重,因为未必总的比重是100%
        int totalWeight = calculateTotalWeight(upstreamList);
        // 判断是否所有节点都是一样的比重
        boolean sameWeight = isAllUpStreamSameWeight(upstreamList);
        if (totalWeight > 0 && !sameWeight) {
            // 随机获取
            return random(totalWeight, upstreamList);
        }
        // If the weights are the same or the weights are 0 then random
        // 如果几个节点比重一致,则直接随机算法获取其中一个
        return random(upstreamList);
    }
    
  private DivideUpstream random(final int totalWeight, final List<DivideUpstream> upstreamList) {
      // If the weights are not the same and the weights are greater than 0, then random by the total number of weights
      // 获取总比重中的随机值
      int offset = RANDOM.nextInt(totalWeight);
      // Determine which segment the random value falls on
      // 比较比重,选择具体的分流规则
      for (DivideUpstream divideUpstream : upstreamList) {
          offset -= getWeight(divideUpstream);
          if (offset < 0) {
              return divideUpstream;
          }
      }
      return upstreamList.get(0);
  }

轮询

直接上代码

    public DivideUpstream doSelect(final List<DivideUpstream> upstreamList, final String ip) {
        String key = upstreamList.get(0).getUpstreamUrl();
        ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
        if (map == null) {
            //第一次进来初始化map
            methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<>(16));
            map = methodWeightMap.get(key);
        }
        int totalWeight = 0;
        long maxCurrent = Long.MIN_VALUE;
        long now = System.currentTimeMillis();
        DivideUpstream selectedInvoker = null;
        WeightedRoundRobin selectedWRR = null;
        for (DivideUpstream upstream : upstreamList) {
            String rKey = upstream.getUpstreamUrl();
            WeightedRoundRobin weightedRoundRobin = map.get(rKey);
            int weight = getWeight(upstream);
            if (weightedRoundRobin == null) {
                weightedRoundRobin = new WeightedRoundRobin();
                weightedRoundRobin.setWeight(weight);
                map.putIfAbsent(rKey, weightedRoundRobin);
            }
            if (weight != weightedRoundRobin.getWeight()) {
                //weight changed
                weightedRoundRobin.setWeight(weight);
            }
            long cur = weightedRoundRobin.increaseCurrent();
            weightedRoundRobin.setLastUpdate(now);
            if (cur > maxCurrent) {
                maxCurrent = cur;
                selectedInvoker = upstream;
                selectedWRR = weightedRoundRobin;
            }
            totalWeight += weight;
        }
        // 获取自旋锁
        // 更新 methodWeightMap
        if (!updateLock.get() && upstreamList.size() != map.size() && updateLock.compareAndSet(false, true)) {
            try {
                // copy -> modify -> update reference
                ConcurrentMap<String, WeightedRoundRobin> newMap = new ConcurrentHashMap<>(map);
                newMap.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > recyclePeriod);
                methodWeightMap.put(key, newMap);
            } finally {
                //释放自旋锁
                updateLock.set(false);
            }
        }
        if (selectedInvoker != null) {
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
        // should not happen here
        return upstreamList.get(0);
    }