soul 源码分析 —— 插件解析之DividePlugin插件

插件作用

divide 插件是进行 http 服务(除dubbo、springcloud)代理的插件,所有 http 类型的请求,都是由该插件进行负载均衡的调用。

经过http请求的选择器、规则处理之后,开始进行divide请求处理。

divide插件的作用:

1)通过负载均衡策略(hash、random、roundrobin),获取服务主机实例

2)构建完整的服务请求路径

核心方法

请求到达DividePlugindoExecute方法:

@Override
    protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
        // 获取soul上下文
        final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
        assert soulContext != null;
        // 获取规则对应的负载均衡策略
        final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
        // 获取服务器对应的在线服务实例
        final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());
        // 如果没有在线的服务实例,返回“divide upstream configuration error: {规则handler信息}”错误
        if (CollectionUtils.isEmpty(upstreamList)) {
            log.error("divide upstream configuration error: {}", rule.toString());
            Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
            return WebFluxResultUtils.result(exchange, error);
        }
        // 获取当前请求的主机ip
        final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
        // 获取负载均衡实例,目前负载均衡策略支持:hash、random、robbin
        DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
        // 如果没有获取到实例,返回“divide has no upstream”
        if (Objects.isNull(divideUpstream)) {
            log.error("divide has no upstream");
            Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
            return WebFluxResultUtils.result(exchange, error);
        }
        // set the http url
        // 构建域名部分路径。比如:http://192.168.0.10:8819
        String domain = buildDomain(divideUpstream);
        // 构建真实的路径。比如:http://192.168.0.10:8819/http/order/findById?id=9
        String realURL = buildRealURL(domain, soulContext, exchange);
        // 设置请求路径
        exchange.getAttributes().put(Constants.HTTP_URL, realURL);
        // set the http timeout
        // 设置超时时间
        exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout());
        // 设置请求重试次数
        exchange.getAttributes().put(Constants.HTTP_RETRY, ruleHandle.getRetry());
        // 继续下一个插件链请求
        return chain.execute(exchange);
    }
  • 先获取服务的实例列表upstreamList,如果不存在,返回"divide upstream configuration error: {}",表示当前没有服务实例在运行。
  • 接着通过LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip),获取用户定义的负载均衡策略
  • 最后,拿到负载均衡返回的实例对象,通过buildRealURL(domain, soulContext, exchange)去构建完整的请求url

上述执行中存在两个对象:

DivideRuleHandle:请求服务的规则

DivideUpstream:负载均衡获取到的服务实例

DivideRuleHandle

@Data
@NoArgsConstructor
public class DivideRuleHandle implements RuleHandle {
    
    private String loadBalance; // 负载均衡策略
    private int retry; // 请求重试次数
    private long timeout = Constants.TIME_OUT; // 请求超时时间

    @Override
    public RuleHandle createDefault(final String path) {
        this.loadBalance = RuleHandleConstants.DEFAULT_LOAD_BALANCE.getName();
        this.retry = RuleHandleConstants.DEFAULT_RETRY;
        return this;
    }
}

DivideUpstream

@Data
@ToString
@Builder
public class DivideUpstream implements Serializable {

    private String upstreamHost; // 主机 比如:localhost
    private String protocol; // 协议 比如:http、https
    private String upstreamUrl; // ip:port
    private int weight; // 权重
    @Builder.Default
    private boolean status = true; // 状态:开启、禁用
    private long timestamp; // 开启时间
    private int warmup; // 预热
}

负载均衡策略

权重随机策略(random)
@Join
public class RandomLoadBalance extends AbstractLoadBalance {

    private static final Random RANDOM = new Random();

    @Override
    public DivideUpstream doSelect(final List<DivideUpstream> upstreamList, final String ip) {
        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);
    }
    ......
}

通过calculateTotalWeight(upstreamList)方法,获取所有服务实例的总权重值

private int calculateTotalWeight(final List<DivideUpstream> upstreamList) {
        // total weight
        int totalWeight = 0;
        // 遍历每个服务实例
        for (DivideUpstream divideUpstream : upstreamList) {
            int weight = getWeight(divideUpstream);
            // Cumulative total weight
            totalWeight += weight;
        }
        return totalWeight;
    }

通过isAllUpStreamSameWeight(upstreamList);方法,每两个服务实例进行比较,判断是否所有服务实例的权重值都相同。如果相同返回true,否则返回false

private boolean isAllUpStreamSameWeight(final List<DivideUpstream> upstreamList) {
        boolean sameWeight = true;
        int length = upstreamList.size();
        // 通过每两个服务实例进行比较,判断是否所有服务实例的权重值都相同
        // 如果相同返回true,否则返回false
        for (int i = 0; i < length; i++) {
            int weight = getWeight(upstreamList.get(i));
            if (i > 0 && weight != getWeight(upstreamList.get(i - 1))) {
                // Calculate whether the weight of ownership is the same
                sameWeight = false;
                break;
            }
        }
        return sameWeight;
    }

如果每个服务实例的权重值都相同,随机返回一个服务实例:

private DivideUpstream random(final List<DivideUpstream> upstreamList) {
        return upstreamList.get(RANDOM.nextInt(upstreamList.size()));
    }

如果每个服务实例的权重值不相同:

算法描述:

比如有4台服务器,权重值如下:

四台服务器:

服务器名服务器地址权重
A192.168.0.11
B192.168.0.22
C192.168.0.33
D192.168.0.44

算法:累加每个元素的权重A(1)-B(3)-C(6)-D(10),则4个元素的的权重区间分别为[0,1)、[1,3)、[3,6)、[6,10)。然后随机出一个[0,10)之间的随机数。落在哪个区间,则该区间之后的元素即为按权重命中的元素。

即:

服务器名服务器地址序号
A192.168.0.10
B192.168.0.21
B192.168.0.22
C192.168.0.33
C192.168.0.34
C192.168.0.35
D192.168.0.46
D192.168.0.47
D192.168.0.48
D192.168.0.49

算法代码:

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);
            // 当offset小于0时,说明offset落在此服务实例的区间,返回该服务实例
            if (offset < 0) {
                return divideUpstream;
            }
        }
        return upstreamList.get(0);
    }
  • 先生成一个总权重值的随机数
  • 遍历每一个服务实例,随机值每次减去服务实例获取的权重值
  • 当offset小于0时,说明offset落在此服务实例的区间,返回该服务实例

获取权重值时getWeight(divideUpstream),还加了一个预热的功能:

预热算法如下:

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 calculateWarmupWeight(final int uptime, final int warmup, final int weight) {
        // 获取预热时间的权重值
        int ww = (int) ((float) uptime / ((float) warmup / (float) weight));
        // 如果计算ww < 1; 取 ww = 1返回
        // 如果ww > weight, 取 ww == weight 返回
        // 如果ww < weight, 取ww返回
        return ww < 1 ? 1 : (ww > weight ? weight : ww);
    }
加权轮询算法

有三个节点{a, b, c},他们的权重分别是{a=5, b=1, c=1}。发送7次请求,a会被分配5次,b会被分配1次,c会被分配1次。

算法思路:
1)计算当前状态下所有节点的之和totalWeight,开始轮询

2)第一次按初始值,选出当前权重(currentWeight)值最大的那个节点。作为当前选择的服务实例。

3)选中节点之后,选中的节点进行减值操作:currentWeight = currentWeight - totalWeight,其它节点不变。这就是选中后的节点,继续轮询

4)轮询前,每个节点加上各自的初始值,变成一个新的当前权重,开始轮询。按照3、4部周而复始的轮询

算法示例:

服务实例权重值
192.168.0.15
192.168.0.21
192.168.0.31

经过加权轮询后,

请求选中前的当前权重currentPos选中的实例选中后的当前权重
1{5, 1, 1}0192.168.0.1{-2, 1, 1}
2{3, 2, 2}0192.168.0.1{-4, 2, 2}
3{1, 3, 3}1192.168.0.2{1, -4, 3}
4{6, -3, 4}0192.168.0.1{-1, -3, 4}
5{4, -2, 5}2192.168.0.3{4, -2, -2}
6{9, -1, -1}0192.168.0.1{2, -1, -1}
7{7, 0, 0}0192.168.0.1{0, 0, 0}
8{5, 1, 1}0192.168.0.1{-2, 1, 1}

第 8 次调度时当前有效权重值又回到 {5, 1, 1}

divide中加权轮询算法实现:

public DivideUpstream doSelect(final List<DivideUpstream> upstreamList, final String ip) {
        // 获取第一个服务的实例作为key
        String key = upstreamList.get(0).getUpstreamUrl();
        // 获取当前的服务器实例
        ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
        // 如果值为空,初始服务器实例大小:16个
        if (map == null) {
            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();
            // 根据权重ip:端口,获取服务实例的权重
            WeightedRoundRobin weightedRoundRobin = map.get(rKey);
            int weight = getWeight(upstream);
            // 如果map中不存在,添加roundrobin权重对象
            if (weightedRoundRobin == null) {
                weightedRoundRobin = new WeightedRoundRobin();
                weightedRoundRobin.setWeight(weight);
                map.putIfAbsent(rKey, weightedRoundRobin);
            }
            // 如果weightedRoundRobin已存在,并且与当前的权重值不同,更改权重值
            if (weight != weightedRoundRobin.getWeight()) {
                //weight changed
                weightedRoundRobin.setWeight(weight);
            }

            // 当前的权重值,默认值为0,加上自身的weight
            long cur = weightedRoundRobin.increaseCurrent();
            // 设置当前的毫秒数
            weightedRoundRobin.setLastUpdate(now);
            // maxCurrent 每次都会被设为当前权重的最大值,即权重最大的服务器实例会被选中
            if (cur > maxCurrent) {
                maxCurrent = cur;
                selectedInvoker = upstream;
                selectedWRR = weightedRoundRobin;
            }
            // 每次循环都要重新算一次总权重
            totalWeight += weight;
        }
        // 如果服务器实例下线,剔除掉无用的服务器实例
        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);
            }
        }
        // 如果selectedInvoker不为空,即获取到了被选中的服务器实例
        if (selectedInvoker != null) {
            // 当前被选中的服务实例的权重值 减去总权重值
            selectedWRR.sel(totalWeight);
            return selectedInvoker;
        }
        // should not happen here
        return upstreamList.get(0);
}

首先构建了一个map实例,来存放服务实例

ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);

接着遍历服务实例,把每个服务实例的权重值存放到map中

每次遍历前,各服务实例的初始值cur都为0,通过increaseCurrent方法,加上各自的权重值

并通过maxCurrent临时变量,找到选中的服务实例

maxCurrent:用来保存当前最大的权重值,通过它来判断出当前的服务实例和服务对应的权重值实例

selectedInvoker:当前被选中的服务实例

selectedWRR:当前被选中的服务实例对应的权重实例

选中服务实例之后,通过 selectedWRR.sel(totalWeight),被选中的服务实例的权重值 减去总权重值

这样,每一次请求完,map对象都会保存各自当前的current(权重值)。

如果服务实例有下线,通过以下方法剔除:

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);
            }
 }
一致性hash算法

在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。

“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一个实际节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。

带虚拟节点的

public class HashLoadBalance extends AbstractLoadBalance {

    // 虚拟节点的数目
    private static final int VIRTUAL_NODE_NUM = 5;

    @Override
    public DivideUpstream doSelect(final List<DivideUpstream> upstreamList, final String ip) {
        //虚拟节点,key表示虚拟节点的hash值,value表示虚拟节点的名称
        final ConcurrentSkipListMap<Long, DivideUpstream> treeMap = new ConcurrentSkipListMap<>();
        // 再添加虚拟节点,遍历LinkedList使用foreach循环效率会比较高
        for (DivideUpstream address : upstreamList) {
            for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
                // 虚拟节点:SOUL-192.168.1.100-HASH-1
                long addressHash = hash("SOUL-" + address.getUpstreamUrl() + "-HASH-" + i);
                treeMap.put(addressHash, address);
            }
        }
        // 获取当前真实ip的哈希值
        long hash = hash(String.valueOf(ip));
        // 得到大于该Hash值的所有Map
        SortedMap<Long, DivideUpstream> lastRing = treeMap.tailMap(hash);
        if (!lastRing.isEmpty()) {
            // 第一个Key就是顺时针过去离node最近的那个结点,返回对应的服务器
            return lastRing.get(lastRing.firstKey());
        }
        // 如果没有比该key的hash值大的,则从第一个node开始,返回对应的服务器
        return treeMap.firstEntry().getValue();
    }

    // 使用md5算法计算服务器的Hash值
    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
        long hashCode = (long) (digest[3] & 0xFF) << 24
                | ((long) (digest[2] & 0xFF) << 16)
                | ((long) (digest[1] & 0xFF) << 8)
                | (digest[0] & 0xFF);
        return hashCode & 0xffffffffL;
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值