RestTemplate 是 Spring Resources 中一个访问第三方 RESTful API 接口的网络请求框架。RestTemplate 是用来消费 REST 服务的,所以 RestTemplate 的主要方法都与 REST 的 Http协议的一些方法紧密相连,例如 HEAD、GET、POST、PUT、DELETE 和 OPTIONS 等方法,这些方法在 RestTemplate 类对应的方法为 headForHeaders()、getForObject()、postForObject()、put() 和 delete() 等。
RestTemplate 本身是不具备负载均衡的能力的,如果 RestTemplate 未使用 @LoadBalanced 标记,就通过服务名的形式来调用,必然会报错。用 @LoadBalanced 标记后,调用 RestTemplate 的 REST 方法就会通过负载均衡的方式通过一定的策略路由到某个服务实例上,底层负责负载均衡的组件就是 Ribbon。后面我们再来看 @LoadBalanced 是如何让 RestTemplate 具备负载均衡的能力
① 负载均衡
负载均衡是指将负载分摊到多个执行单元上,负载均衡主要可以分为集中式负载均衡与进程内负载均衡:
Ribbon 是一个客户端负载均衡器,可以很好地控制 HTTP 和 TCP 客户端的负载均衡行为。Ribbon 是 Netflix 公司开源的一个负载均衡组件,已经整合到 SpringCloud 生态中,它在 Spring Cloud 生态内是一个不可缺少的组件,少了它,服务便不能横向扩展。
② Ribbon 模块
Ribbon 有很多子模块,官方文档中说明,目前 Netflix 公司主要用于生产环境的 Ribbon 子模块如下:
③ springcloud 与 ribbon 整合
与 eureka 整合到 springcloud 类似,springcloud 提供了对应的
spring-cloud-starter-netflix-eureka-client(server) 依赖包,ribbon 则整合到了 spring-cloud-starter-netflix-ribbon 中。一般也不需要单独引入 ribbon 的依赖包,spring-cloud-starter-netflix-eureka-client 中已经依赖了 spring-cloud-starter-netflix-ribbon。因此我们引入了 spring-cloud-starter-netflix-eureka-client 就可以使用 Ribbon 的功能了。
④ Ribbon 与 RestTemplate 整合使用
在 Spring Cloud 构建的微服务系统中,Ribbon 作为服务消费者的负载均衡器,有两种使用方式,一种是和 RestTemplate 相结合,另一种是和 Feign 相结合。前面已经演示了带有负载均衡的 RestTemplate 的使用,下面用一张图来看看 RestTemplate 基于 Ribbon 的远程调用。
以 RestTemplate 为切入点,来看 Ribbon 的负载均衡核心原理。那么首先就要先看看 @LoadBalanced 注解如何让 RestTemplate 具备负载均衡的能力了。
首先看 @LoadBalanced 这个注解的定义,可以得到如下信息:
注意 @LoadBalanced 是 spring-cloud-commons 模块下 loadbalancer 包下的。
在 @LoadBalanced 同包下,有一个
LoadBalancerAutoConfiguration 自动化配置类,从注释也可以看出,这是客户端负载均衡 Ribbon 的自动化配置类。
从这个自动化配置类可以得到如下信息:
LoadBalancerAutoConfiguration 自动化配置主要就是给 RestTemplate 添加了一个负载均衡拦截器 LoadBalancerInterceptor。从 setInterceptors 的参数可以看出,拦截器的类型是 ClientHttpRequestInterceptor,如果我们想定制化 RestTemplate,就可以实现这个接口来定制化,然后还可以用 @Order 标记拦截器的先后顺序。
interceptors 拦截器是在 RestTemplate 的父类 InterceptingHttpAccessor 中的, RestTemplate 的类结构如下图所示。
从 restTemplate.getForEntity("
http://demo-producer/v1/uuid", String.class) 这个GET请求进去看看,是如何使用 LoadBalancerInterceptor 的。一步步进去,可以看到最终是进入到 doExecute 这个方法了。
在 doExecute 方法中,首先根据 url、method 创建一个 ClientHttpRequest,然后利用 ClientHttpRequest 来发起请求。
InterceptingHttpAccessor 中重写了父类 HttpAccessor 的 getRequestFactory 方法,父类默认的 requestFactory 是
SimpleClientHttpRequestFactory。
重写后的 getRequestFactory 方法中,如果拦截器不为空,则基于父类默认的
SimpleClientHttpRequestFactory 和拦截器创建了 InterceptingClientHttpRequestFactory。
也就是说调用了
InterceptingClientHttpRequestFactory 的 createRequest 方法来创建 ClientHttpRequest。进去可以看到,ClientHttpRequest 的实际类型就是 InterceptingClientHttpRequest。
protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
}
InterceptingClientHttpRequest 的类结构如下:
RestTemplate 的 doExecute 中调用 request.execute() 其实是调用了
InterceptingClientHttpRequest 父类 AbstractClientHttpRequest 中的 execute 方法。一步步进去可以发现最终其实是调用了 InterceptingClientHttpRequest 的 executeInternal 方法。
在
InterceptingClientHttpRequest 的 executeInternal 方法中,创建了 InterceptingRequestExecution 来执行请求。在 InterceptingRequestExecution 的 execute 方法中,会先遍历执行所有拦截器,然后通过 ClientHttpRequest 发起真正的 http 请求。
进入到 LoadBalancerInterceptor 的 intercept 拦截方法内,可以看到从请求的原始地址中获取了服务名称,然后调用了 loadBalancer 的 execute 方法,也就是 LoadBalancerClient。
到这里,其实已经可以想象,loadBalancer.execute 这行代码就是根据服务名称去获取一个具体的实例,然后将原始地址替换为实例的IP地址。那这个 loadBalancer 又是什么呢?
在配置 LoadBalancerInterceptor 时,需要两个参数,LoadBalancerClient 和
LoadBalancerRequestFactory,LoadBalancerRequestFactory前面已经知道是如何创建的了。LoadBalancerClient 又是在哪创建的呢?通过 IDEA 搜索,可以发现是在 spring-cloud-netflix-ribbon 模块下的 RibbonAutoConfiguration 中配置的,可以看到 LoadBalancerClient 的实际类型是 RibbonLoadBalancerClient。
配置类的顺序是
EurekaClientAutoConfiguration、RibbonAutoConfiguration、LoadBalancerAutoConfiguration,因为使 RestTemplate 具备负载均衡的能力需要 LoadBalancerInterceptor 拦截器,创建 LoadBalancerInterceptor 又需要 LoadBalancerClient,而 LoadBalancerClient 底层要根据服务名获取某个实例,肯定又需要一个实例库,比如从配置文件、注册中心获取。从这里就可以看出来,RibbonLoadBalancerClient 默认会从 Eureka 注册中心获取实例。
LoadBalancerClient 主要提供了三个接口:
public interface LoadBalancerClient extends ServiceInstanceChooser {
// 从 LoadBalancer 找一个 Server 来发送请求
T execute(String serviceId, LoadBalancerRequest request) throws IOException;
// 从传入的 ServiceInstance 取 Server 来发送请求
T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException;
// 对原始 URI 重构
URI reconstructURI(ServiceInstance instance, URI original);
}
进入到 RibbonLoadBalancerClient 的 execute 方法中可以看到:
这个 LoadBalancerRequest 其实就是 LoadBalancerInterceptor 的 intercept 中创建的一个匿名类,在它的函数式接口内,主要是用装饰器 ServiceRequestWrapper 将 request 包了一层。
ServiceRequestWrapper 主要就是重写了 getURI 方法,在重写的 getURI 方法内,它用 loadBalancer 对 URI 进行了重构,进去可以发现,就是将原始地址中的服务名替换为 Server 的真实IP、端口地址。
@Override
public URI getURI() {
// 重构 URI
URI uri = this.loadBalancer.reconstructURI(this.instance, getRequest().getURI());
return uri;
}
reconstructURIWithServer:
到这里,我们基本就弄清楚了一个简单的 @LoadBalanced 注解如何让 RestTemplate 具备了负载均衡的能力了,这一节来做个小结。
① RestTemplate 如何获得负载均衡的能力
② 核心组件
这里会涉及多个模块,下面是核心组件的所属模块:
spring-web:
spring-cloud-commons:
spring-cloud-netflix-ribbon:
ribbon-loadbalancer:
③ 最后再用一张图把 RestTemplate 这块的关系捋一下
从前面 RestTemplate 那张图可以看出,使 RestTemplate 具备负载均衡的能力,最重要的一个组件之一就是 ILoadBalancer,因为要用它来获取能调用的 Server,有了 Server 才能对原始带有服务名的 URI 进行重构。这节就来看下 Ribbon 的负载均衡器 ILoadBalancer 是如何创建的以及如何通过它获取 Server。
① SpringClientFactory与上下文
ILoadBalancer 是用 SpringClientFactory 的 getLoadBalancer 方法根据服务名获取的,从 getInstance 一步步进去可以发现,每个服务都会创建一个
AnnotationConfigApplicationContext,也就是一个应用上下文 ApplicationContext。相当于就是一个服务绑定一个 ILoadBalancer。
public C getInstance(String name, Class type) {
C instance = super.getInstance(name, type);
if (instance != null) {
return instance;
}
IClientConfig config = getInstance(name, IClientConfig.class);
return instantiateWithConfig(getContext(name), type, config);
}
public T getInstance(String name, Class type) {
// 根据名称获取
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) {
return context.getBean(type);
}
return null;
}
protected AnnotationConfigApplicationContext getContext(String name) {
// contexts => Map
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
调试看下
AnnotationConfigApplicationContext 上下文,可以看到放入了与这个服务绑定的 ILoadBalancer、IClientConfig、RibbonLoadBalancerContext 等。
它这里为什么要每个服务都绑定一个 ApplicationContext 呢?我猜想应该是因为服务实例列表可以有多个来源,比如可以从 eureka 注册中心获取、可以通过代码配置、可以通过配置文件配置,另外每个服务还可以有很多个性化的配置,有默认的配置、定制的全局配置、个别服务的特定配置等,它这样做就便于用户定制每个服务的负载均衡策略。
② Ribbon的饥饿加载
而且这个Ribbon客户端的应用上下文默认是懒加载的,并不是在启动的时候就加载上下文,而是在第一次调用的时候才会去初始化。
如果想服务启动时就初始化,可以指定Ribbon客户端的具体名称,在启动的时候就加载配置项的上下文:
ribbon:
eager-load:
enabled: true
clients: demo-producer,demo-xxx
在 RibbonAutoConfiguration 配置类中可以找到这个饥饿配置,如果开启了饥饿加载,就会创建
RibbonApplicationContextInitializer 来在启动时初始化上下文。
③ RibbonClientConfiguration
ILoadBalancer 的创建在哪呢?看 RibbonClientConfiguration,这个配置类提供了 ILoadBalancer 的默认创建方法,ILoadBalancer 的默认实现类为 ZoneAwareLoadBalancer。
可以看到创建 ILoadBalancer 需要 IClientConfig、ServerList
这7个核心接口和默认实现类如下:
可以看到在 RibbonClientConfiguration 中创建 IRule、IPing、ServerList
也就是说针对特定的服务,这几个类可以自行定制化,也可以通过配置指定其它的实现类。
① 全局策略配置
如果想要全局更改配置,需要加一个配置类,比如像下面这样:
@Configuration
public class GlobalRibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new RandomRule();
}
@Bean
public IPing ribbonPing() {
return new NoOpPing();
}
}
② 基于注解的配置
如果想针对某一个服务定制配置,可以通过 @RibbonClients 来配置特定服务的配置类。
需要先定义一个服务配置类:
@Configuration
public class ProducerRibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new RandomRule();
}
@Bean
public IPing ribbonPing() {
return new NoOpPing();
}
}
用 @RibbonClients 注解为服务指定特定的配置类,并排除掉,不让 Spring 扫描,否则就变成了全局配置了。
@RibbonClients({
@RibbonClient(name = "demo-producer", configuration = ProducerRibbonConfiguration.class)
})
@ComponentScan(excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ProducerRibbonConfiguration.class)
})
③ 配置文件配置
通过配置文件的方式来配置,配置的格式就是 <服务名称>.ribbon.<属性>:
demo-producer:
ribbon:
# ILoadBalancer
NFLoadBalancerClassName: com.netflix.loadbalancer.NoOpLoadBalancer
# IRule
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
# IPing
NFLoadBalancerPingClassName:
# ServerList
NIWSServerListClassName:
# ServerListFilter
NIWSServerListFilterClassName:
④ 优先级顺序
这几种配置方式的优先级顺序是 配置文件配置 > @RibbonClients 配置 > 全局配置 > 默认配置。
获取到 ILoadBalancer 后,就要去获取 Server 了,可以看到,就是用 ILoadBalancer 来获取 Server。
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
if (loadBalancer == null) {
return null;
}
// Use 'default' on a null hint, or just pass it on?
return loadBalancer.chooseServer(hint != null ? hint : "default");
}
ILoadBalancer 的默认实现类是 ZoneAwareLoadBalancer,进入它的 chooseServer 方法内,如果只配置了一个 zone,就走父类的 chooseServer,否则从多个 zone 中去选择实例。
public Server chooseServer(Object key) {
// ENABLED => ZoneAwareNIWSDiscoveryLoadBalancer.enabled 默认 true
// AvailableZones 配置的只有一个 defaultZone
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
// 走父类获取 Server 的逻辑
return super.chooseServer(key);
}
// 多 zone 逻辑....
}
先看下 ZoneAwareLoadBalancer 的类继承结构,ZoneAwareLoadBalancer 的直接父类是
DynamicServerListLoadBalancer,DynamicServerListLoadBalancer 的父类又是 BaseLoadBalancer。
ZoneAwareLoadBalancer 调用父类的 chooseServer 方法是在 BaseLoadBalancer 中的,进去可以看到,它主要是用 IRule 来选择实例,最终选择实例的策略就交给了 IRule 接口。
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
// IRule
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
IRule 的默认实现类是 ZoneAvoidanceRule,先看下 ZoneAvoidanceRule 的继承结构,ZoneAvoidanceRule 的直接父类是 PredicateBasedRule。
rule.choose 的逻辑在 PredicateBasedRule 中,getPredicate() 返回的是 ZoneAvoidanceRule 创建的一个组合断言 CompositePredicate,就是用这个断言来过滤出可用的 Server,并通过轮询的策略返回一个 Server。
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
// getPredicate() Server断言 => CompositePredicate
// RoundRobin 轮询方式获取实例
Optional server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
在初始化 ZoneAvoidanceRule 配置时,创建了 CompositePredicate,可以看到这个组合断言主要有两个断言,一个是断言 Server 的 zone 是否可用,一个断言 Server 本身是否可用,例如 Server 无法 ping 通。
public void initWithNiwsConfig(IClientConfig clientConfig) {
// 断言 Server 的 zone 是否可用,只有一个 defaultZone 的情况下都是可用的
ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this, clientConfig);
// 断言 Server 是否可用
AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this, clientConfig);
// 封装组合断言
compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
}
private CompositePredicate createCompositePredicate(ZoneAvoidancePredicate p1, AvailabilityPredicate p2) {
// 建造者模式创建断言
return CompositePredicate.withPredicates(p1, p2)
.addFallbackPredicate(p2)
.addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
.build();
}
接着看选择Server的
chooseRoundRobinAfterFiltering,参数 servers 是通过 ILoadBalancer 获取的所有实例,可以看到它其实就是返回了 ILoadBalancer 在内存中缓存的服务所有 Server。这个 Server 从哪来的我们后面再来看。
public List getAllServers() {
// allServerList => List
return Collections.unmodifiableList(allServerList);
}
先对所有实例通过断言过滤掉不可用的 Server,然后是通过轮询的方式获取一个 Server 返回。这就是默认配置下 ILoadBalancer(ZoneAwareLoadBalancer) 通过 IRule(ZoneAvoidanceRule) 选择 Server 的流程了。
public Optional chooseRoundRobinAfterFiltering(List servers, Object loadBalancerKey) {
// 断言获取可用的 Server
List eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
// 通过取模的方式轮询 Server
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}
public List getEligibleServers(List servers, Object loadBalancerKey) {
if (loadBalancerKey == null) {
return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate()));
} else {
List results = Lists.newArrayList();
// 对每个 Server 断言
for (Server server: servers) {
if (this.apply(new PredicateKey(loadBalancerKey, server))) {
results.add(server);
}
}
return results;
}
}
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextIndex.get();
// 模运算取余数
int next = (current + 1) % modulo;
// CAS 更新 nextIndex
if (nextIndex.compareAndSet(current, next) && current < modulo)
return current;
}
}
前面在通过 IRule 选择 Server 的时候,首先通过 lb.getAllServers() 获取了所有的 Server,那这些 Server 从哪里来的呢,这节就来看下。
ILoadBalancer 的默认实现类是 ZoneAwareLoadBalancer,先从 ZoneAwareLoadBalancer 的构造方法进去看看都做了些什么事情。
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList serverList, ServerListFilter serverListFilter,
IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
可以看到,ZoneAwareLoadBalancer 直接调用了父类
DynamicServerListLoadBalancer 的构造方法,DynamicServerListLoadBalancer 先调用父类 BaseLoadBalancer 初始化,然后又做了一些剩余的初始化工作。
public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule,
IPing ping, ServerList serverList, ServerListFilter filter,
ServerListUpdater serverListUpdater) {
// DynamicServerListLoadBalancer
super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
}
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
ServerList serverList, ServerListFilter filter,
ServerListUpdater serverListUpdater) {
// BaseLoadBalancer
super(clientConfig, rule, ping);
this.serverListImpl = serverList;
this.filter = filter;
this.serverListUpdater = serverListUpdater;
if (filter instanceof AbstractServerListFilter) {
((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
}
// 剩余的一些初始化
restOfInit(clientConfig);
}
public BaseLoadBalancer(IClientConfig config, IRule rule, IPing ping) {
// createLoadBalancerStatsFromConfig => LoadBalancerStats 统计
initWithConfig(config, rule, ping, createLoadBalancerStatsFromConfig(config));
看 BaseLoadBalancer 的 initWithConfig,主要做了如下的初始化:
void initWithConfig(IClientConfig clientConfig, IRule rule, IPing ping, LoadBalancerStats stats) {
this.config = clientConfig;
String clientName = clientConfig.getClientName();
this.name = clientName;
// ping 间隔时间,默认30秒
int pingIntervalTime = Integer.parseInt(""
+ clientConfig.getProperty(
CommonClientConfigKey.NFLoadBalancerPingInterval,
Integer.parseInt("30")));
// 没看到用的地方
int maxTotalPingTime = Integer.parseInt(""
+ clientConfig.getProperty(
CommonClientConfigKey.NFLoadBalancerMaxTotalPingTime,
Integer.parseInt("2")));
// 设置 ping 间隔时间,并重新设置了 ping 任务
setPingInterval(pingIntervalTime);
setMaxTotalPingTime(maxTotalPingTime);
// 设置 IRule、IPing
setRule(rule);
setPing(ping);
setLoadBalancerStats(stats);
rule.setLoadBalancer(this);
if (ping instanceof AbstractLoadBalancerPing) {
((AbstractLoadBalancerPing) ping).setLoadBalancer(this);
}
logger.info("Client: {} instantiated a LoadBalancer: {}", name, this);
// PrimeConnections,请求预热,默认关闭
// 作用主要用于解决那些部署环境(如读EC2)在实际使用实时请求之前,从防火墙连接/路径进行预热(比如先加白名单、初始化等等动作比较耗时,可以用它先去打通)。
boolean enablePrimeConnections = clientConfig.get(
CommonClientConfigKey.EnablePrimeConnections, DefaultClientConfigImpl.DEFAULT_ENABLE_PRIME_CONNECTIONS);
if (enablePrimeConnections) {
this.setEnablePrimingConnections(true);
PrimeConnections primeConnections = new PrimeConnections(
this.getName(), clientConfig);
this.setPrimeConnections(primeConnections);
}
// 注册一些监控
init();
}
protected void init() {
Monitors.registerObject("LoadBalancer_" + name, this);
// register the rule as it contains metric for available servers count
Monitors.registerObject("Rule_" + name, this.getRule());
// 默认关闭
if (enablePrimingConnections && primeConnections != null) {
primeConnections.primeConnections(getReachableServers());
}
}
再看下
DynamicServerListLoadBalancer 的初始化,核心的初始化逻辑在 restOfInit 中,主要就是做了两件事情:
void restOfInit(IClientConfig clientConfig) {
boolean primeConnection = this.isEnablePrimingConnections();
// turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
this.setEnablePrimingConnections(false);
// 开启动态更新 Server 的特性
enableAndInitLearnNewServersFeature();
// 更新 Server 列表
updateListOfServers();
// 开启请求预热的情况下,对可用的 Server 进行预热
if (primeConnection && this.getPrimeConnections() != null) {
this.getPrimeConnections()
.primeConnections(getReachableServers());
}
this.setEnablePrimingConnections(primeConnection);
LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
}
先看下 updateListOfServers() 是如何更新 Server 列表的,进而看下 ILoadBalancer 是如何存储 Server 的。
public void updateListOfServers() {
List servers = new ArrayList();
if (serverListImpl != null) {
// 从 ServerList 获取所有 Server 列表
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", getIdentifier(), servers);
if (filter != null) {
// 用 ServerListFilter 过滤 Server
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", getIdentifier(), servers);
}
}
// 更新所有 Server 到本地缓存
updateAllServerList(servers);
}
protected void updateAllServerList(List ls) {
if (serverListUpdateInProgress.compareAndSet(false, true)) {
try {
for (T s : ls) {
s.setAlive(true); // 设置 Server alive
}
setServersList(ls);
// 强制初始化 Ping
super.forceQuickPing();
} finally {
serverListUpdateInProgress.set(false);
}
}
}
public void setServersList(List lsrv) {
// BaseLoadBalancer
super.setServersList(lsrv);
// 将 Server 更新到 LoadBalancerStats 统计中 ....
}
接着看父类的 setServersList,可以看出,存储所有 Server 的数据结构 allServerList 是一个加了 synchronized 的线程安全的容器,setServersList 就是直接将得到的 Server 列表替换 allServerList。
1 public void setServersList(List lsrv) {
2 Lock writeLock = allServerLock.writeLock();
3 ArrayList newServers = new ArrayList();
4 // 加写锁
5 writeLock.lock();
6 try {
7 // for 循环将 lsrv 中的 Server 转移到 allServers
8 ArrayList allServers = new ArrayList();
9 for (Object server : lsrv) {
10 if (server == null) {
11 continue;
12 }
13 if (server instanceof String) {
14 server = new Server((String) server);
15 }
16 if (server instanceof Server) {
17 logger.debug("LoadBalancer [{}]: addServer [{}]", name, ((Server) server).getId());
18 allServers.add((Server) server);
19 } else {
20 throw new IllegalArgumentException("Type String or Server expected, instead found:" + server.getClass());
21 }
22 }
23
24 boolean listChanged = false;
25 // allServerList => volatile List allServerList = Collections.synchronizedList(new ArrayList())
26 if (!allServerList.equals(allServers)) {
27 listChanged = true;
28 // 服务列表变更监听器 ServerListChangeListener, 发出服务变更通知...
29 }
30
31 // 启用了服务预热,开始 Server 预热...
32
33 // 直接替换
34 allServerList = allServers;
35 if (canSkipPing()) {
36 for (Server s : allServerList) {
37 s.setAlive(true);
38 }
39 upServerList = allServerList;
40 } else if (listChanged) {
41 forceQuickPing();
42 }
43 } finally {
44 // 释放写锁
45 writeLock.unlock();
46 }
47 }
前面
chooseRoundRobinAfterFiltering 获取所有 Server 时就是返回的这个 allServerList列表。
1 public List getAllServers() {
2 return Collections.unmodifiableList(allServerList);
3 }
获取 Server 的组件是 ServerList,RibbonClientConfiguration 中配置的默认实现类是
ConfigurationBasedServerList。ConfigurationBasedServerList 默认是从配置文件中获取,可以像下面这样配置服务实例地址,多个 Server 地址用逗号隔开。
1 demo-producer:
2 ribbon:
3 listOfServers: http://10.215.0.92:8010,http://10.215.0.92:8011
但是和 eureka-client 结合后,也就是引入
spring-cloud-starter-netflix-eureka-client 的客户端依赖,它会帮我们引入 spring-cloud-netflix-eureka-client 依赖,这个包中有一个 RibbonEurekaAutoConfiguration 自动化配置类,它通过 @RibbonClients 注解定义了全局的 Ribbon 客户端配置类 为 EurekaRibbonClientConfiguration
1 @Configuration(proxyBeanMethods = false)
2 @EnableConfigurationProperties
3 @ConditionalOnRibbonAndEurekaEnabled
4 @AutoConfigureAfter(RibbonAutoConfiguration.class)
5 @RibbonClients(defaultConfiguration = EurekaRibbonClientConfiguration.class)
6 public class RibbonEurekaAutoConfiguration {
7
8 }
进入
EurekaRibbonClientConfiguration 可以看到:
1 @Configuration(proxyBeanMethods = false)
2 public class EurekaRibbonClientConfiguration {
3 @Value("${ribbon.eureka.approximateZoneFromHostname:false}")
4 private boolean approximateZoneFromHostname = false;
5
6 @RibbonClientName
7 private String serviceId = "client";
8 @Autowired
9 private PropertiesFactory propertiesFactory;
10
11 @Bean
12 @ConditionalOnMissingBean
13 public IPing ribbonPing(IClientConfig config) {
14 if (this.propertiesFactory.isSet(IPing.class, serviceId)) {
15 return this.propertiesFactory.get(IPing.class, config, serviceId);
16 }
17 NIWSDiscoveryPing ping = new NIWSDiscoveryPing();
18 ping.initWithNiwsConfig(config);
19 return ping;
20 }
21
22 @Bean
23 @ConditionalOnMissingBean
24 public ServerList> ribbonServerList(IClientConfig config,
25 Provider eurekaClientProvider) {
26 if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
27 return this.propertiesFactory.get(ServerList.class, config, serviceId);
28 }
29 DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(config, eurekaClientProvider);
30 DomainExtractingServerList serverList = new DomainExtractingServerList(discoveryServerList, config, this.approximateZoneFromHostname);
31 return serverList;
32 }
33 }
DynamicServerListLoadBalancer 中通过 ServerList 的 getUpdatedListOfServers 方法全量获取服务列表,在 eureka-client 环境下,ServerList 默认实现类为 DomainExtractingServerList,那就先看下它的 getUpdatedListOfServers 方法。
可以看出,
DomainExtractingServerList 先用 DomainExtractingServerList 获取服务列表,然后根据 Ribbon 客户端配置重新构造 Server 对象返回。获取服务列表的核心在 DiscoveryEnabledNIWSServerList 中。
1 @Override
2 public List getUpdatedListOfServers() {
3 // list => DiscoveryEnabledNIWSServerList
4 List servers = setZones(this.list.getUpdatedListOfServers());
5 return servers;
6 }
7
8 private List setZones(List servers) {
9 List result = new ArrayList<>();
10 boolean isSecure = this.ribbon.isSecure(true);
11 boolean shouldUseIpAddr = this.ribbon.isUseIPAddrForServer();
12 // 根据客户端配置重新构造 DomainExtractingServer 返回
13 for (DiscoveryEnabledServer server : servers) {
14 result.add(new DomainExtractingServer(server, isSecure, shouldUseIpAddr, this.approximateZoneFromHostname));
15 }
16 return result;
17 }
先看下
DiscoveryEnabledNIWSServerList 的构造初始化:
1 public DiscoveryEnabledNIWSServerList(IClientConfig clientConfig, Provider eurekaClientProvider) {
2 this.eurekaClientProvider = eurekaClientProvider;
3 initWithNiwsConfig(clientConfig);
4 }
5
6 @Override
7 public void initWithNiwsConfig(IClientConfig clientConfig) {
8 // 客户端名称,就是服务名称
9 clientName = clientConfig.getClientName();
10 // vipAddresses 得到的也是客户端名称
11 vipAddresses = clientConfig.resolveDeploymentContextbasedVipAddresses();
12
13 // 其它的一些配置....
14 }
接着看获取实例的 getUpdatedListOfServers,可以看到它的核心逻辑就是根据服务名从 EurekaClient 获取 InstanceInfo 实例列表,然后封装 Server 信息返回。
1 public List getUpdatedListOfServers(){
2 return obtainServersViaDiscovery();
3 }
4
5 private List obtainServersViaDiscovery() {
6 List serverList = new ArrayList();
7 if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
8 return new ArrayList();
9 }
10 // 得到 EurekaClient,实际类型是 CloudEurekaClient,其父类是 DiscoveryClient
11 EurekaClient eurekaClient = eurekaClientProvider.get();
12 if (vipAddresses!=null){
13 // 分割 vipAddresses,默认就是服务名称
14 for (String vipAddress : vipAddresses.split(",")) {
15 // 根据服务名称从 EurekaClient 获取实例信息
16 List listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
17 for (InstanceInfo ii : listOfInstanceInfo) {
18 if (ii.getStatus().equals(InstanceStatus.UP)) {
19 // ...
20 // 根据实例信息 InstanceInfo 创建 Server
21 DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr);
22 serverList.add(des);
23 }
24 }
25 if (serverList.size()>0 && prioritizeVipAddressBasedServers){
26 break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
27 }
28 }
29 }
30 return serverList;
31 }
注意这里的 vipAddress 其实就是服务名:
最后看 EurekaClient 的 getInstancesByVipAddress,到这里就很清楚了,其实就是从 DiscoveryClient 的本地应用 Applications 中根据服务名取出所有的实例列表。
这里就和 Eureka 源码那块衔接上了,eureka-client 全量抓取注册表以及每隔30秒增量抓取注册表,都是合并到本地的 Applications 中。Ribbon 与 Eureka 结合后,Ribbon 获取 Server 就从 DiscoveryClient 的 Applications 中获取 Server 列表了。
1 public List getInstancesByVipAddress(String vipAddress, boolean secure, String region) {
2 // ...
3 Applications applications;
4 if (instanceRegionChecker.isLocalRegion(region)) {
5 // 取本地应用 Applications
6 applications = this.localRegionApps.get();
7 } else {
8 applications = remoteRegionVsApps.get(region);
9 if (null == applications) {
10 return Collections.emptyList();
11 }
12 }
13
14 if (!secure) {
15 // 返回服务名对应的实例
16 return applications.getInstancesByVirtualHostName(vipAddress);
17 } else {
18 return applications.getInstancesBySecureVirtualHostName(vipAddress);
19 }
20 }
DynamicServerListLoadBalancer 初始化时,有个方法还没说,就是 enableAndInitLearnNewServersFeature()。这个方法只是调用 ServerListUpdater 启动了一个 UpdateAction,这个 UpdateAction 又只是调用了一下 updateListOfServers 方法,就是前面讲解过的全量更新 Server 的逻辑。
1 public void enableAndInitLearnNewServersFeature() {
2 serverListUpdater.start(updateAction);
3 }
4
5 protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
6 @Override
7 public void doUpdate() {
8 // 调用 updateListOfServers
9 updateListOfServers();
10 }
11 };
ServerListUpdater 的默认实现类是 PollingServerListUpdater,看下它的 start 方法:
其实就是以固定的频率,每隔30秒调用一下 updateListOfServers 方法,将 DiscoveryClient 中 Applications 中缓存的实例同步到 ILoadBalancer 中的 allServerList 列表中。
1 public synchronized void start(final UpdateAction updateAction) {
2 if (isActive.compareAndSet(false, true)) {
3 final Runnable wrapperRunnable = new Runnable() {
4 @Override
5 public void run() {
6 // ...
7 try {
8 // 执行一次 updateListOfServers
9 updateAction.doUpdate();
10 // 设置最后更新时间
11 lastUpdated = System.currentTimeMillis();
12 } catch (Exception e) {
13 logger.warn("Failed one update cycle", e);
14 }
15 }
16 };
17
18 // 固定频率调度
19 scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
20 wrapperRunnable,
21 initialDelayMs, // 默认 1000
22 refreshIntervalMs, // 默认 30 * 1000
23 TimeUnit.MILLISECONDS
24 );
25 } else {
26 logger.info("Already active, no-op");
27 }
28 }
在创建 ILoadBalancer 时,IPing 还没有看过是如何工作的。在初始化的时候,可以看到,主要就是设置了当前的 ping,然后重新设置了一个调度任务,默认每隔30秒调度一次 PingTask 任务。
1 public void setPing(IPing ping) {
2 if (ping != null) {
3 if (!ping.equals(this.ping)) {
4 this.ping = ping;
5 // 设置 Ping 任务
6 setupPingTask();
7 }
8 } else {
9 this.ping = null;
10 // cancel the timer task
11 lbTimer.cancel();
12 }
13 }
14
15 void setupPingTask() {
16 // ...
17 // 创建一个定时调度器
18 lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name, true);
19 // pingIntervalTime 默认为 30 秒,每隔30秒调度一次 PingTask
20 lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
21 // 立即发起以 Ping
22 forceQuickPing();
23 }
ShutdownEnabledTimer 可以简单了解下,它是继承自 Timer 的,它在创建的时候向 Runtime 注册了一个回调,在 jvm 关闭的时候来取消 Timer 的执行,进而释放资源。
View Code
再来看下 PingTask,PingTask 核心逻辑就是遍历 allServers 列表,使用 IPingStrategy 和 IPing 来判断 Server 是否存活,并更新 Server 的状态,以及将所有存活的 Server 更新到 upServerList 中,upServerList 缓存了所有存活的 Server。
View Code
IPingStrategy 的默认实现类是 SerialPingStrategy,进入可以发现它只是遍历所有 Server,然后用 IPing 判断 Server 是否存活。
1 private static class SerialPingStrategy implements IPingStrategy {
2 @Override
3 public boolean[] pingServers(IPing ping, Server[] servers) {
4 int numCandidates = servers.length;
5 boolean[] results = new boolean[numCandidates];
6
7 for (int i = 0; i < numCandidates; i++) {
8 results[i] = false;
9 try {
10 if (ping != null) {
11 // 使用 IPing 判断 Server 是否存活
12 results[i] = ping.isAlive(servers[i]);
13 }
14 } catch (Exception e) {
15 logger.error("Exception while pinging Server: '{}'", servers[i], e);
16 }
17 }
18 return results;
19 }
20 }
在集成 eureka-client 后,IPing默认实现类是 NIWSDiscoveryPing,看它的 isAlive 方法,其实就是判断对应 Server 的实例 InstanceInfo 的状态是否是 UP 状态,UP状态就表示 Server 存活。
1 public boolean isAlive(Server server) {
2 boolean isAlive = true;
3 if (server!=null && server instanceof DiscoveryEnabledServer){
4 DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server;
5 InstanceInfo instanceInfo = dServer.getInstanceInfo();
6 if (instanceInfo!=null){
7 InstanceStatus status = instanceInfo.getStatus();
8 if (status!=null){
9 // 判断Server对应的实例状态是否是 UP
10 isAlive = status.equals(InstanceStatus.UP);
11 }
12 }
13 }
14 return isAlive;
15 }
① Ribbon 核心工作原理总结
② 下面用一张图来总结下 Ribbon 这块获取Server的核心流程以及对应的核心接口间的关系。
在默认情况下,Ribbon 客户端会从 EurekaClient 获取服务列表,其实就是间接从注册中心读取服务注册信息列表,来达到动态负载均衡的功能。但如果不想从 EurekaClient 读取,可以禁用 Ribbon 的 Eureka 功能。 在 Ribbon 中禁用Eureka功能,可以做如下配置:
1 ribbon:
2 eureka:
3 enabled: false
那 ribbon.eureka.enabled 是如何控制禁用 Eureka 的呢?看
RibbonEurekaAutoConfiguration:
1 @Configuration(proxyBeanMethods = false)
2 @EnableConfigurationProperties
3 @ConditionalOnRibbonAndEurekaEnabled
4 @AutoConfigureAfter(RibbonAutoConfiguration.class)
5 @RibbonClients(defaultConfiguration = EurekaRibbonClientConfiguration.class)
6 public class RibbonEurekaAutoConfiguration {
7
8 }
这个配置类通过 @RibbonClients 指定了默认的客户端配置类为
EurekaRibbonClientConfiguration,但生效的前提是 @ConditionalOnRibbonAndEurekaEnabled,进去可以看到这个条件注解就是判断 Ribbon Eureka 是否启用了,就可以设置 ribbon.eureka.enabled=false 来禁用 RIbbon Eureka。
1 @Target({ ElementType.TYPE, ElementType.METHOD })
2 @Retention(RetentionPolicy.RUNTIME)
3 @Documented
4 // 条件类 OnRibbonAndEurekaEnabledCondition
5 @Conditional(ConditionalOnRibbonAndEurekaEnabled.OnRibbonAndEurekaEnabledCondition.class)
6 public @interface ConditionalOnRibbonAndEurekaEnabled {
7
8 class OnRibbonAndEurekaEnabledCondition extends AllNestedConditions {
9
10 OnRibbonAndEurekaEnabledCondition() {
11 super(ConfigurationPhase.REGISTER_BEAN);
12 }
13
14 // 引入了类:DiscoveryEnabledNIWSServerList
15 @ConditionalOnClass(DiscoveryEnabledNIWSServerList.class)
16 // 存在bean对象:SpringClientFactory
17 @ConditionalOnBean(SpringClientFactory.class)
18 // ribbon.eureka.enabled=true
19 @ConditionalOnProperty(value = "ribbon.eureka.enabled", matchIfMissing = true)
20 static class Defaults {
21
22 }
23
24 // 存在bean对象:EurekaClient
25 @ConditionalOnBean(EurekaClient.class)
26 static class EurekaBeans {
27
28 }
29
30 // eureka.client.enabled=true
31 @ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
32 @ConditionalOnDiscoveryEnabled
33 static class OnEurekaClientEnabled {
34
35 }
36 }
37 }
如果想从其它地方获取服务列表,可以自定义接口实现 ServerList
1 :
2 ribbon:
3 listOfServers: http://10.215.0.92:8010,http://10.215.0.92:8011