Spring Cloud源码分析(二)Ribbon

}

else {

ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod());

delegate.getHeaders().putAll(request.getHeaders());

if (body.length > 0) {

StreamUtils.copy(body, delegate.getBody());

}

return delegate.execute();

}

}

可以看到在创建请求的时候requestFactory.createRequest(request.getURI(), request.getMethod());,这里request.getURI()会调用之前介绍的ServiceRequestWrapper对象中重写的getURI函数。此时,它就会使用RibbonLoadBalancerClient中实现的reconstructURI来组织具体请求的服务实例地址。

public URI reconstructURI(ServiceInstance instance, URI original) {

Assert.notNull(instance, “instance can not be null”);

String serviceId = instance.getServiceId();

RibbonLoadBalancerContext context = this.clientFactory

.getLoadBalancerContext(serviceId);

Server server = new Server(instance.getHost(), instance.getPort());

boolean secure = isSecure(server, serviceId);

URI uri = original;

if (secure) {

uri = UriComponentsBuilder.fromUri(uri).scheme(“https”).build().toUri();

}

return context.reconstructURIWithServer(server, uri);

}

reconstructURI函数中,我们可以看到,它通过ServiceInstance实例对象的serviceId,从SpringClientFactory类的clientFactory对象中获取对应serviceId的负载均衡器的上下文RibbonLoadBalancerContext对象。然后根据ServiceInstance中的信息来构建具体服务实例信息的Server对象,并使用RibbonLoadBalancerContext对象的reconstructURIWithServer函数来构建服务实例的URI。

为了帮助理解,简单介绍一下上面提到的SpringClientFactoryRibbonLoadBalancerContext

  • SpringClientFactory类是一个用来创建客户端负载均衡器的工厂类,该工厂会为每一个不同名的ribbon客户端生成不同的Spring上下文。

  • RibbonLoadBalancerContext类是LoadBalancerContext的子类,该类用于存储一些被负载均衡器使用的上下文内容和Api操作(reconstructURIWithServer就是其中之一)。

reconstructURIWithServer的实现中我们可以看到,它同reconstructURI的定义类似。只是reconstructURI的第一个保存具体服务实例的参数使用了Spring Cloud定义的ServiceInstance,而reconstructURIWithServer中使用了Netflix中定义的Server,所以在RibbonLoadBalancerClient实现reconstructURI时候,做了一次转换,使用ServiceInstance的host和port信息来构建了一个Server对象来给reconstructURIWithServer使用。从reconstructURIWithServer的实现逻辑中,我们可以看到,它从Server对象中获取host和port信息,然后根据以服务名为host的URI对象original中获取其他请求信息,将两者内容进行拼接整合,形成最终要访问的服务实例的具体地址。

public class LoadBalancerContext implements IClientConfigAware {

public URI reconstructURIWithServer(Server server, URI original) {

String host = server.getHost();

int port = server .getPort();

if (host.equals(original.getHost())

&& port == original.getPort()) {

return original;

}

String scheme = original.getScheme();

if (scheme == null) {

scheme = deriveSchemeAndPortFromPartialUri(original).first();

}

try {

StringBuilder sb = new StringBuilder();

sb.append(scheme).append(“😕/”);

if (!Strings.isNullOrEmpty(original.getRawUserInfo())) {

sb.append(original.getRawUserInfo()).append(“@”);

}

sb.append(host);

if (port >= 0) {

sb.append(“:”).append(port);

}

sb.append(original.getRawPath());

if (!Strings.isNullOrEmpty(original.getRawQuery())) {

sb.append(“?”).append(original.getRawQuery());

}

if (!Strings.isNullOrEmpty(original.getRawFragment())) {

sb.append(“#”).append(original.getRawFragment());

}

URI newURI = new URI(sb.toString());

return newURI;

} catch (URISyntaxException e) {

throw new RuntimeException(e);

}

}

}

另外,从RibbonLoadBalancerClientexecute的函数逻辑中,我们还能看到在回调拦截器中,执行具体的请求之后,ribbon还通过RibbonStatsRecorder对象对服务的请求还进行了跟踪记录,这里不再展开说明,有兴趣的读者可以继续研究。

分析到这里,我们已经可以大致理清Spring Cloud中使用Ribbon实现客户端负载均衡的基本脉络。了解了它是如何通过LoadBalancerInterceptor拦截器对RestTemplate的请求进行拦截,并利用Spring Cloud的负载均衡器LoadBalancerClient将以逻辑服务名为host的URI转换成具体的服务实例的过程。同时通过分析LoadBalancerClient的Ribbon实现RibbonLoadBalancerClient,可以知道在使用Ribbon实现负载均衡器的时候,实际使用的还是Ribbon中定义的ILoadBalancer接口的实现,自动化配置会采用ZoneAwareLoadBalancer的实例来进行客户端负载均衡实现。

负载均衡器


通过之前的分析,我们已经对Spring Cloud如何使用Ribbon有了基本的了解。虽然Spring Cloud中定义了LoadBalancerClient为负载均衡器的接口,并且针对Ribbon实现了RibbonLoadBalancerClient,但是它在具体实现客户端负载均衡时,则是通过Ribbon的ILoadBalancer接口实现。在上一节分析时候,我们对该接口的实现结构已经做了一些简单的介绍,下面我们根据ILoadBalancer接口的实现类逐个看看它都是如何实现客户端负载均衡的。

AbstractLoadBalancer

AbstractLoadBalancerILoadBalancer接口的抽象实现。在该抽象类中定义了一个关于服务实例的分组枚举类ServerGroup,它包含了三种不同类型:ALL-所有服务实例、STATUS_UP-正常服务的实例、STATUS_NOT_UP-停止服务的实例;实现了一个chooseServer()函数,该函数通过调用接口中的chooseServer(Object key)实现,其中参数key为null,表示在选择具体服务实例时忽略key的条件判断;最后还定义了两个抽象函数,getServerList(ServerGroup serverGroup)定义了根据分组类型来获取不同的服务实例列表,getLoadBalancerStats()定义了获取LoadBalancerStats对象的方法,LoadBalancerStats对象被用来存储负载均衡器中各个服务实例当前的属性和统计信息,这些信息非常有用,我们可以利用这些信息来观察负载均衡器的运行情况,同时这些信息也是用来制定负载均衡策略的重要依据。

public abstract class AbstractLoadBalancer implements ILoadBalancer {

public enum ServerGroup{

ALL,

STATUS_UP,

STATUS_NOT_UP

}

public Server chooseServer() {

return chooseServer(null);

}

public abstract List getServerList(ServerGroup serverGroup);

public abstract LoadBalancerStats getLoadBalancerStats();

}

BaseLoadBalancer

BaseLoadBalancer类是Ribbon负载均衡器的基础实现类,在该类中定义很多关于均衡负载器相关的基础内容:

@Monitor(name = PREFIX + “AllServerList”, type = DataSourceType.INFORMATIONAL)

protected volatile List allServerList = Collections

.synchronizedList(new ArrayList());

@Monitor(name = PREFIX + “UpServerList”, type = DataSourceType.INFORMATIONAL)

protected volatile List upServerList = Collections

.synchronizedList(new ArrayList());

  • 定义了之前我们提到的用来存储负载均衡器各服务实例属性和统计信息的LoadBalancerStats对象。

  • 定义了检查服务实例是否正常服务的IPing对象,在BaseLoadBalancer中默认为null,需要在构造时注入它的具体实现。

  • 定义了检查服务实例操作的执行策略对象IPingStrategy,在BaseLoadBalancer中默认使用了该类中定义的静态内部类SerialPingStrategy实现。根据源码,我们可以看到该策略采用线性遍历ping服务实例的方式实现检查。该策略在当我们实现的IPing速度不理想,或是Server列表过大时,可能变的不是很为理想,这时候我们需要通过实现IPingStrategy接口并实现pingServers(IPing ping, Server[] servers)函数去扩展ping的执行策略。

private static class SerialPingStrategy implements IPingStrategy {

@Override

public boolean[] pingServers(IPing ping, Server[] servers) {

int numCandidates = servers.length;

boolean[] results = new boolean[numCandidates];

if (logger.isDebugEnabled()) {

logger.debug(“LoadBalancer: PingTask executing [”

  • numCandidates + “] servers configured”);

}

for (int i = 0; i < numCandidates; i++) {

results[i] = false;

try {

if (ping != null) {

results[i] = ping.isAlive(servers[i]);

}

} catch (Throwable t) {

logger.error(“Exception while pinging Server:”

  • servers[i], t);

}

}

return results;

}

}

  • 定义了负载均衡的处理规则IRule对象,从BaseLoadBalancerchooseServer(Object key)的实现源码,我们可以知道负载均衡器实际进行服务实例选择任务是委托给了IRule实例中的choose函数来实现。而在这里,默认初始化了RoundRobinRuleIRule的实现对象。RoundRobinRule实现了最基本且常用的线性负载均衡规则。

public Server chooseServer(Object key) {

if (counter == null) {

counter = createCounter();

}

counter.increment();

if (rule == null) {

return null;

} else {

try {

return rule.choose(key);

} catch (Throwable t) {

return null;

}

}

}

  • 启动ping任务:在BaseLoadBalancer的默认构造函数中,会直接启动一个用于定时检查Server是否健康的任务。该任务默认的执行间隔为:10秒。

  • 实现了ILoadBalancer接口定义的负载均衡器应具备的一系列基本操作:

public void addServers(List newServers) {

if (newServers != null && newServers.size() > 0) {

try {

ArrayList newList = new ArrayList();

newList.addAll(allServerList);

newList.addAll(newServers);

setServersList(newList);

} catch (Exception e) {

logger.error(“Exception while adding Servers”, e);

}

}

}

  • chooseServer(Object key):挑选一个具体的服务实例,在上面介绍IRule的时候,已经做了说明,这里不再赘述。

public void markServerDown(Server server) {

if (server == null) {

return;

}

if (!server.isAlive()) {

return;

}

logger.error(“LoadBalancer: markServerDown called on [”

  • server.getId() + “]”);

server.setAlive(false);

notifyServerStatusChangeListener(singleton(server));

}

public List getReachableServers() {

return Collections.unmodifiableList(upServerList);

}

public List getAllServers() {

return Collections.unmodifiableList(allServerList);

}

DynamicServerListLoadBalancer

DynamicServerListLoadBalancer类继承于BaseLoadBalancer类,它是对基础负载均衡器的扩展。在该负载均衡器中,实现了服务实例清单的在运行期的动态更新能力;同时,它还具备了对服务实例清单的过滤功能,也就是说我们可以通过过滤器来选择性的获取一批服务实例清单。下面我们具体来看看在该类中增加了一些什么内容:

ServerList

DynamicServerListLoadBalancer的成员定义中,我们马上可以发现新增了一个关于服务列表的操作对象:ServerList<T> serverListImpl。其中泛型T从类名中对于T的限定DynamicServerListLoadBalancer<T extends Server>可以获知它是一个Server的子类,即代表了一个具体的服务实例的扩展类。而ServerList接口定义如下所示:

public interface ServerList {

public List getInitialListOfServers();

public List getUpdatedListOfServers();

}

它定义了两个抽象方法:getInitialListOfServers用于获取初始化的服务实例清单,而getUpdatedListOfServers用于获取更新的服务实例清单。那么该接口的实现有哪些呢?通过搜索源码,我们可以整出如下图的结构:

从图中我们可以看到有很多个ServerList的实现类,那么在DynamicServerListLoadBalancer中的ServerList默认配置到底使用了哪个具体实现呢?既然在该负载均衡器中需要实现服务实例的动态更新,那么势必需要ribbon具备访问eureka来获取服务实例的能力,所以我们从Spring Cloud整合ribbon与eureka的包org.springframework.cloud.netflix.ribbon.eureka下探索,可以找到配置类EurekaRibbonClientConfiguration,在该类中可以找到看到下面创建ServerList实例的内容:

@Bean

@ConditionalOnMissingBean

public ServerList<?> ribbonServerList(IClientConfig config) {

DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(

config);

DomainExtractingServerList serverList = new DomainExtractingServerList(

discoveryServerList, config, this.approximateZoneFromHostname);

return serverList;

}

可以看到,这里创建的是一个DomainExtractingServerList实例,从下面它的源码中我们可以看到在它内部还定义了一个ServerList list。同时,DomainExtractingServerList类中对getInitialListOfServersgetUpdatedListOfServers的具体实现,其实委托给了内部定义的ServerList list对象,而该对象是通过创建DomainExtractingServerList时候,由构造函数传入的DiscoveryEnabledNIWSServerList实现的。

public class DomainExtractingServerList implements ServerList {

private ServerList list;

private IClientConfig clientConfig;

private boolean approximateZoneFromHostname;

public DomainExtractingServerList(ServerList list,

IClientConfig clientConfig, boolean approximateZoneFromHostname) {

this.list = list;

this.clientConfig = clientConfig;

this.approximateZoneFromHostname = approximateZoneFromHostname;

}

@Override

public List getInitialListOfServers() {

List servers = setZones(this.list

.getInitialListOfServers());

return servers;

}

@Override

public List getUpdatedListOfServers() {

List servers = setZones(this.list

.getUpdatedListOfServers());

return servers;

}

}

那么DiscoveryEnabledNIWSServerList是如何实现这两个服务实例的获取的呢?我们从源码中可以看到这两个方法都是通过该类中的一个私有函数obtainServersViaDiscovery来通过服务发现机制来实现服务实例的获取。

@Override

public List getInitialListOfServers(){

return obtainServersViaDiscovery();

}

@Override

public List getUpdatedListOfServers(){

return obtainServersViaDiscovery();

}

obtainServersViaDiscovery的实现逻辑如下,主要依靠EurekaClient从服务注册中心中获取到具体的服务实例InstanceInfo列表(EurekaClient的具体实现,我们在分析Eureka的源码时已经做了详细的介绍,这里传入的vipAddress可以理解为逻辑上的服务名,比如“USER-SERVICE”)。接着,对这些服务实例进行遍历,将状态为“UP”(正常服务)的实例转换成DiscoveryEnabledServer对象,最后将这些实例组织成列表返回。

private List obtainServersViaDiscovery() {

List serverList = new ArrayList();

if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {

logger.warn(“EurekaClient has not been initialized yet, returning an empty list”);

return new ArrayList();

}

EurekaClient eurekaClient = eurekaClientProvider.get();

if (vipAddresses!=null){

for (String vipAddress : vipAddresses.split(“,”)) {

List listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(

vipAddress, isSecure, targetRegion);

for (InstanceInfo ii : listOfInstanceInfo) {

if (ii.getStatus().equals(InstanceStatus.UP)) {

// 省略了一些实例信息的加工逻辑

DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr);

des.setZone(DiscoveryClient.getZone(ii));

serverList.add(des);

}

}

if (serverList.size()>0 && prioritizeVipAddressBasedServers){

break;

}

}

}

return serverList;

}

DiscoveryEnabledNIWSServerList中通过EurekaClient从服务注册中心获取到最新的服务实例清单后,返回的List到了DomainExtractingServerList类中,将继续通过setZones函数进行处理,而这里的处理具体内容如下,主要完成将DiscoveryEnabledNIWSServerList返回的List列表中的元素,转换成内部定义的DiscoveryEnabledServer的子类对象DomainExtractingServer,在该对象的构造函数中将为服务实例对象设置一些必要的属性,比如id、zone、isAliveFlag、readyToServe等信息。

private List setZones(List servers) {

List result = new ArrayList<>();

boolean isSecure = this.clientConfig.getPropertyAsBoolean(

CommonClientConfigKey.IsSecure, Boolean.TRUE);

boolean shouldUseIpAddr = this.clientConfig.getPropertyAsBoolean(

CommonClientConfigKey.UseIPAddrForServer, Boolean.FALSE);

for (DiscoveryEnabledServer server : servers) {

result.add(new DomainExtractingServer(server, isSecure, shouldUseIpAddr,

this.approximateZoneFromHostname));

}

return result;

}

ServerListUpdater

通过上面的分析我们已经知道了Ribbon与Eureka整合后,如何实现从Eureka Server中获取服务实例清单。那么它又是如何触发向Eureka Server去获取服务实例清单以及如何在获取到服务实例清单后更新本地的服务实例清单的呢?继续来看DynamicServerListLoadBalancer中的实现内容,我们可以很容易的找到下面定义的关于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 {

public interface UpdateAction {

void doUpdate();

}

// 启动服务更新器,传入的UpdateAction对象为更新操作的具体实现。

void start(UpdateAction updateAction);

// 停止服务更新器

void stop();

// 获取最近的更新时间戳

String getLastUpdate();

// 获取上一次更新到现在的时间间隔,单位为毫秒

long getDurationSinceLastUpdateMs();

// 获取错过的更新周期数

int getNumberMissedCycles();

// 获取核心线程数

int getCoreThreads();

}

ServerListUpdater的实现类不多,具体下图所示。

根据两个类的注释,我们可以很容易的知道它们的作用分别是:

  • PollingServerListUpdater:动态服务列表更新的默认策略,也就是说DynamicServerListLoadBalancer负载均衡器中的默认实现就是它,它通过定时任务的方式进行服务列表的更新。

  • EurekaNotificationServerListUpdater:该更新器也可服务于DynamicServerListLoadBalancer负载均衡器,但是它的触发机制与PollingServerListUpdater不同,它需要利用Eureka的事件监听器来驱动服务列表的更新操作。

下面我们来详细看看它默认实现的PollingServerListUpdater。先从用于启动“服务更新器”的start函数源码看起,具体如下。我们可以看到start函数的实现内容验证了之前提到的:以定时任务的方式进行服务列表的更新。它先创建了一个Runnable的线程实现,在该实现中调用了上面提到的具体更新服务实例列表的方法updateAction.doUpdate(),最后再为这个Runnable的线程实现启动了一个定时任务来执行。

@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”);

}

}

继续看PollingServerListUpdater的其他内容,我们可以找到用于启动定时任务的2个重要参数initialDelayMsrefreshIntervalMs的默认定义分别为1000和30*1000,单位为毫秒。也就是说更新服务实例在初始化之后延迟1秒后开始执行,并以30秒为周期重复执行。除了这些内容之外,我们还能看到它还会记录最后更新时间、是否存活等信息,同时也实现了ServerListUpdater中定义的一些其他操作内容,这些操作相对比较简单,这里不再具体说明,有兴趣的读者可以自己查看源码了解其实现原理。

ServerListFilter

在了解了更新服务实例的定时任务是如何启动的之后,我们继续回到updateAction.doUpdate()调用的具体实现位置,在DynamicServerListLoadBalancer中,它的实际实现委托给了updateListOfServers函数,具体实现如下:

public void updateListOfServers() {

List servers = new ArrayList();

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);

}

可以看到,这里终于用到了我们之前提到的ServerListgetUpdatedListOfServers,通过之前的介绍我们已经可以知道这一步实现了从Eureka Server中获取服务可用实例的列表。在获得了服务实例列表之后,这里又将引入一个新的对象filter,追朔该对象的定义,我们可以找到它是ServerListFilter定义的。

ServerListFilter接口非常简单,该接口中之定义了一个方法List getFilteredListOfServers(List servers),主要用于实现对服务实例列表的过滤,通过传入的服务实例清单,根据一些规则返回过滤后的服务实例清单。该接口的实现如下图所示:

其中,除了ZonePreferenceServerListFilter的实现是Spring Cloud Netflix中对Ribbon的扩展实现外,其他均是Netflix Ribbon中的实现类。我们可以分别看看这些过滤器实现都有什么特点:

  • AbstractServerListFilter:这是一个抽象过滤器,在这里定义了过滤时需要的一个重要依据对象LoadBalancerStats,我们在之前介绍过的,该对象存储了关于负载均衡器的一些属性和统计信息等。

public abstract class AbstractServerListFilter implements ServerListFilter {

private volatile LoadBalancerStats stats;

public void setLoadBalancerStats(LoadBalancerStats stats) {

this.stats = stats;

}

public LoadBalancerStats getLoadBalancerStats() {

return stats;

}

}

  • ZoneAffinityServerListFilter:该过滤器基于“区域感知(Zone Affinity)”的方式实现服务实例的过滤,也就是说它会根据提供服务的实例所处区域(Zone)与消费者自身的所处区域(Zone)进行比较,过滤掉那些不是同处一个区域的实例。

public List getFilteredListOfServers(List servers) {

if (zone != null && (zoneAffinity || zoneExclusive) && servers !=null && servers.size() > 0){

List filteredServers = Lists.newArrayList(Iterables.filter(

servers, this.zoneAffinityPredicate.getServerOnlyPredicate()));

if (shouldEnableZoneAffinity(filteredServers)) {

return filteredServers;

} else if (zoneAffinity) {

overrideCounter.increment();

}

}

return servers;

}

从上面的源码中我们可以看到,对于服务实例列表的过滤是通过Iterables.filter(servers, this.zoneAffinityPredicate.getServerOnlyPredicate())来实现的,其中判断依据由ZoneAffinityPredicate实现服务实例与消费者的Zone比较。而在过滤之后,这里并不会马上返回过滤的结果,而是通过shouldEnableZoneAffinity函数来判断是否要启用“区域感知”的功能,从下面shouldEnableZoneAffinity的实现中,我们可以看到,它使用了LoadBalancerStatsgetZoneSnapshot方法来获取这些过滤后的同区域实例的基础指标(包含了:实例数量、断路器断开数、活动请求数、实例平均负载等),根据一系列的算法求出下面的几个评价值并与设置的阈值对比(下面的为默认值),若有一个条件符合,就不启用“区域感知”过滤的服务实例清单。这一算法实现对于集群出现区域故障时,依然可以依靠其他区域的实例进行正常服务提供了完善的高可用保障。同时,通过这里的介绍,我们也可以关联着来理解之前介绍Eureka的时候提到的对于区域分配设计来保证跨区域故障的高可用问题。

  • blackOutServerPercentage:故障实例百分比(断路器断开数 / 实例数量) >= 0.8

  • activeReqeustsPerServer:实例平均负载 >= 0.6

  • availableServers:可用实例数(实例数量 - 断路器断开数) < 2

private boolean shouldEnableZoneAffinity(List filtered) {

if (!zoneAffinity && !zoneExclusive) {

return false;

}

if (zoneExclusive) {

return true;

}

LoadBalancerStats stats = getLoadBalancerStats();

if (stats == null) {

return zoneAffinity;

} else {

logger.debug(“Determining if zone affinity should be enabled with given server list: {}”, filtered);

ZoneSnapshot snapshot = stats.getZoneSnapshot(filtered);

double loadPerServer = snapshot.getLoadPerServer();

int instanceCount = snapshot.getInstanceCount();

int circuitBreakerTrippedCount = snapshot.getCircuitTrippedCount();

if (((double) circuitBreakerTrippedCount) / instanceCount >= blackOutServerPercentageThreshold.get()

|| loadPerServer >= activeReqeustsPerServerThreshold.get()

|| (instanceCount - circuitBreakerTrippedCount) < availableServersThreshold.get()) {

logger.debug(“zoneAffinity is overriden. blackOutServerPercentage: {}, activeReqeustsPerServer: {}, availableServers: {}”,

new Object[] {(double) circuitBreakerTrippedCount / instanceCount, loadPerServer, instanceCount - circuitBreakerTrippedCount});

return false;

} else {

return true;

}

}

}

  • DefaultNIWSServerListFilter:该过滤器完全继承自ZoneAffinityServerListFilter,是默认的NIWS(Netflix Internal Web Service)过滤器。

  • ServerListSubsetFilter:该过滤器也继承自ZoneAffinityServerListFilter,它非常适用于拥有大规模服务器集群(上百或更多)的系统。因为它可以产生一个“区域感知”结果的子集列表,同时它还能够通过比较服务实例的通信失败数量和并发连接数来判定该服务是否健康来选择性的从服务实例列表中剔除那些相对不够健康的实例。该过滤器的实现主要分为三步:

  1. 获取“区域感知”的过滤结果,来作为候选的服务实例清单

  2. 从当前消费者维护的服务实例子集中剔除那些相对不够健康的实例(同时也将这些实例从候选清单中剔除,防止第三步的时候又被选入),不够健康的标准如下:

a. 服务实例的并发连接数超过客户端配置的值,默认为0,配置参数为:<clientName>.<nameSpace>.ServerListSubsetFilter.eliminationConnectionThresold

b. 服务实例的失败数超过客户端配置的值,默认为0,配置参数为:<clientName>.<nameSpace>.ServerListSubsetFilter.eliminationFailureThresold

c. 如果按符合上面任一规则的服务实例剔除后,剔除比例小于客户端默认配置的百分比,默认为0.1(10%),配置参数为:<clientName>.<nameSpace>.ServerListSubsetFilter.forceEliminatePercent。那么就先对剩下的实例列表进行健康排序,再开始从最不健康实例进行剔除,直到达到配置的剔除百分比。

  1. 在完成剔除后,清单已经少了至少10%(默认值)的服务实例,最后通过随机的方式从候选清单中选出一批实例加入到清单中,以保持服务实例子集与原来的数量一致,而默认的实例子集数量为20,其配置参数为:<clientName>.<nameSpace>.ServerListSubsetFilter.size
  • ZonePreferenceServerListFilter:Spring Cloud整合时新增的过滤器。若使用Spring Cloud整合Eureka和Ribbon时会默认使用该过滤器。它实现了通过配置或者Eureka实例元数据的所属区域(Zone)来过滤出同区域的服务实例。如下面的源码所示,它的实现非常简单,首先通过父类ZoneAffinityServerListFilter的过滤器来获得“区域感知”的服务实例列表,然后遍历这个结果取出根据消费者配置预设的区域Zone来进行过滤,如果过滤的结果是空的就直接返回父类获取的结果,如果不为空就返回通过消费者配置的Zone过滤后的结果。

@Override

public List getFilteredListOfServers(List servers) {

List output = super.getFilteredListOfServers(servers);

if (this.zone != null && output.size() == servers.size()) {

List local = new ArrayList();

for (Server server : output) {

if (this.zone.equalsIgnoreCase(server.getZone())) {

local.add(server);

}

}

if (!local.isEmpty()) {

return local;

}

}

return output;

}

ZoneAwareLoadBalancer

ZoneAwareLoadBalancer负载均衡器是对DynamicServerListLoadBalancer的扩展。在DynamicServerListLoadBalancer中,我们可以看到它并没有重写选择具体服务实例的chooseServer函数,所以它依然会采用在BaseLoadBalancer中实现的算法,使用RoundRobinRule规则,以线性轮询的方式来选择调用的服务实例,该算法实现简单并没有区域(Zone)的概念,所以它会把所有实例视为一个Zone下的节点来看待,这样就会周期性的产生跨区域(Zone)访问的情况,由于跨区域会产生更高的延迟,这些实例主要以防止区域性故障实现高可用为目的而不能作为常规访问的实例,所以在多区域部署的情况下会有一定的性能问题,而该负载均衡器则可以避免这样的问题。那么它是如何实现的呢?

首先,在ZoneAwareLoadBalancer中,我们可以发现,它并没有重写setServersList,说明实现服务实例清单的更新主逻辑没有修改。但是我们可以发现它重写了这个函数:setServerListForZones(Map<String, List<Server>> zoneServersMap)。看到这里可能会有一些陌生,因为它并不是接口中定义的必备函数,所以我们不妨去父类DynamicServerListLoadBalancer中寻找一下该函数,我们可以找到下面的定义了:

public void setServersList(List lsrv) {

super.setServersList(lsrv);

List serverList = (List) lsrv;

Map<String, List> serversInZones = new HashMap<String, List>();

setServerListForZones(serversInZones);

最后

ActiveMQ消息中间件面试专题

  • 什么是ActiveMQ?
  • ActiveMQ服务器宕机怎么办?
  • 丢消息怎么办?
  • 持久化消息非常慢怎么办?
  • 消息的不均匀消费怎么办?
  • 死信队列怎么办?
  • ActiveMQ中的消息重发时间间隔和重发次数吗?

ActiveMQ消息中间件面试专题解析拓展:

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


redis面试专题及答案

  • 支持一致性哈希的客户端有哪些?
  • Redis与其他key-value存储有什么不同?
  • Redis的内存占用情况怎么样?
  • 都有哪些办法可以降低Redis的内存使用情况呢?
  • 查看Redis使用情况及状态信息用什么命令?
  • Redis的内存用完了会发生什么?
  • Redis是单线程的,如何提高多核CPU的利用率?

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


Spring面试专题及答案

  • 谈谈你对 Spring 的理解
  • Spring 有哪些优点?
  • Spring 中的设计模式
  • 怎样开启注解装配以及常用注解
  • 简单介绍下 Spring bean 的生命周期

Spring面试答案解析拓展

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


高并发多线程面试专题

  • 现在有线程 T1、T2 和 T3。你如何确保 T2 线程在 T1 之后执行,并且 T3 线程在 T2 之后执行?
  • Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。
  • Java 中 wait 和 sleep 方法有什么区别?
  • 如何在 Java 中实现一个阻塞队列?
  • 如何在 Java 中编写代码解决生产者消费者问题?
  • 写一段死锁代码。你在 Java 中如何解决死锁?

高并发多线程面试解析与拓展

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM


jvm面试专题与解析

  • JVM 由哪些部分组成?
  • JVM 内存划分?
  • Java 的内存模型?
  • 引用的分类?
  • GC什么时候开始?

JVM面试专题解析与拓展!

BAT面试文档:ActiveMQ+redis+Spring+高并发多线程+JVM

nZones);

最后

ActiveMQ消息中间件面试专题

  • 什么是ActiveMQ?
  • ActiveMQ服务器宕机怎么办?
  • 丢消息怎么办?
  • 持久化消息非常慢怎么办?
  • 消息的不均匀消费怎么办?
  • 死信队列怎么办?
  • ActiveMQ中的消息重发时间间隔和重发次数吗?

ActiveMQ消息中间件面试专题解析拓展:

[外链图片转存中…(img-yFffpGSj-1714456072805)]


redis面试专题及答案

  • 支持一致性哈希的客户端有哪些?
  • Redis与其他key-value存储有什么不同?
  • Redis的内存占用情况怎么样?
  • 都有哪些办法可以降低Redis的内存使用情况呢?
  • 查看Redis使用情况及状态信息用什么命令?
  • Redis的内存用完了会发生什么?
  • Redis是单线程的,如何提高多核CPU的利用率?

[外链图片转存中…(img-MCimqQHO-1714456072805)]


Spring面试专题及答案

  • 谈谈你对 Spring 的理解
  • Spring 有哪些优点?
  • Spring 中的设计模式
  • 怎样开启注解装配以及常用注解
  • 简单介绍下 Spring bean 的生命周期

Spring面试答案解析拓展

[外链图片转存中…(img-acqrsGX3-1714456072805)]


高并发多线程面试专题

  • 现在有线程 T1、T2 和 T3。你如何确保 T2 线程在 T1 之后执行,并且 T3 线程在 T2 之后执行?
  • Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。
  • Java 中 wait 和 sleep 方法有什么区别?
  • 如何在 Java 中实现一个阻塞队列?
  • 如何在 Java 中编写代码解决生产者消费者问题?
  • 写一段死锁代码。你在 Java 中如何解决死锁?

高并发多线程面试解析与拓展

[外链图片转存中…(img-7iJDl4ZN-1714456072806)]


jvm面试专题与解析

  • JVM 由哪些部分组成?
  • JVM 内存划分?
  • Java 的内存模型?
  • 引用的分类?
  • GC什么时候开始?

JVM面试专题解析与拓展!

[外链图片转存中…(img-HZPPBs88-1714456072806)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值