系列文章:
SpringCloud 源码系列(1)— 注册中心Eureka 之 启动初始化
SpringCloud 源码系列(2)— 注册中心Eureka 之 服务注册、续约
SpringCloud 源码系列(3)— 注册中心Eureka 之 抓取注册表
SpringCloud 源码系列(4)— 注册中心Eureka 之 服务下线、故障、自我保护机制
SpringCloud 源码系列(5)— 注册中心Eureka 之 EurekaServer集群
SpringCloud 源码系列(6)— 注册中心Eureka 之 总结篇
SpringCloud 源码系列(7)— 负载均衡Ribbon 之 RestTemplate
负载均衡器 ILoadBalancer
从上一篇 RestTemplate 负载均衡的原理中了解到,使 RestTemplate 具备负载均衡的能力,最重要的一个组件之一就是负载均衡器 ILoadBalancer,因为要用它来获取能调用的 Server,有了 Server 才能对原始带有服务名的 URI 进行重构。这节就来看下 Ribbon 负载均衡器 ILoadBalancer 是如何创建的以及如何通过它获取 Server。
RibbonLoadBalancerClient:
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
// 从 SpringClientFactory 中获取负载均衡器 ILoadBalancer
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
// 利用负载均衡器获取 Server
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
SpringClientFactory与上下文
从上一篇文章已经了解到,RibbonLoadBalancerClient 执行负载均衡请求时,通过 SpringClientFactory 来获取 ILoadBalancer。从 getInstance 一步步进去,最后会进入到 NamedContextFactory 中,从 getContext 方法可以发现,不同的服务都会创建一个 AnnotationConfigApplicationContext,也就是一个应用上下文 ApplicationContext。也就是说每个服务都有自己的一个上下文环境,会绑定不同的 ILoadBalancer。
RibbonLoadBalancerClient:
protected ILoadBalancer getLoadBalancer(String serviceId) {
return this.clientFactory.getLoadBalancer(serviceId);
}
SpringClientFactory:
public ILoadBalancer getLoadBalancer(String name) {
// 获取 ILoadBalancer
return getInstance(name, ILoadBalancer.class);
}
public <C> C getInstance(String name, Class<C> type) {
// 从父类 NamedContextFactory 中获取
C instance = super.getInstance(name, type);
if (instance != null) {
return instance;
}
IClientConfig config = getInstance(name, IClientConfig.class);
return instantiateWithConfig(getContext(name), type, config);
}
NamedContextFactory:
/**
* name - 服务名称
* type - 要获取的 bean 的类型
*/
public <T> T getInstance(String name, Class<T> 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<String, AnnotationConfigApplicationContext>
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 注册中心获取、可以通过代码配置、可以通过配置文件配置,另外每个服务还可以有很多个性化的配置,有默认的配置、定制的全局配置、个别服务的特定配置等,它这样做就便于用户定制每个服务的负载均衡策略。
创建 ILoadBalancer
ILoadBalancer 的创建在哪呢?看 RibbonClientConfiguration,这个配置类提供了 ILoadBalancer 的默认创建方法,可以看到 ILoadBalancer 的默认实现类为 ZoneAwareLoadBalancer。
public class RibbonClientConfiguration {
public static final int DEFAULT_CONNECT_TIMEOUT = 1000;
public static final int DEFAULT_READ_TIMEOUT = 1000;
public static final boolean DEFAULT_GZIP_PAYLOAD = true;
@RibbonClientName
private String name = "client";
@Autowired
private PropertiesFactory propertiesFactory;
@Bean
@ConditionalOnMissingBean
public IClientConfig ribbonClientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.loadProperties(this.name);
// 可以看到默认的连接超时和读取超时时间都是 1 秒
config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
return config;
}
@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
if (this.propertiesFactory.isSet(IRule.class, name)) {
return this.propertiesFactory.get(IRule.class, config, name);
}
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
rule.initWithNiwsConfig(config);
return rule;
}
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
if (this.propertiesFactory.isSet(IPing.class, name)) {
return this.propertiesFactory.get(IPing.class, config, name);
}
return new DummyPing();
}
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("unchecked")
public ServerList<Server> ribbonServerList(IClientConfig config) {
if (this.propertiesFactory.isSet(ServerList.class, name)) {
return this.propertiesFactory.get(ServerList.class, config, name);
}
ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
serverList.initWithNiwsConfig(config);
return serverList;
}
@Bean
@ConditionalOnMissingBean
public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
return new PollingServerListUpdater(config);
}
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("unchecked")
public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
if (this.propertiesFactory.isSet(ServerListFilter.class, name)) {
return this.propertiesFactory.get(ServerListFilter.class, config, name);
}
ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
filter.initWithNiwsConfig(config);
return filter;
}
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> 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);
}
}
可以看到创建 ILoadBalancer 需要 IClientConfig、ServerList<Server>、ServerListFilter<Server>、IRule、IPing、ServerListUpdater,其实这6个接口加上 ILoadBalancer 就是 Ribbon 的核心接口,它们共同定义了 Ribbon 的行为特性。
从 RibbonClientConfiguration 可以知道这7个核心接口以及默认实现类:
ILoadBalancer 选择 Server
获取到 ILoadBalancer 后,就要去获取 Server 了。从 getServer 方法可以看到,RibbonLoadBalancerClient 就是用 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 (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 轮询选择 Server
IRule 的默认实现类是 ZoneAvoidanceRule,先看下 ZoneAvoidanceRule 的继承结构,ZoneAvoidanceRule 的直接父类是 PredicateBasedRule。
rule.choose 的逻辑在 PredicateBasedRule 中,getPredicate() 返回的是 ZoneAvoidanceRule 创建的一个组合断言 CompositePredicate,就是用这个断言来过滤出可用的 Server,并通过轮询的策略返回一个 Server。而 Server 列表的来源是通过 ILoadBalancer 的 getAllServers() 方法来获取的。
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
// getPredicate() Server断言 => CompositePredicate
// RoundRobin 轮询方式获取实例
// 参数 servers => 通过 lb 负载均衡器获取所有 Server
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
从这里也可以了解到 ILoadBalancer 和 IRule 的关系,ILoadBalancer 负责获取 Server 列表,而 IRule 是负责通过一定策略从 Server 列表中选出一个 Server。
在初始化 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<Server> getAllServers() {
// allServerList => List<Server>
return Collections.unmodifiableList(allServerList);
}
先对所有实例通过断言过滤掉不可用的 Server,然后是通过轮询的方式获取一个 Server 返回。这就是默认配置下 ILoadBalancer(ZoneAwareLoadBalancer) 通过 IRule(ZoneAvoidanceRule) 选择 Server 的机制了,默认采用的是轮询的策略获取 Server。
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
// 断言获取可用的 Server
List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
// 通过取模的方式轮询 Server
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
if (loadBalancerKey == null) {
return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate()));
} else {
List<Server> 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;
}
}
Ribbon 整合EurekaClient拉取Server列表
前面在通过 IRule 选择 Server 的时候,首先通过 lb.getAllServers() 获取了所有的 Server,那这些 Server 从哪里来的呢,这节就来看下。
ILoadBalancer 初始化
ILoadBalancer 的默认实现类是 ZoneAwareLoadBalancer,先从 ZoneAwareLoadBalancer 的构造方法进去看看都做了些什么事情。
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> 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<T> serverList, ServerListFilter<T> filter,
ServerListUpdater serverListUpdater) {
// DynamicServerListLoadBalancer
super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
}
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
ServerList<T> serverList, ServerListFilter<T> 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,主要做了如下的初始化:
- 设置 IPing 和 IRule,ping 的间隔时间是
30秒,setPing会启动一个后台定时任务,然后每隔30秒运行一次PingTask任务。 - 设置了 ILoadBalancer 的 统计器
LoadBalancerStats,对 ILoadBalancer 的 Server 状态进行统计,比如连接失败、成功、熔断等信息。 - 在启用 PrimeConnections 请求预热的情况下,创建 PrimeConnections 来预热客户端 与 Server 的链接。默认是关闭的。
- 最后是注册了一些监控、开启请求预热。
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);
// 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 中,主要就是做了两件事情:
- 开启动态更新 Server 的特性,比如实例上线、下线、故障等,要能够更新 ILoadBalancer 的 Server 列表。
- 然后就全量更新一次本地的 Server 列表。
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);
}
全量更新Server列表
先看下 updateListOfServers() 是如何更新 Server 列表的,进而看下 ILoadBalancer 是如何存储 Server 的。
- 首先使用
ServerList获取所有的 Server 列表,在RibbonClientConfiguration中配置的是ConfigurationBasedServerList,但和 eureka 继承后,就不是 ConfigurationBasedServerList 了,这块下一节再来看。 - 然后使用
ServerListFilter对 Server 列表过滤,其默认实现类是ZonePreferenceServerListFilter,它主要是过滤出当前 Zone(defaultZone)下的 Server。 - 最后就是更新所有 Server 列表,先是设置
Server alive,然后调用父类(BaseLoadBalancer)的setServersList来更新Server列表,这说明 Server 是存储在BaseLoadBalancer里的。
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
// 从 ServerList 获取所有 Server 列表
servers = serverListImpl.getUpdatedListOfServers();
if (filter != null) {
// 用 ServerListFilter 过滤 Server
servers = filter.getFilteredListOfServers(servers);
}
}
// 更新所有 Server 到本地缓存
updateAllServerList(servers);
}
protected void updateAllServerList(List<T> 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。
protected volatile List<Server> allServerList = Collections.synchronizedList(new ArrayList<Server>());
public void setServersList(List lsrv) {
Lock writeLock = allServerLock.writeLock();
ArrayList<Server> newServers = new ArrayList<Server>();
// 加写锁
writeLock.lock();
try {
// for 循环将 lsrv 中的 Server 转移到 allServers
ArrayList<Server> allServers = new ArrayList<Server>();
for (Object server : lsrv) {
if (server instanceof String) {
// 一般就是静态配置中配置的 ip:port 的形式
server = new Server((String) server);
}
if (server instanceof Server) {
// 从 EurekaClient 同步过来的 Server
allServers.add((Server) server);
} else {
throw new IllegalArgumentException("Type String or Server expected, instead found:" + server.getClass());
}
}
// 如果启用了服务预热,开始 Server 预热...
// 直接替换
allServerList = allServers;
// Ping 判断 Server 存活状态
if (canSkipPing()) {
for (Server s : allServerList) {
s.setAlive(true);
}
upServerList = allServerList;
} else if (listChanged) {
forceQuickPing();
}
} finally {
// 释放写锁
writeLock.unlock();
}
}
前面 chooseRoundRobinAfterFiltering 获取所有 Server 时就是返回的这个 allServerList 列表。
public List<Server> getAllServers() {
return Collections.unmodifiableList(allServerList);
}
Eureka Ribbon 客户端配置
获取 Server 的组件是 ServerList,RibbonClientConfiguration 中配置的默认实现类是 ConfigurationBasedServerList。ConfigurationBasedServerList 默认是从配置文件中获取,可以像下面这样配置服务实例地址,多个 Server 地址用逗号隔开。
demo-producer:
ribbon:
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。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnRibbonAndEurekaEnabled
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = EurekaRibbonClientConfiguration.class)
public class RibbonEurekaAutoConfiguration {
}
进入 EurekaRibbonClientConfiguration 可以看到:
- IPing 的默认实现类为
NIWSDiscoveryPing。 - ServerList 的默认实现类为 DomainExtractingServerList,但是 DomainExtractingServerList 在构造时又传入了一个类型为
DiscoveryEnabledNIWSServerList的 ServerList。看名字大概也可以看出,DiscoveryEnabledNIWSServerList 就是从 EurekaClient 获取 Server 的组件。
@Configuration(proxyBeanMethods = false)
public class EurekaRibbonClientConfiguration {
@Value("${ribbon.eureka.approximateZoneFromHostname:false}")
private boolean approximateZoneFromHostname = false;
@RibbonClientName
private String serviceId = "client";
@Autowired
private PropertiesFactory propertiesFactory;
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
if (this.propertiesFactory.isSet(IPing.class, serviceId)) {
return this.propertiesFactory.get(IPing.class, config, serviceId);
}
NIWSDiscoveryPing ping = new NIWSDiscoveryPing();
ping.initWithNiwsConfig(config);
return ping;
}
@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;
}
}
从 DiscoveryClient 获取Server列表
DynamicServerListLoadBalancer 中通过 ServerList 的 getUpdatedListOfServers 方法全量获取服务列表,在 eureka-client 环境下,ServerList 默认实现类为 DomainExtractingServerList,那就先看下它的 getUpdatedListOfServers 方法。
可以看出,DomainExtractingServerList 先用 DomainExtractingServerList 获取服务列表,然后根据 Ribbon 客户端配置重新构造 Server 对象返回。获取服务列表的核心在 DiscoveryEnabledNIWSServerList 中。
@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
// list => DiscoveryEnabledNIWSServerList
List<DiscoveryEnabledServer> servers = setZones(this.list.getUpdatedListOfServers());
return servers;
}
private List<DiscoveryEnabledServer> setZones(List<DiscoveryEnabledServer> servers) {
List<DiscoveryEnabledServer> result = new ArrayList<>();
boolean isSecure = this.ribbon.isSecure(true);
boolean shouldUseIpAddr = this.ribbon.isUseIPAddrForServer();
// 根据客户端配置重新构造 DomainExtractingServer 返回
for (DiscoveryEnabledServer server : servers) {
result.add(new DomainExtractingServer(server, isSecure, shouldUseIpAddr, this.approximateZoneFromHostname));
}
return result;
}
先看下 DiscoveryEnabledNIWSServerList 的构造初始化:
- 主要是传入了
Provider<EurekaClient>用来获取 EurekaClient。 - 另外还设置了客户端名称 clientName ,以及 vipAddresses 也是客户端名称,这个后面会用得上。
public DiscoveryEnabledNIWSServerList(IClientConfig clientConfig, Provider<EurekaClient> eurekaClientProvider) {
this.eurekaClientProvider = eurekaClientProvider;
initWithNiwsConfig(clientConfig);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
// 客户端名称,就是服务名称
clientName = clientConfig.getClientName();
// vipAddresses 得到的也是客户端名称
vipAddresses = clientConfig.resolveDeploymentContextbasedVipAddresses();
// 其它的一些配置....
}
接着看获取实例的 getUpdatedListOfServers,可以看到它的核心逻辑就是根据服务名从 EurekaClient 获取 InstanceInfo 实例列表,然后封装 Server 信息返回。
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
return obtainServersViaDiscovery();
}
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
// 得到 EurekaClient,实际类型是 CloudEurekaClient,其父类是 DiscoveryClient
EurekaClient eurekaClient = eurekaClientProvider.get();
if (vipAddresses!=null){
// 分割 vipAddresses,默认就是服务名称
for (String vipAddress : vipAddresses.split(",")) {
// 根据服务名称从 EurekaClient 获取实例信息
List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
for (InstanceInfo ii : listOfInstanceInfo) {
if (ii.getStatus().equals(InstanceStatus.UP)) {
// 根据实例信息 InstanceInfo 创建 Server
DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr);
serverList.add(des);
}
}
}
}
return serverList;
}
注意这里的 vipAddress 其实就是服务名:
最后看 EurekaClient 的 getInstancesByVipAddress,到这里就很清楚了,其实就是从 DiscoveryClient 的本地应用 Applications 中根据服务名取出所有的实例列表。
这里就和 Eureka 源码那块衔接上了,eureka-client 全量抓取注册表以及每隔30秒增量抓取注册表,都是合并到本地的 Applications 中。Ribbon 与 Eureka 结合后,Ribbon 获取 Server 就从 DiscoveryClient 的 Applications 中获取 Server 列表了。
public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure, String region) {
// ...
Applications applications;
if (instanceRegionChecker.isLocalRegion(region)) {
// 取本地应用 Applications
applications = this.localRegionApps.get();
} else {
applications = remoteRegionVsApps.get(region);
if (null == applications) {
return Collections.emptyList();
}
}
if (!secure) {
// 返回服务名对应的实例
return applications.getInstancesByVirtualHostName(vipAddress);
} else {
return applications.getInstancesBySecureVirtualHostName(vipAddress);
}
}
定时更新Server列表
DynamicServerListLoadBalancer 初始化时,有个方法还没说,就是 enableAndInitLearnNewServersFeature()。这个方法只是调用 ServerListUpdater 启动了一个 UpdateAction,这个 UpdateAction 又只是调用了一下 updateListOfServers 方法,就是前面讲解过的全量更新 Server 的逻辑。
public void enableAndInitLearnNewServersFeature() {
serverListUpdater.start(updateAction);
}
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
// 调用 updateListOfServers
updateListOfServers();
}
};
ServerListUpdater 的默认实现类是 PollingServerListUpdater,看下它的 start 方法:
其实就是以固定的频率,每隔30秒调用一下 updateListOfServers 方法,将 DiscoveryClient 中 Applications 中缓存的实例同步到 ILoadBalancer 中的 allServerList 列表中。
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
// 执行一次 updateListOfServers
updateAction.doUpdate();
// 设置最后更新时间
lastUpdated = System.currentTimeMillis();
}
};
// 固定频率调度
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs, // 默认 1000
refreshIntervalMs, // 默认 30 * 1000
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}
到这里我们就要注意下了,结合 eureka 抓取注册表的流程我们来分析下 Ribbon 客户端最多需要多久才能感应到一个新实例的注册:
- 首先 DiscoveryClient 每隔
30秒向注册中心抓取一次增量注册表,抓取增量注册表会先从只读缓存读取 - 然后 eureka server 端注册表的设计是三层缓存结构,每隔
30秒从读写缓存 readWriteMap 同步到只读缓存 readOnlyMap - 注册中心的实例同步到客户端本地后,Ribbon 又会间隔
30秒从 DiscoveryClient 同步到 BaseLoadBalancer
总结一下,也就是说 Ribbon 负载均衡可能最多需要90秒才能感应到注册中心注册的新实例。
判断Server是否存活
在创建 ILoadBalancer 时,IPing 还没有看过是如何工作的。在初始化的时候,可以看到,主要就是设置了当前的 ping,然后重新设置了一个调度任务,默认每隔30秒调度一次 PingTask 任务。
public void setPing(IPing ping) {
if (ping != null) {
if (!ping.equals(this.ping)) {
this.ping = ping;
// 设置 Ping 任务
setupPingTask();
}
} else {
this.ping = null;
// cancel the timer task
lbTimer.cancel();
}
}
void setupPingTask() {
// ...
// 创建一个定时调度器
lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name, true);
// pingIntervalTime 默认为 30 秒,每隔30秒调度一次 PingTask
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
// 立即发起以 Ping
forceQuickPing();
}
ShutdownEnabledTimer 可以简单了解下,它是继承自 Timer 的,它在创建的时候向 Runtime 注册了一个回调,在 jvm 关闭的时候来取消 Timer 的执行,进而释放资源。
public class ShutdownEnabledTimer extends Timer {
private Thread cancelThread;
private String name;
public ShutdownEnabledTimer(String name, boolean daemon) {
super(name, daemon);
this.name = name;
// 取消定时器的线程
this.cancelThread = new Thread(new Runnable() {
public void run() {
ShutdownEnabledTimer.super.cancel();
}
});
// 向 Runtime 注册一个钩子,在 jvm 关闭时,调用 cancelThread 取消定时任务
Runtime.getRuntime().addShutdownHook(this.cancelThread);
}
@Override
public void cancel() {
super.cancel();
try {
// 移除回调
Runtime.getRuntime().removeShutdownHook(this.cancelThread);
} catch (IllegalStateException ise) {
LOGGER.info("Exception caught (might be ok if at shutdown)", ise);
}
}
}
再来看下 PingTask,PingTask 核心逻辑就是遍历 allServers 列表,使用 IPingStrategy 和 IPing 来判断 Server 是否存活,并更新 Server 的状态,以及将所有存活的 Server 更新到 upServerList 中,upServerList 缓存了所有存活的 Server。
class PingTask extends TimerTask {
public void run() {
try {
// pingStrategy => SerialPingStrategy
new Pinger(pingStrategy).runPinger();
} catch (Exception e) {
logger.error("LoadBalancer [{}]: Error pinging", name, e);
}
}
}
class Pinger {
private final IPingStrategy pingerStrategy;
public Pinger(IPingStrategy pingerStrategy) {
this.pingerStrategy = pingerStrategy;
}
public void runPinger() throws Exception {
if (!pingInProgress.compareAndSet(false, true)) {
return; // Ping in progress - nothing to do
}
Server[] allServers = null;
boolean[] results = null;
Lock allLock = null;
Lock upLock = null;
try {
allLock = allServerLock.readLock();
allLock.lock();
// 加读锁,取出 allServerList 中的 Server
allServers = allServerList.toArray(new Server[allServerList.size()]);
allLock.unlock();
int numCandidates = allServers.length;
// 使用 IPingStrategy 和 IPing 对所有 Server 发起 ping 请求
results = pingerStrategy.pingServers(ping, allServers);
final List<Server> newUpList = new ArrayList<Server>();
final List<Server> changedServers = new ArrayList<Server>();
for (int i = 0; i < numCandidates; i++) {
boolean isAlive = results[i];
Server svr = allServers[i];
boolean oldIsAlive = svr.isAlive();
// 设置 alive 是否存活
svr.setAlive(isAlive);
// 实例变更
if (oldIsAlive != isAlive) {
changedServers.add(svr);
}
// 添加存活的 Server
if (isAlive) {
newUpList.add(svr);
}
}
upLock = upServerLock.writeLock();
upLock.lock();
// 更新 upServerList,upServerList 只保存了存活的 Server
upServerList = newUpList;
upLock.unlock();
// 通知变更
notifyServerStatusChangeListener(changedServers);
} finally {
pingInProgress.set(false);
}
}
}
IPingStrategy 的默认实现类是 SerialPingStrategy,进入可以发现它只是遍历所有 Server,然后用 IPing 判断 Server 是否存活。
private static class SerialPingStrategy implements IPingStrategy {
@Override
public boolean[] pingServers(IPing ping, Server[] servers) {
int numCandidates = servers.length;
boolean[] results = new boolean[numCandidates];
for (int i = 0; i < numCandidates; i++) {
results[i] = false;
try {
if (ping != null) {
// 使用 IPing 判断 Server 是否存活
results[i] = ping.isAlive(servers[i]);
}
} catch (Exception e) {
logger.error("Exception while pinging Server: '{}'", servers[i], e);
}
}
return results;
}
}
在集成 eureka-client 后,IPing 默认实现类是 NIWSDiscoveryPing,看它的 isAlive 方法,其实就是判断对应 Server 的实例 InstanceInfo 的状态是否是 UP 状态,UP状态就表示 Server 存活。
public boolean isAlive(Server server) {
boolean isAlive = true;
if (server!=null && server instanceof DiscoveryEnabledServer){
DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server;
InstanceInfo instanceInfo = dServer.getInstanceInfo();
if (instanceInfo!=null){
InstanceStatus status = instanceInfo.getStatus();
if (status!=null){
// 判断Server对应的实例状态是否是 UP
isAlive = status.equals(InstanceStatus.UP);
}
}
}
return isAlive;
}
一张图总结 Ribbon 核心原理
1、Ribbon 核心工作原理总结
首先,Ribbon 的7个核心接口共同定义了 Ribbon 的行为特性,它们就是 Ribbon 的核心骨架。
- 使用 Ribbon 来对客户端做负载均衡,基本的用法就是用
@LoadBalanced注解标注一个 RestTemplate 的 bean 对象,之后在LoadBalancerAutoConfiguration配置类中会对带有 @LoadBalanced 注解的 RestTemplate 添加LoadBalancerInterceptor拦截器。 - LoadBalancerInterceptor 会拦截 RestTemplate 的 HTTP 请求,将请求绑定进 Ribbon 负载均衡的生命周期,然后使用
LoadBalancerClient的execute方法来处理请求。 - LoadBalancerClient 首先会得到一个
ILoadBalancer,再使用它去得到一个 Server,这个 Server 就是具体某一个实例的信息封装。得到 Server 之后,就用 Server 的 IP 和端口重构原始 URI。 - ILoadBalancer 最终在选择实例的时候,会通过
IRule均衡策略来选择一个 Server。 - ILoadBalancer 的父类 BaseLoadBalancer 中有一个
allServerList列表缓存了所有 Server,Ribbon 中 Server 的来源就是 allServerList。 - 在加载Ribbon客户端上下文时,ILoadBalancer 会用
ServerList从 DiscoveryClient 的Applications中获取客户端对应的实例列表,然后使用ServerListFilter过滤,最后更新到 allServerList 中。 - ILoadBalancer 还会开启一个后台任务
ServerListUpdater,每隔30秒运行一次,用 ServerList 将 DiscoveryClient 的 Applications 中的实例列表同步到 allServerList 中。 - ILoadBalancer 还会开启一个后台任务
PingTask,每隔30秒运行一次,用 IPing 判断 Server 的存活状态,EurekaClient 环境下,就是判断InstanceInfo的状态是否为UP。
2、下面用一张图来总结下 Ribbon 这块获取Server的核心流程以及对应的核心接口间的关系。