负载均衡
回顾下上篇文章Dubbo集群容错策略中介绍 AbstractClusterInvoker 代码会initLoadBalance 获取对应负载均衡算法,
具体的负载均衡逻辑 select 方法在各自Cluster实现类对应的Invoker中如 FailfastClusterInvoker
AbstractClusterInvoker.java
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();
// binding attachments into invocation.
Map<String, Object> contextAttachments = RpcContext.getContext().getObjectAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addObjectAttachments(contextAttachments);
}
List<Invoker<T>> invokers = list(invocation);
LoadBalance loadbalance = initLoadBalance(invokers, invocation);
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
return doInvoke(invocation, invokers, loadbalance);
}
select 实现
AbstractClusterInvoker.java
protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
if (CollectionUtils.isEmpty(invokers)) {
return null;
}
String methodName = invocation == null ? StringUtils.EMPTY_STRING : invocation.getMethodName();
boolean sticky = invokers.get(0).getUrl()
.getMethodParameter(methodName, CLUSTER_STICKY_KEY, DEFAULT_CLUSTER_STICKY);
//ignore overloaded method
if (stickyInvoker != null && !invokers.contains(stickyInvoker)) {
stickyInvoker = null;
}
//ignore concurrency problem
if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))) {
if (availablecheck && stickyInvoker.isAvailable()) {
return stickyInvoker;
}
}
Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);
if (sticky) {
stickyInvoker = invoker;
}
return invoker;
}
粘滞连接
再执行负载均衡算法时,首先判断请求参数中是sticky 参数,该参数为true 则开启粘滞连接,否则使用负载均衡算法选择一个服务提供者。
粘滞连接:让客户端总是向同一提供者发起调用,除非该提供者挂了才会重新选一台服务提供者进行调用。粘滞连接是有状态的,stickyInvoker记录上次使用的invoker。
使用方式如下:
<dubbo:reference id="xxxService" interface="com.xxx.XxxService" sticky="true" />
负载均衡算法核心
doSelect 真正从多个服务提供者选择一个提供者进行调用,doSelect 大致流程
-
如果只有一个Invoker , 直接返回(别无选择)
-
如果选择的invoker 不可用,则需要重新选择(
reselect
)reselect 重新选择,排除上次selected,尽量在未选的invoker列表中重新选择。 核心都是调用
loadbalance.select(reselectInvokers, getUrl(), invocation);
选择一个invoker
private Invoker<T> reselect(LoadBalance loadbalance, Invocation invocation,
List<Invoker<T>> invokers, List<Invoker<T>> selected, boolean availablecheck) throws RpcException {
//Allocating one in advance, this list is certain to be used.
List<Invoker<T>> reselectInvokers = new ArrayList<>(
invokers.size() > 1 ? (invokers.size() - 1) : invokers.size());
// First, try picking a invoker not in `selected`.
for (Invoker<T> invoker : invokers) {
if (availablecheck && !invoker.isAvailable()) {
continue;
}
if (selected == null || !selected.contains(invoker)) {
reselectInvokers.add(invoker);
}
}
if (!reselectInvokers.isEmpty()) {
return loadbalance.select(reselectInvokers, getUrl(), invocation);
}
// Just pick an available invoker using loadbalance policy
if (selected != null) {
for (Invoker<T> invoker : selected) {
if ((invoker.isAvailable()) // available first
&& !reselectInvokers.contains(invoker)) {
reselectInvokers.add(invoker);
}
}
}
if (!reselectInvokers.isEmpty()) {
return loadbalance.select(reselectInvokers, getUrl(), invocation);
}
return null;
}
负载均衡接口 LoadBalance
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
/**
* select one invoker in list.
*
* @param invokers invokers.
* @param url refer url
* @param invocation invocation.
* @return selected invoker.
*/
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}
常见几种实现
-
RandomLoadBalance 带权重的随机算法,可用动态调整权重,这是默认策略。
默认权重都一样,这种情况下适用于各个服务提供者性能都差不多情况,当然可以指定各个服务器的权重,使得性能好的机器权重大,性能差的机器权重小,从而使得总体性能最优。
-
RoundRobinLoadBalance 按公约后的权重设置轮询比率
如果某台服务提供者执行较慢,这台服务的请求就会逐渐堆积,堆积一定程度,后面路由到这台机器上请求无法及时处理。
当然这是教科书上说法,我们该如何避免呢?一般接口我们都会设置一个合理的超时时间,对于很慢的机器必然有大量接口超时,介入相关告警机制,及时告警。
-
LeastActive LoadBalance 在调用服务时,优先选择当前活跃调用数(正在处理的请求数)最少的服务提供者。 这种策略能够使慢的提供者收到更少请求。
因此该策略一般适用于服务提供者性能差异较大,或者某些服务提供者容易出现性能瓶颈的场景
-
ConsistentHash LoadBalance
一致性 Hash。当服务提供者数量动态变化时,能够保持请求稳定性。
通常情况下服务提供者不会经常变化,因此大部分情况, 相同参数的请求会被路由到同一提供者。
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
重要源码阅读
AbstractLoadBalance 权重计算
基于权重的负载均衡策略有 RandomLoadBalance,RoundRobinLoadBalance。计算权重时会考虑将系统启动时间,即预热时间
,默认预热时间10分钟。
预热时间:如果服务提供者启动时长,达不到预热时间,对应权重需要进行折算。
calculateWarmupWeight
根据预热时间计算权重
AbstractLoadBalance.java
int getWeight(Invoker<?> invoker, Invocation invocation) {
int weight;
URL url = invoker.getUrl();
// Multiple registry scenario, load balance among multiple registries.
if (REGISTRY_SERVICE_REFERENCE_PATH.equals(url.getServiceInterface())) {
weight = url.getParameter(REGISTRY_KEY + "." + WEIGHT_KEY, DEFAULT_WEIGHT);
} else {
weight = url.getMethodParameter(invocation.getMethodName(), WEIGHT_KEY, DEFAULT_WEIGHT);
if (weight > 0) {
long timestamp = invoker.getUrl().getParameter(TIMESTAMP_KEY, 0L);
if (timestamp > 0L) {
long uptime = System.currentTimeMillis() - timestamp;
if (uptime < 0) {
return 1;
}
int warmup = invoker.getUrl().getParameter(WARMUP_KEY, DEFAULT_WARMUP);
if (uptime > 0 && uptime < warmup) {
weight = calculateWarmupWeight((int)uptime, warmup, weight);
}
}
}
}
return Math.max(weight, 0);
}
使用方式
<dubbo:reference interface="..." loadbalance="roundrobin" />
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>