Soul 网关源码分析(十三)divide 插件(二)

205 阅读3分钟

Divide 插件是 Soul 网关中的核心插件,在之前的文章中我们实际上已经在经常使用它了。今天我们从源码层面来探秘 divide 插件的底层实现。

项目结构

.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── org
        │       └── dromara
        │           └── soul
        │               └── plugin
        │                   └── divide
        │                       ├── DividePlugin.java AbstractSoulPlugin 实现类,实现了 doExecute 函数,插件链的一环
        │                       ├── balance
        │                       │   ├── LoadBalance.java LoadBalance SPI,定义了 select 函数,用于选择负载均衡策略
        │                       │   ├── spi
        │                       │   │   ├── AbstractLoadBalance.java 定义了抽象函数 doSelect 和 获取负载权重的方式
        │                       │   │   ├── HashLoadBalance.java 根据 upstream url 做 hash 后的负载均衡策略
        │                       │   │   ├── RandomLoadBalance.java 随机生成权重后的的负载均衡策略
        │                       │   │   └── RoundRobinLoadBalance.java 轮询调度算法负载均衡策略
        │                       │   └── utils
        │                       │       └── LoadBalanceUtils.java 根据配置选择负载均衡策略的工具类
        │                       ├── cache
        │                       │   └── UpstreamCacheManager.java upstream 缓存管理
        │                       ├── handler
        │                       │   └── DividePluginDataHandler.java divide 插件数据控制器
        │                       └── websocket
        │                           └── WebSocketPlugin.java websocket 插件
        └── resources
            └── META-INF
                └── soul
                    └── org.dromara.soul.plugin.divide.balance.LoadBalance

首先看下 DividePlugin:

public class DividePlugin extends AbstractSoulPlugin {

    @Override
    protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
    	//首先从 ServerWebExchange 中获取到 SoulContext
        final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
        assert soulContext != null;
        //用传入的 rule 反序列化成 DivideRuleHandle 对象,其中包括负载均衡策略,重试次数和超时时间
        final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
        // 用selector 的 id 获取 upstream 列表,其中包含上游域名 url,所用协议等属性
        final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());
        //如果upstreamList为空,则返回异常
        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);
        }
        // 从 ServerWebExchange 获取请求的 ip 地址
        final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
        // 根据负载均衡策略选择将要使用的 DivideUpstream
        DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
        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);
        }
        // 设置域名,如果没有设置默认为 http://
        String domain = buildDomain(divideUpstream);
        // 获取真实的请求地址
        String realURL = buildRealURL(domain, soulContext, exchange);
        // 设置相关属性,将配置的属性设置到 ServerWebExchange 中以便传递给 SoulPluginChain 的下一链
        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);
    }
    ...
}

再看下 HashLoadBalance:

@Join
public class HashLoadBalance extends AbstractLoadBalance {

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

    @Override
    public DivideUpstream doSelect(final List<DivideUpstream> upstreamList, final String ip) {
    	// 第一次看见这个玩意,ConcurrentSkipListMap 是基于跳表实现的线程安全的有序的哈希表,是线程安全的TreeMap,TreeMap 由红黑树实现
        final ConcurrentSkipListMap<Long, DivideUpstream> treeMap = new ConcurrentSkipListMap<>();
        // 遍历 upstream 列表
        for (DivideUpstream address : upstreamList) {
            for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
            	//调用 hash函数 计算地址的 hash value
                long addressHash = hash("SOUL-" + address.getUpstreamUrl() + "-HASH-" + i);
                treeMap.put(addressHash, address);
            }
        }
        // 将 客户端 ip 地址做一次 hash
        long hash = hash(String.valueOf(ip));
        // tailMap 的作用是返回此map的部分视图,其键大于等于 fromKey,也就是我们使用 ip 地址生成的 hash
        SortedMap<Long, DivideUpstream> lastRing = treeMap.tailMap(hash);
        if (!lastRing.isEmpty()) {
        	//返回第一个 key 的 value
            return lastRing.get(lastRing.firstKey());
        }
        // 如果没有比 hash 值大的,则返回第一个 value
        return treeMap.firstEntry().getValue();
    }

    private static long hash(final String key) {
        // 使用 md5 作为信息摘要
        MessageDigest md5;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new SoulException("MD5 not supported", e);
        }
        md5.reset();
        byte[] keyBytes;
        //读取 key 的二进制数组
        keyBytes = key.getBytes(StandardCharsets.UTF_8);

        md5.update(keyBytes);
        //摘要生成字节数组
        byte[] digest = md5.digest();

        // 一系列的位运算,计算出 32 位的 hashcode
        long hashCode = (long) (digest[3] & 0xFF) << 24
                | ((long) (digest[2] & 0xFF) << 16)
                | ((long) (digest[1] & 0xFF) << 8)
                | (digest[0] & 0xFF);
        return hashCode & 0xffffffffL;
    }

}

总结

我们粗略的看了下 divide 插件的项目结构,理清楚了 doExecute 中的对于我们 配置 的 url,域名,协议,负载均衡算法的处理逻辑,还在源码中发现了之前没有接触过的 ConcurrentSkipListMap,了解了它的数据结构,应用场景和在 HashLoadBalance 中的用法。