前言
通过前两节Spring Cloud Ribbon客户端负载均衡(一),Spring Cloud Ribbon客户端负载均衡(二)的介绍,我们已经对SpringCloud如何使用Ribbon有了基本的了解。虽然SpringCloud中定义了LoadBalancerClient作为负载均衡器的通用接口,并且针对Ribbon实现了RibbonLoadBalancerClient,但是它在具体实现客户端负载均衡时,是通过Ribbon的ILoadBalancer接口实现的。
接下来就根据ILoadBalancer接口的实现类逐个看看它是怎样实现客户端负载均衡的。
AbstractLoadBalancer
在AbstractLoadBalancer类中定义了一个枚举类ServerGroup
public abstract class AbstractLoadBalancer implements ILoadBalancer {
public enum ServerGroup{
ALL,//所有服务实例
STATUS_UP,//正常服务的实例
STATUS_NOT_UP //停止服务的实例
}
public Server chooseServer() {
return chooseServer(null);
}
//定义了根据分组类型来获取不同的服务实例的列表
public abstract List<Server> getServerList(ServerGroup serverGroup);
public abstract LoadBalancerStats getLoadBalancerStats();
}
实现了一个chooseServer() 函数,该函数通过接口中的chooseServer(null)实现,表示在选择具体服务实例时忽略key的条件判断。
BaseLoadBalancer
BaseLoadBalancer类是Ribbon负载均衡器的基础实现类。
- 定义并维护了两个存储服务实例Server对象的列表,一个用于存储所有服务实例的请单,一个用于存储正常服务的实例清单。
@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> allServerList = Collections
.synchronizedList(new ArrayList<Server>());
@Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> upServerList = Collections
.synchronizedList(new ArrayList<Server>());
- 定义了用来存储负载均衡器各服务实例属性和统计信息的LoadBalancerStats对象
protected LoadBalancerStats lbStats;
- 定义了检查服务实例是否正常服务的IPing对象,在BaseLoadBalancer中默认为null,需要在构造时注入它的具体实现。
protected IPing ping = null;
- 定义了检查服务实例操作的执行策略对象IPingStrategy,在BaseLoadBalancer中默认使用了该类中定义的静态内部类SerialPingStrategy实现。
private static class SerialPingStrategy implements IPingStrategy {
@Override
public boolean[] pingServers(IPing ping, Server[] servers) {
int numCandidates = servers.length;
boolean[] results = new boolean[numCandidates];
logger.debug("LoadBalancer: PingTask executing [{}] servers configured", numCandidates);
for (int i = 0; i < numCandidates; i++) {
results[i] = false; /* Default answer is DEAD. */
try {
if (ping != null) {
results[i] = ping.isAlive(servers[i]);
}
} catch (Exception e) {
logger.error("Exception while pinging Server: '{}'", servers[i], e);
}
}
return results;
}
}
- 定义了负载均衡的处理规则IRule对象
负载均衡器实际将服务实例选择任务委托给个IRule实例中的choose函数来实现。在这里默认初始化RoundRobinRule为IRule的实现对象。RoundRobinRule实现了最基本且常用的线性负载均衡规则。
private final static IRule DEFAULT_RULE = new RoundRobinRule();
protected IRule rule = DEFAULT_RULE;
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
- 启动ping任务:在BaseLoadBalancer的默认构造函数中,会直接启动一个用于定时检查Server是否健康的任务,执行间隔为10s
protected Timer lbTimer = null;
protected int pingIntervalSeconds = 10;
public BaseLoadBalancer() {
this.name = DEFAULT_NAME;
this.ping = null;
setRule(DEFAULT_RULE);
setupPingTask();
lbStats = new LoadBalancerStats(DEFAULT_NAME);
}
public void setRule(IRule rule) {
if (rule != null) {
this.rule = rule;
} else {
/* default rule */
this.rule = new RoundRobinRule();
}
if (this.rule.getLoadBalancer() != this) {
this.rule.setLoadBalancer(this);
}
}
void setupPingTask() {
if (canSkipPing()) {
return;
}
if (lbTimer != null) {
lbTimer.cancel();
}
lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
true);
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
forceQuickPing();
}
DynamicServerListLoadBalancer
DynamicServerListLoadBalancer 类继承于BaseLoadBalancer类,它是对基础负载均衡器的扩展。
- ServerList
在DynamicServerListLoadBalancer的成员变量中有一个关于服务列表的操作对象ServerList serverListImpl,其中泛型T从类名中对于T的限定DynamicServerListLoadBalancer可以获知它是一个Server的子类。
public interface ServerList<T extends Server> {
//用于获取初始化的服务实例清单
public List<T> getInitialListOfServers();
//用于获取更新的服务实例清单
public List<T> getUpdatedListOfServers();
}
我们发现在DynamicServerListLoadBalancer类中updateListOfServers()方法中调用了getUpdatedListOfServers()函数,
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
然后我们再来看这个getUpdatedListOfServers都有哪些实现。
从上图中我们可以看到有多个ServerList的实现类,那么在DynamicServerListLoadBalancer中的ServerList默认配置到底使用了哪个具体实现呢?既然在该负载均衡器中需要实现服务实例的动态更新,那么势必需要Ribbon具备访问Eurake来获取服务实例的能力,所以我们从SpringCloud整合Ribbon与Eurake的包org.springframework.cloud.netflix.ribbon.eureka下可以找到配置类EurekaRibbonClientConfiguration,在该类中可以找到如下创建ServerList实例的内容
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config, Provider<EurekaClient> eurekaClientProvider) {
if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
return this.propertiesFactory.get(ServerList.class, config, serviceId);
}
DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
config, eurekaClientProvider);
DomainExtractingServerList serverList = new DomainExtractingServerList(
discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}
不难发现,这里创建了一个DomainExtractingServerList 实例,在DomainExtractingServerList类中我们可以看到它定义了一个ServerList list,同时DomainExtractingServerList类中对getInitialListOfServers()和getUpdatedListOfServers()的具体实现,其实是委托给了内部定义的ServerList对象,而该对象是通过创建DomainExtractingServerList 时由构造函数传入的DiscoveryEnabledNIWSServerList 实现的。
public class DomainExtractingServerList implements ServerList<DiscoveryEnabledServer> {
private ServerList<DiscoveryEnabledServer> list;
private final RibbonProperties ribbon;
private boolean approximateZoneFromHostname;
public DomainExtractingServerList(ServerList<DiscoveryEnabledServer> list,
IClientConfig clientConfig, boolean approximateZoneFromHostname) {
this.list = list;
this.ribbon = RibbonProperties.from(clientConfig);
this.approximateZoneFromHostname = approximateZoneFromHostname;
}
@Override
public List<DiscoveryEnabledServer> getInitialListOfServers() {
List<DiscoveryEnabledServer> servers = setZones(this.list
.getInitialListOfServers());
return servers;
}
@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
List<DiscoveryEnabledServer> servers = setZones(this.list
.getUpdatedListOfServers());
return servers;
}
...
}
那就来看看DiscoveryEnabledNIWSServerList是如果实现这两个服务实例获取的
@Override
public List<DiscoveryEnabledServer> getInitialListOfServers(){
return obtainServersViaDiscovery();
}
@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
return obtainServersViaDiscovery();
}
可以发现是obtainServersViaDiscovery()这个私有函数通过服务发现机制来实现服务实例的获取。
- ServerListFilter
我们来看一下DynamicServerListLoadBalancer类中ServerListFilter,在updateListOfServers函数中进行了条件不为空的判断。
volatile ServerListFilter<T> filter;
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};
@VisibleForTesting
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
ServerListFilter接口非常简单,主要用来实现对服务实例列表的过滤,来看看ServerListFilter接口的源码如下:
public interface ServerListFilter<T extends Server> {
public List<T> getFilteredListOfServers(List<T> servers);
}
再来看看这个接口有哪些具体的实现
1、AbstractServerListFilter:这是一个抽象过滤器,在这里定义了过滤是需要的一个重要依据对象LoadBalancerStats,
public abstract class AbstractServerListFilter<T extends Server> implements ServerListFilter<T> {
private volatile LoadBalancerStats stats;
public void setLoadBalancerStats(LoadBalancerStats stats) {
this.stats = stats;
}
public LoadBalancerStats getLoadBalancerStats() {
return stats;
}
}
2、ZoneAffinityServerListFilter:该过滤器基于“区域感知(Zone Affinity)”的方式实现服务实例的过滤,也就是说,它会根据提供服务的实例所处的区域(Zone)与消费者自身的所处区域(Zone)进行比较,过滤掉那些不是同处一个区域的实例。
@Override
public List<T> getFilteredListOfServers(List<T> servers) {
if (zone != null && (zoneAffinity || zoneExclusive) && servers !=null && servers.size() > 0){
List<T> filteredServers = Lists.newArrayList(Iterables.filter(
servers, this.zoneAffinityPredicate.getServerOnlyPredicate()));
if (shouldEnableZoneAffinity(filteredServers)) {
return filteredServers;
} else if (zoneAffinity) {
overrideCounter.increment();
}
}
return servers;
}
- ServerListUpdater
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};
protected volatile ServerListUpdater serverListUpdater;
这个对象实现的是对ServerList的更新,在ServerListUpdater内部还定义了一个UpdateAction接口。上面定义的updateAction对象就是以匿名内部类的方式创建了一个它的具体实现,其中doUpdate实现的内容就是对ServerList的具体更新操作。
public interface ServerListUpdater {
//updateAction的接口,它实际执行服务实例列表更新
public interface UpdateAction {
void doUpdate();
}
//启动服务更新器,传入的UpdateAction对象为更新操作的具体实现
void start(UpdateAction updateAction);
//停止服务更新器
void stop();
//获取最近的更新时间戳
String getLastUpdate();
//返回自上次更新以来经过的毫秒数
long getDurationSinceLastUpdateMs();
//返回错过的更新周期数
int getNumberMissedCycles();
//返回核心线程数
int getCoreThreads();
}
而ServerListUpdate的实现类不多
- PollingServerListUpdater:动态服务列表更新的默认策略,也就是说,DynamicServerListLoadBalancer负载均衡器中国的默认实现就是它,它通过定时任务的方式进行服务列表的更新
- EurekaNotificationServerListUpdater:该更新器也可服务于DynamicServerListLoadBalancer负载均衡器,但是它的触发机制与PollingServerListUpdater不同,它需要利用Eureka的事件监听器来驱动服务列表的更新操作。
来看一下用于启动“服务更新器”的start函数源码看起
@Override
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
};
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs,
refreshIntervalMs,
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}
以定时任务的方式进行服务列表的更新,它先创建一个Runnable的线程实现,在该实现中调用了上面提到的具体更新服务实例列表的方法updateAction.doUpdate(),最后再为这个Runnable线程实现启动一个定时任务来实现。
我们在这个 start(final UpdateAction updateAction)函数中可以看到两个重要的参数initialDelayMs和refreshIntervalMs,默认定义分别为1000和10*1000,单位毫秒。也就是说,更新服务实例在初始化之后延迟1秒后开始执行,30s为周期。
private static long LISTOFSERVERS_CACHE_UPDATE_DELAY = 1000; // msecs;
private static int LISTOFSERVERS_CACHE_REPEAT_INTERVAL = 30 * 1000; // msecs;
public PollingServerListUpdater() {
this(LISTOFSERVERS_CACHE_UPDATE_DELAY, LISTOFSERVERS_CACHE_REPEAT_INTERVAL);
}
public PollingServerListUpdater(final long initialDelayMs, final long refreshIntervalMs) {
this.initialDelayMs = initialDelayMs;
this.refreshIntervalMs = refreshIntervalMs;
}
ZoneAwareLoadBalancer
ZoneAwareLoadBalancer负载均衡器是对DynamicServerListLoadBalancer的扩展。在DynamicServerListLoadBalancer没有重写选择具体服务实例的chooseServer函数,它依然是采用BaseLoadBalancer中实现的算法,使用RoundRobbinRule规则,以线性轮询的方式来选择调用的服务实例,该算法没有区域(Zone)的概念,它会把所有实现视为一个Zone下的节点来看待,这样就会周期性地产生跨区域访问的情况,由于跨区域会产生更高的延迟,这些实例主要以防止区域性故障实现高可用为目的而不能作为常规访问的实例,所以在多区域部署的情况下会有一定的性能问题,而该负载均衡则可以避免这样的问题。下面我们就来看看它是怎么实现的。
在ZoneAwareLoadBalancer中,它并没有重写setServersList,说明实现服务实例清单的更新主逻辑没有修改。但是它重写了setServerListForZones(Map<String, List> zoneServersMap)函数。
先来看一下在DynamicServerListLoadBalancer中setServerListForZones函数的实现
@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) {
// make sure ServerStats is created to avoid creating them on hot
// path
getLoadBalancerStats().getSingleServerStat(server);
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);
}
}
setServerListForZones(serversInZones);
}
protected void setServerListForZones(
Map<String, List<Server>> zoneServersMap) {
LOGGER.debug("Setting server list for zones: {}", zoneServersMap);
getLoadBalancerStats().updateZoneServerMapping(zoneServersMap);
}
setServerListForZones函数的调用位于更新服务实例清单函数setServersList(List lsrv)的最后,从其实现的内容来看,在DynamicServerListLoadBalancer中的作用是根据按区域Zone分组的实例列表,为负载均衡器中的LoadBalancerStats对象创建ZoneStats并放入Map zoneStatsMap集合中,每一个区域Zone对应一个ZoneStats,它对于存储每个Zone的一些状态和统计信息。
在ZoneAwareLoadBalancer中对setServerListForZones重写如下:
@Override
protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) {
super.setServerListForZones(zoneServersMap);
if (balancers == null) {
balancers = new ConcurrentHashMap<String, BaseLoadBalancer>();
}
for (Map.Entry<String, List<Server>> entry: zoneServersMap.entrySet()) {
String zone = entry.getKey().toLowerCase();
getLoadBalancer(zone).setServersList(entry.getValue());
}
// check if there is any zone that no longer has a server
// and set the list to empty so that the zone related metrics does not
// contain stale data
for (Map.Entry<String, BaseLoadBalancer> existingLBEntry: balancers.entrySet()) {
if (!zoneServersMap.keySet().contains(existingLBEntry.getKey())) {
existingLBEntry.getValue().setServersList(Collections.emptyList());
}
}
}
在该实现中创建了一个ConcurrentHashMap类型的balancers对象,它将用来存储每个Zone区域对应的负载均衡器。而具体的负载均衡器的创建则是通过在下面的第一个循环中调用getLoadBalancer函数来完成,同时在创建负载均衡器的时候会创建它的规则(如果当前实现中没有IRule的实例,就创建一个AvailabilityFilteringRule规则,如果已经有具体实例就克隆一个)。在创建完负载均衡器后又调用setServersList函数为其设置对应Zone区域的实例清单。遍历balancers对Zone区域中实例清单检查,看看是否有Zone区域下已经没有实例了,是的话就将balancers中对应Zone区域的实例列表清空,该操作的作用是为了后续选择节点时,防止过时的Zone区域统计信息干扰具体实例的选择算法。
接下来再来看看它是如何选择服务实例来实现对区域的识别:
@Override
public Server chooseServer(Object key) {
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
Server server = null;
try {
LoadBalancerStats lbStats = getLoadBalancerStats();
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
if (triggeringLoad == null) {
triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
}
if (triggeringBlackoutPercentage == null) {
triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
}
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
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 {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
}
从源码中我们可以看到,只有当负载均衡器中维护的实例所属的Zone区域的个数大于1的时候才会执行这里的选择策略,否则还是将使用父类的实现,当Zone区域的个数大于1的时候,它的实现步骤如下:
- 调用ZoneAvoidanceRule的静态方法createSnapshot(lbStats),为当前负载均衡器中所有的Zone区域分别创建快照中的数据将用于后续的算法
- 调用ZoneAvoidanceRule的静态方法getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get())来获取可用的Zone区域集合,在该函数中会通过Zone区域快照中的统计数据来实现可用区的挑选。
- 当获得的可用Zone区域集合不为空,并且个数小于Zone区域总数,就随机选择一个Zone区域
- 在确定了整个Zone区域后,则获取了对应Zone区域的服务均衡器,并调用chooseServer来选择具体的服务实例,而在chooseServer中将使用IRule接口的choose函数来选择具体的服务实例。在这里,IRule接口的实现会使用ZoneAvoidanceRule来挑选出具体的服务实例。
这次就写到这里吧。。。