上文介绍了负载均衡器ILoadBalancer
的基本内容,并且详述了基本实现:BaseLoadBalancer
。它实现了作为ILoadBalancer
负载均衡器的基本功能,比如:服务列表维护、服务定时探活、负载均衡选择Server等。
但是BaseLoadBalancer
虽完成了基本功能,但还稍显捡漏,无法应对一些复杂场景如:
本文将继续介绍ILoadBalancer
的实现,它们均是BaseLoadBalancer
的子类,在此技术上增强功能,应对如上复杂场景。
本文介绍的是在面试中会问、工作中实际会用到的两个负载均衡器实现。在这之前,我希望你已经掌握了如下内容:
ZoneAvoidanceRule.getAvailableZones()
获取可用区的逻辑,参考文章:四十七、Ribbon多区域选择:ZoneAvoidanceRule.getAvailableZones()获取可用区AbstractServerPredicate
,参考文章:四十八、Ribbon服务器过滤逻辑的基础组件:AbstractServerPredicate它是BaseLoadBalancer
子类,具有动态源获取服务器列表的功能。即服务器列表在运行时可能会更改,此外,它还包含一些工具,其中包含服务器列表可以通过筛选条件过滤掉不需要的服务器。
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
// 这两个属性木有任何用处,也不知道是不是开发人员忘记删了 哈哈
boolean isSecure = false;
boolean useTunnel = false;
protected AtomicBoolean serverListUpdateInProgress = new AtomicBoolean(false);
volatile ServerList<T> serverListImpl;
volatile ServerListFilter<T> filter;
protected final ServerListUpdater.UpdateAction updateAction = () -> updateListOfServers();
protected volatile ServerListUpdater serverListUpdater;
}
serverListUpdateInProgress
:跟踪服务器列表的修改,当正在修改列表时,赋值为true,放置多个线程重复去操作,有点上锁的意思serverListImpl
:提供服务列表。默认实现是ConfigurationBasedServerList
,也就是Server列表来自于配置文件,如:account.ribbon.listOfServers = xxx,xxx
NIWSServerListClassName
来配置,比如你的自定义实现。当然你也可以通过set方法/构造器初始化时指定ConfigurationBasedServerList
,但是eureka环境下默认给你配置的是DomainExtractingServerList
(详见EurekaRibbonClientConfiguration
)filter
:对ServerList执行过滤。默认使用的ZoneAffinityServerListFilter
,可以通过key:NIWSServerListFilterClassName
来配置指定updateAction
:执行更新动作的action:updateListOfServers()
更新所有serverListUpdater
:更新器。默认使用PollingServerListUpdater
30轮询一次的更新方式,当然你可以通过ServerListUpdaterClassName
这个key自己去指定。当然set/构造器传入进来也是可以的可以看到,ILoadBalancer
管理的五大核心组件至此全部齐活:
BaseLoadBalancer
管理2个:IPing、IRule。负责了Server isAlive的探活,负责了负载均衡算法选择ServerDynamicServerListLoadBalancer
管理3个:ServerList、ServerListFilter、ServerListUpdater
负责动态管理、更新服务列表它有个restOfInit
方法,在初始化时进行调用。
DynamicServerListLoadBalancer:
void restOfInit(IClientConfig clientConfig) {
...
updateListOfServers();
// ~~~~~update之后马上进行首次连接~~~~~
if (primeConnection && this.getPrimeConnections() != null) {
this.getPrimeConnections().primeConnections(getReachableServers());
}
...
}
该初始化方法会在其它初始化事项完成后(如给各属性赋值)执行:对当前的Server列表进行初始化、更新。
该方法是维护动态列表的核心方法,它在初始化的时候便会调用一次,后续作为一个ServerListUpdater.UpdateAction
动作每30s便会执行一次。
DynamicServerListLoadBalancer:
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
servers = serverListImpl.getUpdatedListOfServers();
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
}
}
updateAllServerList(servers);
}
// 该方法作用:能保证同一时间只有一个线程可以进来做修改动作~~~
// 把准备好的Servers更新到列表里。该方法是protected,子类并无复写
// 但是,但是,但是setServersList()方法子类是有复写的哦
protected void updateAllServerList(List<T> ls) {
if (serverListUpdateInProgress.compareAndSet(false, true)) {
try {
// 显示标注Server均是活的(至于真活假活交给forceQuickPing()去判断)
for (T s : ls) {
s.setAlive(true);
}
setServersList(ls);
super.forceQuickPing();
} finally {
serverListUpdateInProgress.set(false);
}
}
}
逻辑简单,描述如下:
ServerList
拿到所有的Server们(如从配置文件中读取、从配置中心里拉取)ServerListFilter
过滤一把:如过滤掉zone负载过高的 / Server负载过高或者已经熔断了的ServersetServersList()
方法完成初始化。此处需要注意的是,本子类复写了此初始化方法:DynamicServerListLoadBalancer:
@Override
public void setServersList(List lsrv) {
super.setServersList(lsrv);
List<T> serverList = (List<T>) lsrv;
Map<String, List<Server>> serversInZones = new HashMap<String, List<Server>>();
for (Server server : serverList) {
// 调用这句的作用:确保初始化的时候就把ServerStats创建好
getLoadBalancerStats().getSingleServerStat(server);
// 把Server们按照zone进行分类,最终放到LoadBalancerStats.upServerListZoneMap属性里面去
String zone = server.getZone();
if (zone != null) {
zone = zone.toLowerCase();
List<Server> servers = serversInZones.get(zone);
if (servers == null) {
servers = new ArrayList<Server>();
serversInZones.put(zone, servers);
}
servers.add(server);
}
}
// 该方法父类实现很简单:getLoadBalancerStats().updateZoneServerMapping(zoneServersMap)
//子类有复写哦
setServerListForZones(serversInZones);
}
在父类super.setServersList(lsrv)
初始化的基础上,完成了对LoadBalancerStats、ServerStats
的初始化。
可是,你是否有疑问,为毛这段初始化逻辑不放在父类上呢???
解答:父类BaseLoadBalancer
的服务列表是静态的,一旦设置上去将不会再根据负载情况、熔断情况等Stats动态的去做移除等操作,所以放在父类上并无意义。
它是最强王者:具有zone区域意识的负载均衡器。它是Spring Cloud默认的负载均衡器,是对DynamicServerListLoadBalancer
的扩展。
ZoneAwareLoadBalancer
的出现主要是为了弥补DynamicServerListLoadBalancer
的不足:
DynamicServerListLoadBalancer
木有重写chooseServer()
方法,所以它的负载均衡算法依旧是BaseLoadBalancer
中默认的线性轮询(所有Server没区分概念,一视同仁,所以有可能这次请求打到区域A,下次去了区域B了~)ublic class ZoneAwareLoadBalancer<T extends Server> extends DynamicServerListLoadBalancer<T> {
private static final DynamicBooleanProperty ENABLED = DynamicPropertyFactory.getInstance().getBooleanProperty("ZoneAwareNIWSDiscoveryLoadBalancer.enabled", true);
private ConcurrentHashMap<String, BaseLoadBalancer> balancers = new ConcurrentHashMap<String, BaseLoadBalancer>();
private volatile DynamicDoubleProperty triggeringLoad;
private volatile DynamicDoubleProperty triggeringBlackoutPercentage;
}
ENABLED
:是否启用区域意识的choose选择Server。默认是true,你可以通过配置ZoneAwareNIWSDiscoveryLoadBalancer.enabled=false
来禁用它,如果你只有一个zone区域的话 balancers
:缓存zone对应的负载均衡器。每个zone都可以有自己的负载均衡器,从而可以有自己的IRule负载均衡策略~ triggeringLoad/triggeringBlackoutPercentage
:正两个参数讲解的次数太多遍了,请参考ZoneAvoidancePredicate
。实际生产环境中,但凡稍微大点的应用,跨区域部署几乎是必然的。因此ZoneAwareLoadBalancer
重写了setServerListForZones()
方法。
该方法在其父类DynamicServerListLoadBalancer
的中仅仅是根据zone进入了分组,赋值了Map<String, List<? extends Server>> upServerListZoneMap
和Map<String, ZoneStats> zoneStatsMap
这两个属性:
DynamicServerListLoadBalancer:
protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) {
getLoadBalancerStats().updateZoneServerMapping(zoneServersMap);
}
而本类的该方法实现如下:
ZoneAwareLoadBalancer:
@Override
protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) {
// 完成父类的赋值~~~~~
super.setServerListForZones(zoneServersMap);
if (balancers == null) {
balancers = new ConcurrentHashMap<String, BaseLoadBalancer>();
}
// getLoadBalancer(zone)的意思是获得指定zone所属的负载均衡器
// 这个意思是给每个LB设置它所管理的服务列表
for (Map.Entry<String, List<Server>> entry: zoneServersMap.entrySet()) {
String zone = entry.getKey().toLowerCase();
getLoadBalancer(zone).setServersList(entry.getValue());
}
// 这一步属一个小优化:若指定zone不存在了(木有机器了),就把balancers对应zone的机器置空
for (Map.Entry<String, BaseLoadBalancer> existingLBEntry: balancers.entrySet()) {
if (!zoneServersMap.keySet().contains(existingLBEntry.getKey())) {
existingLBEntry.getValue().setServersList(Collections.emptyList());
}
}
}
除了像父类一样完成相关属性的初始化、赋值外,它还做了两件事:
那么,它是如何给每个zone创建一个LB实例的???
ZoneAwareLoadBalancer:
BaseLoadBalancer getLoadBalancer(String zone) {
zone = zone.toLowerCase();
BaseLoadBalancer loadBalancer = balancers.get(zone);
if (loadBalancer == null) {
// ~~~~~~~~为每个zone都指定一个独立的IRule实例~~~~~~~~
IRule rule = cloneRule(this.getRule());
loadBalancer = new BaseLoadBalancer(this.getName() + "_" + zone, rule, this.getLoadBalancerStats());
BaseLoadBalancer prev = balancers.putIfAbsent(zone, loadBalancer);
if (prev != null) {
loadBalancer = prev;
}
}
return loadBalancer;
}
这是一个default访问权限的方法:每个zone对应的LB实例是BaseLoadBalancer
类型,使用的IRule
是克隆当前的单独实例(因为规则要完全隔离开来,所以必须用单独实例~),这么一来每个zone内部的负载均衡算法就可以达到隔离,负载均衡效果更佳。
ZoneAwareLoadBalancer:
@Override
public Server chooseServer(Object key) {
// 如果禁用了区域意识。或者只有一个zone,那就遵照父类逻辑
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
return super.chooseServer(key);
}
Server server = null;
try {
LoadBalancerStats lbStats = getLoadBalancerStats();
...
// 核心方法:根据triggeringLoad等阈值计算出可用区~~~~
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
// 从可用区里随机选择一个区域(zone里面机器越多,被选中概率越大)
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
// 按照IRule从该zone内选择一台Server出来
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
}
if (server != null) {
return server;
} else {
// 回退到父类逻辑~~~兜底
return super.chooseServer(key);
}
}
在已经掌握了ZoneAvoidanceRule#getAvailableZones、randomChooseZone
以及ZoneAvoidancePredicate
的工作原理后,对这部分代码的解读就非常的简单了:
ZoneAvoidanceRule.getAvailableZones()
方法拿到可用区们(会T除掉完全不可用的区域们,以及可用但是负载最高的一个区域)ZoneAvoidanceRule.randomChooseZone
随机选一个zone出来 略。
关于Ribbon负载均衡器ILoadBalancer(二):ZoneAwareLoadBalancer就先介绍到这了,它是Ribbon的最强负载均衡器,也是Spring Cloud默认使用的负载均衡器,因此本文内容重要。
另外需要注意的是:本负载均衡器只是对zone进行了感知,能保证每个zone里面的负载均衡策略都是隔离的。但是,但是,但是:它并不能保证你A区域的请求一定会打到A区域的Server内,而这个事是由过滤器如ZonePreferenceServerListFilter/ZoneAffinityServerListFilter
它们来完成的,它能过滤出和本地zone同一个zone的Servers来使用,请注意划分职责边界~
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有