Spring Cloud源码分析(二,面试经历分享

public interface ServiceInstance {

String getServiceId();

String getHost();

int getPort();

boolean isSecure();

URI getUri();

Map<String, String> getMetadata();

}

而上面提到的具体包装Server服务实例的RibbonServer对象就是ServiceInstance接口的实现,可以看到它除了包含了Server对象之外,还存储了服务名、是否使用https标识以及一个Map类型的元数据集合。

protected static class RibbonServer implements ServiceInstance {

private final String serviceId;

private final Server server;

private final boolean secure;

private Map<String, String> metadata;

protected RibbonServer(String serviceId, Server server) {

this(serviceId, server, false, Collections.<String, String> emptyMap());

}

protected RibbonServer(String serviceId, Server server, boolean secure,

Map<String, String> metadata) {

this.serviceId = serviceId;

this.server = server;

this.secure = secure;

this.metadata = metadata;

}

// 省略实现ServiceInstance的一些获取Server信息的get函数

}

那么apply(final ServiceInstance instance)函数,在接收到了具体ServiceInstance实例后,是如何通过LoadBalancerClient接口中的reconstructURI操作来组织具体请求地址的呢?

@Override

public ClientHttpResponse apply(final ServiceInstance instance)

throws Exception {

HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance);

return execution.execute(serviceRequest, body);

}

apply的实现中,我们可以看到它具体执行的时候,还传入了ServiceRequestWrapper对象,该对象继承了HttpRequestWrapper并重写了getURI函数,重写后的getURI会通过调用LoadBalancerClient接口的reconstructURI函数来重新构建一个URI来进行访问。

private class ServiceRequestWrapper extends HttpRequestWrapper {

private final ServiceInstance instance;

@Override

public URI getURI() {

URI uri = LoadBalancerInterceptor.this.loadBalancer.reconstructURI(

this.instance, getRequest().getURI());

return uri;

}

}

LoadBalancerInterceptor拦截器中,ClientHttpRequestExecution的实例具体执行execution.execute(serviceRequest, body)时,会调用InterceptingClientHttpRequestInterceptingRequestExecution类的execute函数,具体实现如下:

public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {

if (this.iterator.hasNext()) {

ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();

return nextInterceptor.intercept(request, body, this);

}

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

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

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

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

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

@Override

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-CZSwN7K3-1710841361025)]
[外链图片转存中…(img-ObL5cSRg-1710841361026)]
[外链图片转存中…(img-j41A4u4P-1710841361026)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-2q9ZpWZu-1710841361026)]

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值