插件作用
divide 插件是进行 http 服务(除dubbo、springcloud)代理的插件,所有 http 类型的请求,都是由该插件进行负载均衡的调用。
经过http请求的选择器、规则处理之后,开始进行divide请求处理。
divide插件的作用:
1)通过负载均衡策略(hash、random、roundrobin),获取服务主机实例
2)构建完整的服务请求路径
核心方法
请求到达DividePlugin
的doExecute
方法:
@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台服务器,权重值如下:
四台服务器:
服务器名 | 服务器地址 | 权重 |
---|---|---|
A | 192.168.0.1 | 1 |
B | 192.168.0.2 | 2 |
C | 192.168.0.3 | 3 |
D | 192.168.0.4 | 4 |
算法:累加每个元素的权重A(1)-B(3)-C(6)-D(10),则4个元素的的权重区间分别为[0,1)、[1,3)、[3,6)、[6,10)。然后随机出一个[0,10)之间的随机数。落在哪个区间,则该区间之后的元素即为按权重命中的元素。
即:
服务器名 | 服务器地址 | 序号 |
---|---|---|
A | 192.168.0.1 | 0 |
B | 192.168.0.2 | 1 |
B | 192.168.0.2 | 2 |
C | 192.168.0.3 | 3 |
C | 192.168.0.3 | 4 |
C | 192.168.0.3 | 5 |
D | 192.168.0.4 | 6 |
D | 192.168.0.4 | 7 |
D | 192.168.0.4 | 8 |
D | 192.168.0.4 | 9 |
算法代码:
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.1 | 5 |
192.168.0.2 | 1 |
192.168.0.3 | 1 |
经过加权轮询后,
请求 | 选中前的当前权重 | currentPos | 选中的实例 | 选中后的当前权重 |
---|---|---|---|---|
1 | {5, 1, 1} | 0 | 192.168.0.1 | {-2, 1, 1} |
2 | {3, 2, 2} | 0 | 192.168.0.1 | {-4, 2, 2} |
3 | {1, 3, 3} | 1 | 192.168.0.2 | {1, -4, 3} |
4 | {6, -3, 4} | 0 | 192.168.0.1 | {-1, -3, 4} |
5 | {4, -2, 5} | 2 | 192.168.0.3 | {4, -2, -2} |
6 | {9, -1, -1} | 0 | 192.168.0.1 | {2, -1, -1} |
7 | {7, 0, 0} | 0 | 192.168.0.1 | {0, 0, 0} |
8 | {5, 1, 1} | 0 | 192.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;
}
}