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 中的用法。