- EurekaClient 确定源码分析入口
// 程序启动主函数
@EnableEurekaClient
@SpringBootApplication
public class Provider01Application {
public static void main(String[] args) {
SpringApplication.run(Provider01Application.class, args);
}
}
// EnableEurekaClient注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface EnableEurekaClient {
}
通过@EnableEurekaClient注解开启EurekaClient,但是从该注解源码得不到有用的信息。这时候我们可以查看一下eureka-client包META-INFO文件夹下的spring-factories文件,或许可以找到帮助,这也是分析Spring系列相关源码的一个小技巧。
// spring-factories 文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration
该文件中提供了与EurekaClient初始化先关的类,但是这些类的初始化加载顺序并不明确,这时候我们可以通过在这些类中加入打印信息、查看EurekaClient控制台打印信息等确定源码分析的入口。例如在EurekaClientAutoConfiguration类中加入:
@PostConstruct
public void init(){
log.info("== spring.factories 01 EurekaClientAutoConfiguration init …");
}
启动Server、Client可以看到其初始化顺序为:
== spring.factories 01 EurekaClientAutoConfiguration init …
== spring.factories 02 EurekaDiscoveryClientConfiguration init …
== spring.factories 03 EurekaDiscoveryClientConfiguration init …
接下来就以EurekaClientAutoConfiguration为入口,开始分析。
- EurekaClientAutoConfiguration 注解分析
@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(EurekaClientConfig.class)
@Import(DiscoveryClientOptionalArgsConfiguration.class)
@ConditionalOnBean(EurekaDiscoveryClientConfiguration.Marker.class)
@ConditionalOnProperty(value = “eureka.client.enabled”, matchIfMissing = true)
@ConditionalOnDiscoveryEnabled
@AutoConfigureBefore({
NoopDiscoveryClientAutoConfiguration.class,
CommonsClientAutoConfiguration.class,
ServiceRegistryAutoConfiguration.class
})
@AutoConfigureAfter(name = {
// RefreshScope和环境变量更改(例如重新绑定日志程序级别)自动配置类
“org.springframework.cloud.autoconfigure.RefreshAutoConfiguration”,
// 提供了EurekaHealthCheckHandler、EurekaHealthCheckHandler等相关功能
“org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration”,
// 提供了AutoServiceRegistration、AutoServiceRegistrationProperties等相关功能
“org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration”
})
public class EurekaClientAutoConfiguration {
。。。省略代码
}
该类运用了大量的注解,挑选几个比较重要的注解分析:
@Import --> 引入某一个类的实例
@AutoConfigureBefore --> 主类(EurekaClientAutoConfiguration)实例化之后,再实例化该注解指定的类
@AutoConfigureAfter --> 注解指定的类实例化之后,在实例化主类(EurekaClientAutoConfiguration)
接下来就应该分析@AutoConfigureAfter注解指定的类,RefreshAutoConfiguration、EurekaDiscoveryClientConfiguration、和AutoServiceRegistrationAutoConfiguration
2.1 @AutoConfigureAfter注解相关
2.1.1 RefreshAutoConfiguration
该类为@RefreshScope注解以及与环境更改相关的功能重新加载(例如重新绑定日志程序级别)的自动配置类。
RefreshScope 该类提供了对Bean的重新加载功能,通过refresh和refreshAll方法来实现,前者会单独销毁并重新加载某一个Bean,后者会销毁所有缓存的bean并重新加载,两者都会在下次访问时强制刷新。
// RefreshScope Bean
@Bean
@ConditionalOnMissingBean(RefreshScope.class)
public static RefreshScope refreshScope() {
return new RefreshScope();
}
// 局部刷新
public boolean refresh(String name) {
if (!name.startsWith(SCOPED_TARGET_PREFIX)) {
// User wants to refresh the bean with this name but that isn’t the one in the
// cache…
name = SCOPED_TARGET_PREFIX + name;
}
// Ensure lifecycle is finished if bean was disposable
if (super.destroy(name)) {
this.context.publishEvent(new RefreshScopeRefreshedEvent(name));
return true;
}
return false;
}
// 全部刷新
public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
日志级别刷新,通过LoggingRebinder来实现。
// LoggingRebinder Bean
@Bean
@ConditionalOnMissingBean
public static LoggingRebinder loggingRebinder() {
return new LoggingRebinder();
}
// LoggingRebinder 源码
public class LoggingRebinder implements ApplicationListener, EnvironmentAware {
private static final Bindable<Map<String, String>> STRING_STRING_MAP = Bindable.mapOf(String.class, String.class);
private final Log logger = LogFactory.getLog(getClass());
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.environment == null) {
return;
}
LoggingSystem system = LoggingSystem.get(LoggingSystem.class.getClassLoader());
setLogLevels(system, this.environment);
}
protected void setLogLevels(LoggingSystem system, Environment environment) {
Map<String, String> levels = Binder.get(environment)
.bind("logging.level", STRING_STRING_MAP)
.orElseGet(Collections::emptyMap);
for (Entry<String, String> entry : levels.entrySet()) {
setLogLevel(system, environment, entry.getKey(), entry.getValue().toString());
}
}
private void setLogLevel(LoggingSystem system, Environment environment, String name, String level) {
try {
if (name.equalsIgnoreCase("root")) {
name = null;
}
level = environment.resolvePlaceholders(level);
system.setLogLevel(name, LogLevel.valueOf(level.toUpperCase()));
}
catch (RuntimeException ex) {
this.logger.error("Cannot set level: " + level + " for '" + name + "'");
}
}
}
该类实现了EnvironmentAware接口并,系统启动时会将当前系统环境信息注入进来,并通过调用/refreash方法,触发onApplicationEvent方法重新设定日志级别。关于此例demo可以参考:@ScopeRefresh
2.1.2 EurekaDiscoveryClientConfiguration
该类主要提供了RefreshScope事件监听相关的事件、以及健康监控相关的功能,比较简单,下面我们以RefreshScope为例来分析一下该类的源码。
@Configuration
@ConditionalOnClass(RefreshScopeRefreshedEvent.class)
protected static class EurekaClientConfigurationRefresher implements ApplicationListener {
@Autowired(required = false)
private EurekaClient eurekaClient;
@Autowired(required = false)
private EurekaAutoServiceRegistration autoRegistration;
/**
* {@link org.springframework.cloud.endpoint.RefreshEndpoint}
* 调用/refresh请求的时候会触发此事件
*/
public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
// This will force the creation of the EurekaClient bean if not already created
// to make sure the client will be reregistered after a refresh event
// 这将强制创建EurekaClient(如果还没有创建),以确保在刷新事件之后重新注册客户机
if (eurekaClient != null) {
eurekaClient.getApplications();
}
if (autoRegistration != null) {
// register in case meta data changed
// 注册以防元数据更改
this.autoRegistration.stop();
this.autoRegistration.start();
}
}
}
前文提到了调用/refresh (如:http://localhost:7001/actuator/refresh) 方法会重新加载相关bean、设定日志级别等功能。该功能的入口在org.springframework.cloud.endpoint.RefreshEndpoint中。
// RefreshEndpoint端点
// @Endpoint注解提供了对外暴露接口的功能,可以通过JMX或者HTTP进行访问。
@Endpoint(id = “refresh”)
public class RefreshEndpoint {
private ContextRefresher contextRefresher;
public RefreshEndpoint(ContextRefresher contextRefresher) {
this.contextRefresher = contextRefresher;
}
// refresh方法
@WriteOperation
public Collection refresh() {
Set keys = this.contextRefresher.refresh();
return keys;
}
}
// refresh方法体
public synchronized Set refresh() {
//刷新体统环境
Set keys = refreshEnvironment();
//调用refreshAll方法
this.scope.refreshAll();
return keys;
}
2.2 EurekaClientAutoConfiguration内部Bean相关
EurekaClientAutoConfiguration提供了很多相关的内部Bean对于其实例化顺序,我们可以借助代码分析,也可以通过打印日志的方式进行分析。这里为了简便起见使用日志打印的方式进行分析,以便快速理清思路。我们在这些bean中加入日志,如:
// 这些顺序我已经分析过,所以标注了01、02等顺序
log.info("== 01 EurekaClientAutoConfiguration ManagementMetadataProvider init …");
另外该类中还提供了两个静态内部类EurekaClientConfiguration和RefreshableEurekaClientConfiguration,因为EurekaClientConfiguration被@ConditionalOnMissingRefreshScope注解,所以该静态内部类中的bean不会被实例化。我们只需在RefreshableEurekaClientConfiguration类的bean中记录日志即可:
// 这些顺序我已经分析过,所以标注了01、02等顺序
log.info("== static class 01 RefreshableEurekaClientConfiguration EurekaRegistration init …");
启动程序,我们可以看到如下的日志顺序(中间省略了部分其他日志):
== 01 EurekaClientAutoConfiguration ManagementMetadataProvider init …
== 02 EurekaClientAutoConfiguration EurekaInstanceConfigBean init …
== 03 EurekaClientAutoConfiguration EurekaClientConfigBean init …
== 04 EurekaClientAutoConfiguration HasFeatures init …
== 05 EurekaClientAutoConfiguration EurekaServiceRegistry init …
== 06 EurekaClientAutoConfiguration EurekaAutoServiceRegistration init …
== 07 EurekaClientAutoConfiguration DiscoveryClient init …
== static class 01 RefreshableEurekaClientConfiguration EurekaRegistration init …
== static class 02 RefreshableEurekaClientConfiguration ApplicationInfoManager init …
== static class 03 RefreshableEurekaClientConfiguration CloudEurekaClient init …
明确了bean的实例化顺序之后,就可以开始接下来的分析了。
2.2.1 ManagementMetadataProvider
该类主要提供了管理端的一些元数据信息,可以通过其提供的get方法获取:
public ManagementMetadata get(EurekaInstanceConfigBean instance, // Instance实例信息
int serverPort, // 实例端口号
String serverContextPath, // serverContextPath
String managementContextPath, // 管理端点ContextPath
Integer managementPort){ // 管理端点端口号
// managementPort != null && managementPort == 0 返回null
if (isRandom(managementPort)) {
return null;
}
// managementPort == null && (serverPort != null && serverPort == 0) 返回null
if (managementPort == null && isRandom(serverPort)) {
return null;
}
// 获取健康监测路径
String healthCheckUrl = getHealthCheckUrl(instance, serverPort, serverContextPath, managementContextPath, managementPort, false);
// 获取状态监测路径
String statusPageUrl = getStatusPageUrl(instance, serverPort, serverContextPath, managementContextPath, managementPort);
// 实例化端点监控元数据,如果managementPort==null,则默认使用serverPort作为managementPort
ManagementMetadata metadata = new ManagementMetadata(healthCheckUrl, statusPageUrl, managementPort == null ? serverPort : managementPort);
// 如果开启了安全端口通讯,则设置secureHealthCheckUrl,以https开头
if (instance.isSecurePortEnabled()) {
metadata.setSecureHealthCheckUrl(getHealthCheckUrl(instance, serverPort, serverContextPath, managementContextPath, managementPort, true));
}
return metadata;
}
2.2.2 EurekaInstanceConfigBean
该bean会创建一个EurekaInstanceConfigBean实例,该实例包含了Eureka实例的信息。关于这些元数据的使用,大家可以参考SpringCloud的官方文档。
@Bean
@ConditionalOnMissingBean(value = EurekaInstanceConfig.class, search = SearchStrategy.CURRENT)
public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils,ManagementMetadataProvider managementMetadataProvider) {
log.info("== 02 EurekaClientAutoConfiguration EurekaInstanceConfigBean init ...");
String hostname = getProperty("eureka.instance.hostname");
// eureka.instance.prefer-ip-address: 以IP地址注册到服务中心,相互注册使用IP地址
boolean preferIpAddress = Boolean.parseBoolean(getProperty("eureka.instance.prefer-ip-address"));
// eureka.instance.ip-address: 强制指定IP地址,默认会获取本机的IP地址
String ipAddress = getProperty("eureka.instance.ip-address");
// eureka.instance.secure-port-enabled: 开启安全通信的端口,就是使用https进行通信
// 与之相对的non-secure-port-enabled:开启不安全通信的端口,就是使用http进行通信
boolean isSecurePortEnabled = Boolean.parseBoolean(getProperty("eureka.instance.secure-port-enabled"));
// 获取context-path
String serverContextPath = env.getProperty("server.servlet.context-path", "/");
// 获取server.port
int serverPort = Integer.valueOf(env.getProperty("server.port", env.getProperty("port", "8080")));
Integer managementPort = env.getProperty("management.server.port", Integer.class); // nullable.
// SpringBoot管理端点context-path
String managementContextPath = env.getProperty("management.server.servlet.context-path"); // nullable.
// jmx管理端点port
Integer jmxPort = env.getProperty("com.sun.management.jmxremote.port",Integer.class); // nullable
// 创建EurekaInstanceConfigBean,并配置该bean的信息
EurekaInstanceConfigBean instance = new EurekaInstanceConfigBean(inetUtils);
instance.setNonSecurePort(serverPort);
instance.setInstanceId(getDefaultInstanceId(env));
instance.setPreferIpAddress(preferIpAddress);
instance.setSecurePortEnabled(isSecurePortEnabled);
if (StringUtils.hasText(ipAddress)) {
instance.setIpAddress(ipAddress);
}
if (isSecurePortEnabled) {
instance.setSecurePort(serverPort);
}
if (StringUtils.hasText(hostname)) {
instance.setHostname(hostname);
}
// status和health监测地址
String statusPageUrlPath = getProperty("eureka.instance.status-page-url-path");
String healthCheckUrlPath = getProperty("eureka.instance.health-check-url-path");
if (StringUtils.hasText(statusPageUrlPath)) {
instance.setStatusPageUrlPath(statusPageUrlPath);
}
if (StringUtils.hasText(healthCheckUrlPath)) {
instance.setHealthCheckUrlPath(healthCheckUrlPath);
}
// 获取监控端点元数据信息
ManagementMetadata metadata = managementMetadataProvider.get(instance, serverPort,
serverContextPath, managementContextPath, managementPort);
if (metadata != null) {
// 设置监控端点的Status、Health、SecurePort等信息
instance.setStatusPageUrl(metadata.getStatusPageUrl());
instance.setHealthCheckUrl(metadata.getHealthCheckUrl());
if (instance.isSecurePortEnabled()) {
instance.setSecureHealthCheckUrl(metadata.getSecureHealthCheckUrl());
}
Map<String, String> metadataMap = instance.getMetadataMap();
metadataMap.computeIfAbsent("management.port", k -> String.valueOf(metadata.getManagementPort()));
}
else {
// 如果没有监控端点的元数据,状态和健康检查url将不会被设置,
// 状态页面和健康检查url路径将不包括上下文路径,所以在这里设置它们
// without the metadata the status and health check URLs will not be set
// and the status page and health check url paths will not include the
// context path so set them here
if (StringUtils.hasText(managementContextPath)) {
instance.setHealthCheckUrlPath(managementContextPath + instance.getHealthCheckUrlPath());
instance.setStatusPageUrlPath(managementContextPath + instance.getStatusPageUrlPath());
}
}
// 设置Jmx监控端点
setupJmxPort(instance, jmxPort);
return instance;
}
2.2.3 EurekaClientConfigBean
@Bean
@ConditionalOnMissingBean(value = EurekaClientConfig.class, search = SearchStrategy.CURRENT)
public EurekaClientConfigBean eurekaClientConfigBean(ConfigurableEnvironment env) {
log.info("== 03 EurekaClientAutoConfiguration EurekaClientConfigBean init …");
EurekaClientConfigBean client = new EurekaClientConfigBean();
if (“bootstrap”.equals(this.env.getProperty(“spring.config.name”))) {
// 默认情况下,我们不会在引导过程中注册,但是稍后会有另一个机会。
// We don’t register during bootstrap by default, but there will be another chance later.
client.setRegisterWithEureka(false);
}
return client;
}
该段代码会创建一个EurekaClientConfigBean,该bean包含了EurekaClient相关的配置信息,下面列出这些属性的说明,关于其使用,可以多结合自己的Demo调试,
@ConfigurationProperties(EurekaClientConfigBean.PREFIX)
public class EurekaClientConfigBean implements EurekaClientConfig, Ordered {
// EurekaClient配置默认前缀
public static final String PREFIX = "eureka.client";
// Eureka默认的url
public static final String DEFAULT_URL = "http://localhost:8761" + DEFAULT_PREFIX + "/";
// 如果没有基于区域解析任何可用性区域,则默认可用性区域。
public static final String DEFAULT_ZONE = "defaultZone";
private static final int MINUTES = 60;
@Autowired(required = false)
PropertyResolver propertyResolver;
// 是否启用EurekaClient,默认为true
private boolean enabled = true;
@NestedConfigurationProperty
private EurekaTransportConfig transport = new CloudEurekaTransportConfig();
//从eureka服务器注册表中获取注册信息的时间间隔(s),默认为30秒
private int registryFetchIntervalSeconds = 30;
//复制实例变化信息到eureka服务器所需要的时间间隔(s),默认为30秒
private int instanceInfoReplicationIntervalSeconds = 30;
//最初复制实例信息到eureka服务器所需的时间(s),默认为40秒
private int initialInstanceInfoReplicationIntervalSeconds = 40;
//询问Eureka服务url信息变化的时间间隔(s),默认为300秒
private int eurekaServiceUrlPollIntervalSeconds = 5 * MINUTES;
//获取eureka服务的代理主机,默认为null
private String proxyPort;
//获取eureka服务的代理端口, 默认为null
private String proxyHost;
//获取eureka服务的代理用户名,默认为null
private String proxyUserName;
//获取eureka服务的代理密码,默认为null
private String proxyPassword;
//eureka需要超时读取之前需要等待的时间,默认为8秒
private int eurekaServerReadTimeoutSeconds = 8;
//eureka需要超时连接之前需要等待的时间,默认为5秒
private int eurekaServerConnectTimeoutSeconds = 5;
/获取实现了eureka客户端在第一次启动时读取注册表的信息作为回退选项的实现名称
private String backupRegistryImpl;
//eureka客户端允许所有eureka服务器连接的总数目,默认是200
private int eurekaServerTotalConnections = 200;
//eureka客户端允许eureka服务器主机连接的总数目,默认是50
private int eurekaServerTotalConnectionsPerHost = 50;
//表示eureka注册中心的路径,如果配置为eureka,则为http://x.x.x.x:x/eureka/,
//在eureka的配置文件中加入此配置表示eureka作为客户端向注册中心注册,从而构成eureka集群。
//此配置只有在eureka服务器ip地址列表是在DNS中才会用到,默认为null
private String eurekaServerURLContext;
//获取eureka服务器的端口,此配置只有在eureka服务器ip地址列表是在DNS中才会用到。默认为null
private String eurekaServerPort;
//获取要查询的DNS名称来获得eureka服务器,此配置只有在eureka服务器ip地址列表是在DNS中才会用到。默认为null
private String eurekaServerDNSName;
//获取实例所在的地区。默认为us-east-1
private String region = “us-east-1”;
//Eureka服务的http请求关闭之前其响应的时间,默认为30 秒
private int eurekaConnectionIdleTimeoutSeconds = 30;
//此客户端只对一个单一的VIP注册表的信息感兴趣。默认为null
private String registryRefreshSingleVipAddress;
//心跳执行程序线程池的大小,默认为2
private int heartbeatExecutorThreadPoolSize = 2;
//心跳执行程序回退相关的属性,是重试延迟的最大倍数值,默认为10
private int heartbeatExecutorExponentialBackOffBound = 10;
//执行程序缓存刷新线程池的大小,默认为2
private int cacheRefreshExecutorThreadPoolSize = 2;
//执行程序指数回退刷新的相关属性,是重试延迟的最大倍数值,默认为10
private int cacheRefreshExecutorExponentialBackOffBound = 10;
// 可用的服务全限定URl集合
private Map<String, String> serviceUrl = new HashMap<>();
{
this.serviceUrl.put(DEFAULT_ZONE, DEFAULT_URL);
}
//eureka注册表的内容是否被压缩,默认为true,并且是在最好的网络流量下被压缩
private boolean gZipContent = true;
//eureka客户端是否应该使用DNS机制来获取eureka服务器的地址列表,默认为false
private boolean useDnsForFetchingServiceUrls = false;
//实例是否在eureka服务器上注册自己的信息以供其他服务发现,默认为true
private boolean registerWithEureka = true;
//实例是否使用同一zone里的eureka服务器,默认为true,理想状态下,eureka客户端与服务端是在同一zone下
private boolean preferSameZoneEureka = true;
//是否记录eureka服务器和客户端之间在注册表的信息方面的差异,默认为false
private boolean logDeltaDiff;
//指示eureka客户机是否应该禁用对delta的抓取,而应该求助于获取完整的注册表信息。
//注意,delta获取可以极大地减少流量,因为eureka服务器的更改速度通常比获取速度低得多。
//更改在运行时的下一个注册表获取周期中有效,该周期由registryFetchIntervalSeconds指定
private boolean disableDelta;
//eureka服务注册表信息里的以逗号隔开的地区名单,如果不这样返回这些地区名单,则客户端启动将会出错。默认为null
private String fetchRemoteRegionsRegistry;
//获取实例所在的地区下可用性的区域列表,用逗号隔开。
private Map<String, String> availabilityZones = new HashMap<>();
//是否获得处于开启状态的实例的应用程序过滤之后的应用程序。默认为true
private boolean filterOnlyUpInstances = true;
//此客户端是否获取eureka服务器注册表上的注册信息,默认为true
private boolean fetchRegistry = true;
//eureka服务器序列化/反序列化的信息中获取“$”符号的的替换字符串。默认为“-”
private String dollarReplacement = "-";
//eureka服务器序列化/反序列化的信息中获取“_”符号的的替换字符串。默认为“”
private String escapeCharReplacement = "";
//指示服务器是否可以将客户机请求重定向到备份服务器/集群。
//如果设置为false,服务器将直接处理请求,
//如果设置为true,它可能向客户机发送HTTP重定向,并提供一个新的服务器位置。
private boolean allowRedirects = false;
//如果设置为true,客户端的状态更新将会点播更新到远程服务器上,默认为true
private boolean onDemandUpdateStatusChange = true;
//这是一个短暂的编码器的配置,如果最新的编码器是稳定的,则可以去除,默认为null
private String encoderName;
//获取编码器名称
private String decoderName;
//客户端数据接收
private String clientDataAccept = EurekaAccept.full.name();
//指示客户端关闭时是否应显式地从远程服务器注销自己。
private boolean shouldUnregisterOnShutdown = true;
//指示客户端是否应在初始化期间强制注册。默认为false
private boolean shouldEnforceRegistrationAtInit = false;
//“CompositeDiscoveryClient”用于对可用客户机排序的发现客户机的顺序。
private int order = 0;
…省略get/set方法
}
2.2.1 HasFeatures
该ben主要提供了一些系统相关的特性,可以通过下面的配置开启:
management.endpoints.web.exposure.include=features
然后在浏览器中通过:http://localhost:7001/actuator/features 进行访问
// EurekaClient自带的HasFeatures
@Bean
public HasFeatures eurekaFeature() {
log.info("== 04 EurekaClientAutoConfiguration HasFeatures init …");
return HasFeatures.namedFeature(“Eureka Client”, EurekaClient.class);
}
当然我们也可以自定义HasFeatures:
// 自定义HasFeatures
@Bean(name = “myHasFeatures”)
public HasFeatures eurekaFeature1() {
log.info("== 04 EurekaClientAutoConfiguration HasFeatures init …");
return HasFeatures.namedFeature(“Eureka Client Config”, EurekaClientConfigBean.class);
}
访问上面的链接,可以看到:
{
enabled: [
{
type: “com.netflix.discovery.EurekaClient”,
name: “Eureka Client”,
version: “1.9.9-SNAPSHOT”,
vendor: null
},
{
type: “org.springframework.cloud.netflix.eureka.EurekaClientConfigBean”,
name: “Eureka Client Config”,
version: null,
vendor: null
},
{
type: “org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient”,
name: “DiscoveryClient”,
version: “2.1.1.RELEASE”,
vendor: “Pivotal Software, Inc.”
},
{
type: “org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient”,
name: “LoadBalancerClient”,
version: null,
vendor: null
},
{
type: “com.netflix.ribbon.Ribbon”,
name: “Ribbon”,
version: “2.3.0”,
vendor: null
}
],
disabled: [ ]
}
2.2.5 EurekaServiceRegistry
该类实现了ServiceRegistry接口,主要用来注册、注销、设置和获取注册实例的状态。该Bean非常重要哦,关于其具体代码会在后面分析EurekaClient的时候分析。
@Bean
public EurekaServiceRegistry eurekaServiceRegistry() {
log.info("== 05 EurekaClientAutoConfiguration EurekaServiceRegistry init …");
return new EurekaServiceRegistry();
}
2.2.6 EurekaAutoServiceRegistration
该类主要用来实现服务的自动注册,关于其具体代码后面会详细分析。
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = “spring.cloud.service-registry.auto-registration.enabled”, matchIfMissing = true)
public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(
ApplicationContext context,
EurekaServiceRegistry registry,
EurekaRegistration registration) {
log.info("== 06 EurekaClientAutoConfiguration EurekaAutoServiceRegistration init …");
return new EurekaAutoServiceRegistration(context, registry, registration);
}
2.2.7 DiscoveryClient
该bean主要提供了对EurekaClient实例信息的一些读取功能
@Bean
public DiscoveryClient discoveryClient(EurekaClient client, EurekaClientConfig clientConfig) {
log.info("== 07 EurekaClientAutoConfiguration DiscoveryClient init …");
return new EurekaDiscoveryClient(client, clientConfig);
}
2.3 EurekaClientAutoConfiguration 静态内部类相关bean
静态内部类的bean启动顺序如下:
== static class 01 RefreshableEurekaClientConfiguration EurekaRegistration init …
== static class 02 RefreshableEurekaClientConfiguration ApplicationInfoManager init …
== static class 03 RefreshableEurekaClientConfiguration CloudEurekaClient init …
2.3.1 EurekaRegistration
该类封装了自动注册所需的相关信息,如CloudEurekaInstanceConfig、EurekaClient、ApplicationInfoManager等信息
@Bean
@org.springframework.cloud.context.config.annotation.RefreshScope
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = “spring.cloud.service-registry.auto-registration.enabled”, matchIfMissing = true)
public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient,
CloudEurekaInstanceConfig instanceConfig,
ApplicationInfoManager applicationInfoManager,
@Autowired(required = false) ObjectProvider healthCheckHandler) {
log.info("== static class 01 RefreshableEurekaClientConfiguration EurekaRegistration init ...");
return EurekaRegistration
.builder(instanceConfig)
.with(applicationInfoManager)
.with(eurekaClient)
.with(healthCheckHandler)
.build();
}
2.3.2 ApplicationInfoManager
注意:该Bean是懒加载
该类封装了EurekaInstanceConfig、InstanceInfo等信息,并提供了对EurekaInstance的管理功能。
@Bean
@ConditionalOnMissingBean(value = ApplicationInfoManager.class, search = SearchStrategy.CURRENT)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public ApplicationInfoManager eurekaApplicationInfoManager(EurekaInstanceConfig config) {
log.info("== static class 02 RefreshableEurekaClientConfiguration ApplicationInfoManager init …");
InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);
return new ApplicationInfoManager(config, instanceInfo);
}
InstanceInfo封装的信息如下:
public InstanceInfo create(EurekaInstanceConfig config) {
LeaseInfo.Builder leaseInfoBuilder = LeaseInfo.Builder.newBuilder()
.setRenewalIntervalInSecs(config.getLeaseRenewalIntervalInSeconds())
.setDurationInSecs(config.getLeaseExpirationDurationInSeconds());
// 构建要在eureka服务器上注册的实例信息
// Builder the instance information to be registered with eureka server
InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder();
String namespace = config.getNamespace();
if (!namespace.endsWith(".")) {
namespace = namespace + “.”;
}
builder.setNamespace(namespace).setAppName(config.getAppname())
.setInstanceId(config.getInstanceId())
.setAppGroupName(config.getAppGroupName())
.setDataCenterInfo(config.getDataCenterInfo())
.setIPAddr(config.getIpAddress()).setHostName(config.getHostName(false))
.setPort(config.getNonSecurePort())
.enablePort(InstanceInfo.PortType.UNSECURE,config.isNonSecurePortEnabled())
.setSecurePort(config.getSecurePort())
.enablePort(InstanceInfo.PortType.SECURE, config.getSecurePortEnabled())
.setVIPAddress(config.getVirtualHostName())
.setSecureVIPAddress(config.getSecureVirtualHostName())
.setHomePageUrl(config.getHomePageUrlPath(), config.getHomePageUrl())
.setStatusPageUrl(config.getStatusPageUrlPath(),config.getStatusPageUrl())
.setHealthCheckUrls(config.getHealthCheckUrlPath(),config.getHealthCheckUrl(), config.getSecureHealthCheckUr
.setASGName(config.getASGName());
// Start off with the STARTING state to avoid traffic
// 实例注册到Eureka上是否立刻开启通讯。有时候应用在准备好服务之前需要一些预处理。
// 如果设置了不立刻开启通讯,则将当前服务实例状态设置为STARTING
if (!config.isInstanceEnabledOnit()) {
InstanceInfo.InstanceStatus initialStatus = InstanceInfo.InstanceStatus.STARTING;
if (log.isInfoEnabled()) {
log.info("Setting initial instance status as: " + initialStatus);
}
builder.setStatus(initialStatus);
}
else {
if (log.isInfoEnabled()) {
log.info("Setting initial instance status as: "
+ InstanceInfo.InstanceStatus.UP
+ ". This may be too early for the instance to advertise itself as available. "
+ “You would instead want to control this via a healthcheck handler.”);
}
}
// Add any user-specific metadata information
// 添加用户指定的元数据信息
for (Map.Entry<String, String> mapEntry : config.getMetadataMap().entrySet()) {
String key = mapEntry.getKey();
String value = mapEntry.getValue();
// only add the metadata if the value is present
if (value != null && !value.isEmpty()) {
builder.add(key, value);
}
}
InstanceInfo instanceInfo = builder.build();
instanceInfo.setLeaseInfo(leaseInfoBuilder.build());
return instanceInfo;
}
2.3.3 EurekaClient
注意:该Bean是懒加载
// init third
@Bean(destroyMethod = “shutdown”)
@ConditionalOnMissingBean(value = EurekaClient.class, search = SearchStrategy.CURRENT)
@org.springframework.cloud.context.config.annotation.RefreshScope
@Lazy
public EurekaClient eurekaClient(
ApplicationInfoManager manager,
EurekaClientConfig config,
EurekaInstanceConfig instance,
@Autowired(required = false) HealthCheckHandler healthCheckHandler) {
log.info("== static class 03 RefreshableEurekaClientConfiguration CloudEurekaClient init …");
/**
* 如果我们使用ApplicationInfoManager的代理,那么当在CloudEurekaClient上调用shutdown时,
* 我们可能会遇到一个问题,因为在CloudEurekaClient上请求了ApplicationInfoManager bean,但是不允许,
* 因为我们正在关闭它。为了避免这种情况,我们直接使用对象。
*
* If we use the proxy of the ApplicationInfoManager we could run into a problem
* when shutdown is called on the CloudEurekaClient where the ApplicationInfoManager bean is
* requested but wont be allowed because we are shutting down. To avoid this we use the object directly.
*/
ApplicationInfoManager appManager;
if (AopUtils.isAopProxy(manager)) {
appManager = ProxyUtils.getTargetObject(manager);
}
else {
appManager = manager;
}
// 实例化CloudEurekaClient
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,config,this.optionalArgs,this.context);
cloudEurekaClient.registerHealthCheck(healthCheckHandler);
return cloudEurekaClient;
}
3. EurekaClient 实例化的时机
前文提到了两个懒加载的Bean,ApplicationInfoManager和EurekaClient,根据@Lazy注解的定义,第一次加载对应的bean时会将其实例化,那么什么时候会第一次用到该bean的实例呢?别忘了我们前文介绍的两个Bean,EurekaAutoServiceRegistration和EurekaServiceRegistry(当然,如果实在无法联想到这两个类,可以通过启动日志定位):
EurekaAutoServiceRegistration --> 提供服务自动注册功能
该类实现了Lifecycle接口,并重写了start方法,通过WebServerInitializedEvent事件触发该方法,并调用this.serviceRegistry.register(this.registration);注册服务。
/**
-
通过WebServerInitializedEvent事件触发
*/
@Override
public void start() {
// only set the port if the nonSecurePort or securePort is 0 and this.port != 0
if (this.port.get() != 0) {
// 如果未配置nonSecurePort,则使用server.port作为其端口
if (this.registration.getNonSecurePort() == 0) {
this.registration.setNonSecurePort(this.port.get());
}// 如果未配置securePort,但是配置了securePortEnabled,则使用server.port作为其端口 if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) { this.registration.setSecurePort(this.port.get()); }
}
// 如果running标志位未开启且nonSecurePort端口号>0
// only initialize if nonSecurePort is greater than 0 and it isn’t already running
// because of containerPortInitializer below
if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
// 注册服务
this.serviceRegistry.register(this.registration);
// 发布事件
this.context.publishEvent(new InstanceRegisteredEvent<>(this,this.registration.getInstanceConfig()));
// 将running标志位设置为true
this.running.set(true);
}
}
EurekaServiceRegistry --> 提供了服务注册、注销、关闭、设置服务状态、获取服务状态等功能
/** -
注册服务。注册通常包含关于实例的信息,比如它的主机名和端口。
-
通过EurekaAutoServiceRegistration类的start方法调用,而start方法由WebServerInitializedEvent事件触发
/
@Override
public void register(EurekaRegistration reg) {
/*- 初始化相关组件如,ApplicationInfoManager、EurekaClient
*/
maybeInitializeClient(reg);
if (log.isInfoEnabled()) {
log.info("Registering application "
+ reg.getApplicationInfoManager().getInfo().getAppName()
+ " with eureka with status "
+ reg.getInstanceConfig().getInitialStatus());
}reg.getApplicationInfoManager().setInstanceStatus(reg.getInstanceConfig().getInitialStatus());
reg.getHealthCheckHandler().ifAvailable(healthCheckHandler -> reg
.getEurekaClient().registerHealthCheck(healthCheckHandler));
} - 初始化相关组件如,ApplicationInfoManager、EurekaClient
/**
- 强制初始化可能作用域的代理
- @param reg
*/
private void maybeInitializeClient(EurekaRegistration reg) {
// force initialization of possibly scoped proxies
reg.getApplicationInfoManager().getInfo();
reg.getEurekaClient().getApplications();
}
其中maybeInitializeClient就是初始化这两个Bean的入口。接下来分析EurekaClient的实例化过程:
// 接上文,实例化入口
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,config,this.optionalArgs,this.context);
// 实例化CloudEurekaClient
public CloudEurekaClient(ApplicationInfoManager applicationInfoManager,
EurekaClientConfig config,
AbstractDiscoveryClientOptionalArgs<?> args,
ApplicationEventPublisher publisher) {
super(applicationInfoManager, config, args);
this.applicationInfoManager = applicationInfoManager;
this.publisher = publisher;
this.eurekaTransportField = ReflectionUtils.findField(DiscoveryClient.class, “eurekaTransport”);
ReflectionUtils.makeAccessible(this.eurekaTransportField);
}
// 调用父类构造方法
public DiscoveryClient(ApplicationInfoManager applicationInfoManager, final EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args) {
this(applicationInfoManager, config, args, new Provider() {
private volatile BackupRegistry backupRegistryInstance;
@Override
public synchronized BackupRegistry get() {
if (backupRegistryInstance == null) {
String backupRegistryClassName = config.getBackupRegistryImpl();
if (null != backupRegistryClassName) {
try {
backupRegistryInstance = (BackupRegistry) Class.forName(backupRegistryClassName).newInstance();
logger.info("Enabled backup registry of type {}", backupRegistryInstance.getClass());
} catch (InstantiationException e) {
logger.error("Error instantiating BackupRegistry.", e);
} catch (IllegalAccessException e) {
logger.error("Error instantiating BackupRegistry.", e);
} catch (ClassNotFoundException e) {
logger.error("Error instantiating BackupRegistry.", e);
}
}
if (backupRegistryInstance == null) {
logger.warn("Using default backup registry implementation which does not do anything.");
backupRegistryInstance = new NotImplementedRegistryImpl();
}
}
return backupRegistryInstance;
}
});
}
//再次调用构造方法
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager,
EurekaClientConfig config,
AbstractDiscoveryClientOptionalArgs args,
Provider backupRegistryProvider)
{
if (args != null) {
this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
this.eventListeners.addAll(args.getEventListeners());
this.preRegistrationHandler = args.preRegistrationHandler;
} else {
this.healthCheckCallbackProvider = null;
this.healthCheckHandlerProvider = null;
this.preRegistrationHandler = null;
}
this.applicationInfoManager = applicationInfoManager;
InstanceInfo myInfo = applicationInfoManager.getInfo();
clientConfig = config;
staticClientConfig = clientConfig;
transportConfig = config.getTransportConfig();
instanceInfo = myInfo;
if (myInfo != null) {
appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
} else {
logger.warn("Setting instanceInfo to a passed in null value");
}
this.backupRegistryProvider = backupRegistryProvider;
this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
localRegionApps.set(new Applications());
fetchRegistryGeneration = new AtomicLong(0);
remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));
// 此客户端是否获取eureka服务器注册表上的注册信息,默认为true
if (config.shouldFetchRegistry()) {
this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
// 实例是否在eureka服务器上注册自己的信息以供其他服务发现,默认为true
if (config.shouldRegisterWithEureka()) {
this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
logger.info("Initializing Eureka in region {}", clientConfig.getRegion());
/**
* 即不需要注册Eureka也无需获取已注册Eureka信息
*/
if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
logger.info("Client configured to neither register nor query for data.");
scheduler = null;
heartbeatExecutor = null;
cacheRefreshExecutor = null;
eurekaTransport = null;
instanceRegionChecker = new InstanceRegionChecker(new PropertyBasedAzToRegionMapper(config), clientConfig.getRegion());
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",initTimestampMs, this.getApplications().size());
return; // no need to setup up an network tasks and we are done
}
try {
// default size of 2 - 1 each for heartbeat and cacheRefresh
// 初始化线程池相关
// 创建schedulerService
scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-%d")
.setDaemon(true)
.build());
// 创建心跳Executor
heartbeatExecutor = new ThreadPoolExecutor(
1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
// 创建缓存刷新Executor
cacheRefreshExecutor = new ThreadPoolExecutor(
1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
// 初始化Eureka网络通信相关
eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);
AzToRegionMapper azToRegionMapper;
// eureka客户端是否应该使用DNS机制来获取eureka服务器的地址列表,默认为false
if (clientConfig.shouldUseDnsForFetchingServiceUrls()) {
azToRegionMapper = new DNSBasedAzToRegionMapper(clientConfig);
} else {
azToRegionMapper = new PropertyBasedAzToRegionMapper(clientConfig);
}
if (null != remoteRegionsToFetch.get()) {
azToRegionMapper.setRegionsToFetch(remoteRegionsToFetch.get().split(","));
}
instanceRegionChecker = new InstanceRegionChecker(azToRegionMapper, clientConfig.getRegion());
} catch (Throwable e) {
throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
}
// 从Eureka注册中心拉取注册信息
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}
// call and execute the pre registration handler before all background tasks (inc registration) is started
if (this.preRegistrationHandler != null) {
this.preRegistrationHandler.beforeRegistration();
}
if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
try {
if (!register() ) {
throw new IllegalStateException("Registration error at startup. Invalid server response.");
}
} catch (Throwable th) {
logger.error("Registration error at startup: {}", th.getMessage());
throw new IllegalStateException(th);
}
}
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
// 初始化Scheduled任务
initScheduledTasks();
// 注册监控
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register timers", e);
}
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",initTimestampMs, this.getApplications().size());
}
该方法涉及到的知识点较多,下面针对这里面比较重要的知识点逐个进行分析。
3.1 EurekaClient 获取已注册的服务器信息
在上一步Eureka实例化的过程中有这样一段代码:
// 获取已注册的服务器信息
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}
其中的fetchRegistry(false)功能即为获取已经注册的服务器信息。看似简短的一句代码,其内部执行的代码是很复杂的。
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
Stopwatch tracer = FETCH_REGISTRY_TIMER.start();
try {
// If the delta is disabled or if it is the first time, get all applications
Applications applications = getApplications();
// 全量获取注册信息
if (clientConfig.shouldDisableDelta()
|| (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
|| forceFullRegistryFetch
|| (applications == null)
|| (applications.getRegisteredApplications().size() == 0)
|| (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
{
logger.info("Disable delta property : {}", clientConfig.shouldDisableDelta());
logger.info("Single vip registry refresh property : {}", clientConfig.getRegistryRefreshSingleVipAddress());
logger.info("Force full registry fetch : {}", forceFullRegistryFetch);
logger.info("Application is null : {}", (applications == null));
logger.info("Registered Applications size is zero : {}",(applications.getRegisteredApplications().size() == 0));
logger.info("Application version is -1: {}", (applications.getVersion() == -1));
// 获取并将注册信息存储到本地
getAndStoreFullRegistry();
}
// 增量获取注册信息
else {
getAndUpdateDelta(applications);
}
applications.setAppsHashCode(applications.getReconcileHashCode());
logTotalInstances();
} catch (Throwable e) {
logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
return false;
} finally {
if (tracer != null) {
tracer.stop();
}
}
// Notify about cache refresh before updating the instance remote status
// 在更新实例远程状态之前通知缓存刷新
onCacheRefreshed();
// Update remote status based on refreshed data held in the cache
// 根据缓存中刷新的数据更新远程状态
updateInstanceRemoteStatus();
// registry was fetched successfully, so return true
return true;
}
该方法所做的事情如下:
- 全量或增量获取注册信息,获取注册信息并将注册信息缓存到本地
- 在更新实例远程状态之前通知缓存刷新
- 根据缓存中刷新的数据更新远程状态
- 日志信息打印
- 返回结果
这里最重要的步骤就是全量获取注册信息
3.1.1 getAndStoreFullRegistry 全量获取注册信息
getAndStoreFullRegistry方法会获取到注册到Eureka注册中心的所有服务器的信息(全量)并缓存到本地,包括Client和Consumer,同时会将自己也注册到Eureka注册中心。
private void getAndStoreFullRegistry() throws Throwable {
long currentUpdateGeneration = fetchRegistryGeneration.get();
logger.info("Getting all instance registry info from the eureka server");
// 注册的服务器信息(包含Client和Consumer)
Applications apps = null;
// 获取并注册服务器
EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
: eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
// 如果与Eureka注册中心成功通信
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
apps = httpResponse.getEntity();
}
logger.info("The response status is {}", httpResponse.getStatusCode());
if (apps == null) {
logger.error("The application is null for some reason. Not storing this information");
}
else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
// 将获取到的注册服务器信息,对其进行过滤,值保留状态为'UP'的服务器信息,打乱其排序,并存储到本地变量中
localRegionApps.set(this.filterAndShuffle(apps));
logger.debug("Got full registry with apps hashcode {}", apps.getAppsHashCode());
} else {
logger.warn("Not updating applications as another thread is updating it already");
}
}
这段代码的核心是eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
,获取服务器信息。而且接下来的代码执行过程比较绕。这段代码的方法调用栈梳理如下:
-- EurekaHttpClientDecorator.getApplications()
-- SessionedEurekaHttpClient.execute()
-- RetryableEurekaHttpClient.execute()
-- RedirectingEurekaHttpClient.execute()
-- MetricsCollectingEurekaHttpClient.execute()
-- AbstractJerseyEurekaHttpClient.getApplications()
- SessionedEurekaHttpClient.java
// SessionedEurekaHttpClient.java
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
// 超过 当前会话时间,关闭当前委托的 EurekaHttpClient 。
long now = System.currentTimeMillis();
long delay = now - lastReconnectTimeStamp;
if (delay >= currentSessionDurationMs) {
logger.debug("Ending a session and starting anew");
lastReconnectTimeStamp = now;
currentSessionDurationMs = randomizeSessionDuration(sessionDurationMs);
TransportUtils.shutdown(eurekaHttpClientRef.getAndSet(null));
}
// 获得委托的 EurekaHttpClient 。若不存在,则创建新的委托的 EurekaHttpClient 。
EurekaHttpClient eurekaHttpClient = eurekaHttpClientRef.get();
if (eurekaHttpClient == null) {
eurekaHttpClient = TransportUtils.getOrSetAnotherClient(eurekaHttpClientRef, clientFactory.newClient());
}
return requestExecutor.execute(eurekaHttpClient);
}
- RetryableEurekaHttpClient.java
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
// 定义候选主机
List<EurekaEndpoint> candidateHosts = null;
int endpointIdx = 0;
// numberOfRetries 失败重连尝试次数,默认为3次
for (int retry = 0; retry < numberOfRetries; retry++) {
EurekaHttpClient currentHttpClient = delegate.get();
EurekaEndpoint currentEndpoint = null;
if (currentHttpClient == null) {
if (candidateHosts == null) {
// 获取候选主机
candidateHosts = getHostCandidates();
if (candidateHosts.isEmpty()) {
throw new TransportException("There is no known eureka server; cluster server list is empty");
}
}
if (endpointIdx >= candidateHosts.size()) {
throw new TransportException("Cannot execute request on any known server");
}
currentEndpoint = candidateHosts.get(endpointIdx++);
currentHttpClient = clientFactory.newClient(currentEndpoint);
}
try {
// 执行请求
EurekaHttpResponse<R> response = requestExecutor.execute(currentHttpClient);
// 判断是否为可接受的相应,若是,返回。
if (serverStatusEvaluator.accept(response.getStatusCode(), requestExecutor.getRequestType())) {
delegate.set(currentHttpClient);
if (retry > 0) {
logger.info("Request execution succeeded on retry #{}", retry);
}
return response;
}
logger.warn("Request execution failure with status code {}; retrying on another server if available", response.getStatusCode());
} catch (Exception e) {
logger.warn("Request execution failed with message: {}", e.getMessage()); // just log message as the underlying client should log the stacktrace
}
// Connection error or 5xx from the server that must be retried on another server
// 请求失败,若是 currentHttpClient ,清除 delegate
delegate.compareAndSet(currentHttpClient, null);
// 请求失败,将 currentEndpoint 添加到隔离集合
if (currentEndpoint != null) {
quarantineSet.add(currentEndpoint);
}
}
throw new TransportException("Retry limit reached; giving up on completing the request");
}
- RedirectingEurekaHttpClient.java
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
EurekaHttpClient currentEurekaClient = delegateRef.get();
// 未找到非 302 的 Eureka-Server
if (currentEurekaClient == null) {
AtomicReference<EurekaHttpClient> currentEurekaClientRef = new AtomicReference<>(factory.newClient(serviceEndpoint));
try {
EurekaHttpResponse<R> response = executeOnNewServer(requestExecutor, currentEurekaClientRef);
// 关闭原有的委托 EurekaHttpClient ,并设置当前成功非 302 请求的 EurekaHttpClient
TransportUtils.shutdown(delegateRef.getAndSet(currentEurekaClientRef.get()));
return response;
} catch (Exception e) {
logger.error("Request execution error. endpoint={}", serviceEndpoint, e);
TransportUtils.shutdown(currentEurekaClientRef.get());
throw e;
}
}
// 已经找到非 302 的 Eureka-Server
else {
try {
return requestExecutor.execute(currentEurekaClient);
} catch (Exception e) {
logger.error("Request execution error. endpoint={}", serviceEndpoint, e);
delegateRef.compareAndSet(currentEurekaClient, null);
currentEurekaClient.shutdown();
throw e;
}
}
}
- MetricsCollectingEurekaHttpClient.java
@Override
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
EurekaHttpClientRequestMetrics requestMetrics = metricsByRequestType.get(requestExecutor.getRequestType());
Stopwatch stopwatch = requestMetrics.latencyTimer.start();
try {
EurekaHttpResponse<R> httpResponse = requestExecutor.execute(delegate);
requestMetrics.countersByStatus.get(mappedStatus(httpResponse)).increment();
return httpResponse;
} catch (Exception e) {
requestMetrics.connectionErrors.increment();
exceptionsMetric.count(e);
throw e;
} finally {
stopwatch.stop();
}
}
@Override
public EurekaHttpResponse<Applications> getApplications(String... regions) {
return getApplicationsInternal("apps/", regions);
}
private EurekaHttpResponse<Applications> getApplicationsInternal(String urlPath, String[] regions) {
ClientResponse response = null;
String regionsParamValue = null;
try {
// 通过Jersey-client执行请求
WebResource webResource = jerseyClient.resource(serviceUrl).path(urlPath);
if (regions != null && regions.length > 0) {
regionsParamValue = StringUtil.join(regions);
webResource = webResource.queryParam("regions", regionsParamValue);
}
Builder requestBuilder = webResource.getRequestBuilder();
addExtraHeaders(requestBuilder);
response = requestBuilder.accept(MediaType.APPLICATION_JSON_TYPE).get(ClientResponse.class);
// 封装返回的服务器信息
Applications applications = null;
if (response.getStatus() == Status.OK.getStatusCode() && response.hasEntity()) {
applications = response.getEntity(Applications.class);
}
return anEurekaHttpResponse(response.getStatus(), Applications.class)
.headers(headersOf(response))
.entity(applications)
.build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP GET {}/{}?{}; statusCode={}",
serviceUrl, urlPath,
regionsParamValue == null ? "" : "regions=" + regionsParamValue,
response == null ? "N/A" : response.getStatus()
);
}
if (response != null) {
response.close();
}
}
}
到这里EurekaClient的初始化分析基本可以告一段落,但是上面的代码少了很关键的一个步骤,EurekaClient是如何注册到Eureka注册中心的?这个问题我们留在下面的章节分析。
4 EurekaClient 定时任务线程初始化
前面我们分析了EurekaClient的初始化过程,并留了一个问题,EurekaClient是如何注册到Eureka注册中心的?答案就在EurekaClient的线程定时任务中。接前面的分析,打开DiscoveryClient类,找到initScheduledTasks方法:
/**
* 初始化所有计划的任务。
* Initializes all scheduled tasks.
*/
private void initScheduledTasks() {
/**
* 1、按指定间隔提取注册表信息的任务。
*/
// 此客户端是否获取eureka服务器注册表上的注册信息,默认为true
if (clientConfig.shouldFetchRegistry()) {
// registry cache refresh timer
// 注册表缓存刷新定时器
// 从eureka服务器注册表中获取注册信息的时间间隔(s),默认为30秒
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
// 执行程序指数回退刷新的相关属性,是重试延迟的最大倍数值,默认为10
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
// 创建拉取注册信息线程
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
/**
* 2、在给定间隔内更新租约的心跳任务。
*/
// 实例是否在eureka服务器上注册自己的信息以供其他服务发现,默认为true
if (clientConfig.shouldRegisterWithEureka()) {
// 心跳定时任务间隔 默认 30秒
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
// 心跳最大超时倍数,即 心跳最大超时时间 = renewalIntervalInSecs*expBackOffBound,默认为10
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
// Heartbeat timer
// 创建心跳任务定任务
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs, // 超时时间
TimeUnit.SECONDS,
expBackOffBound, // 心跳最大超时倍数
new HeartbeatThread() // 创建心跳任务线程
),
renewalIntervalInSecs, TimeUnit.SECONDS);
// InstanceInfo replicator
// 创建更新和复制本地instanceinfo到远程服务器的任务
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
// 复制实例变化信息到eureka服务器所需要的时间间隔(s),默认为30秒
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
// 创建应用实例状态变更监听器
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
// log at warn level if DOWN was involved
logger.warn("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
instanceInfoReplicator.onDemandUpdate();
}
};
// 注册应用实例状态变更监听器
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
// 开启应用实例信息复制器
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
这里一共初始化了三个定时任务:
- 按指定间隔提取注册表信息的任务
- 在给定间隔内更新租约的心跳任务
- 创建更新和复制本地instanceinfo到远程服务器的任务
- 创建应用实例状态变更监听器
下面对在给定间隔内更新租约的心跳任务和创建更新、复制本地instanceinfo到远程服务器、应用实例状态变更监听器做下分析:
4.1 在给定间隔内更新租约的心跳任务
续约心跳参数:
- renewalIntervalInSecs --> 心跳定时任务间隔 默认 30秒
- expBackOffBound --> 心跳最大超时倍数,即 心跳最大超时时间 = renewalIntervalInSecs*expBackOffBound,默认为10
续约任务和线程:
- TimedSupervisorTask --> 心跳任务
- HeartbeatThread --> 心跳执行线程
4.1.1 TimedSupervisorTask
TimedSupervisorTask的构造函数如下,都是一些赋值操作,这里有一个最大超时时间的设置:maxDelay = timeoutMillis * expBackOffBound,即前文我们提到的expBackOffBound。
public TimedSupervisorTask(String name, ScheduledExecutorService scheduler, ThreadPoolExecutor executor,
int timeout, TimeUnit timeUnit, int expBackOffBound, Runnable task) {
this.scheduler = scheduler;
this.executor = executor;
this.timeoutMillis = timeUnit.toMillis(timeout);
this.task = task;
this.delay = new AtomicLong(timeoutMillis);
// 计算最大超时时间
this.maxDelay = timeoutMillis * expBackOffBound;
// Initialize the counters and register.
successCounter = Monitors.newCounter("success");
timeoutCounter = Monitors.newCounter("timeouts");
rejectedCounter = Monitors.newCounter("rejectedExecutions");
throwableCounter = Monitors.newCounter("throwables");
threadPoolLevelGauge = new LongGauge(MonitorConfig.builder("threadPoolUsed").build());
Monitors.registerObject(name, this);
}
该类继承了TimerTask,所以继续看其run方法:
public void run() {
Future<?> future = null;
try {
// 提交任务
future = executor.submit(task);
threadPoolLevelGauge.set((long) executor.getActiveCount());
// 获取任务结果,这里将阻塞,直至任务完成或者超时
future.get(timeoutMillis, TimeUnit.MILLISECONDS); // block until done or timeout
delay.set(timeoutMillis);
threadPoolLevelGauge.set((long) executor.getActiveCount());
successCounter.increment();
} catch (TimeoutException e) {
logger.warn("task supervisor timed out", e);
timeoutCounter.increment();
/**
* 超时请求阶梯重试:
* 当发生超时异常时,需要重新计算下一次的请求超时时间,计算方法为
* time1 = currentDelay * 2
* time2 = timeoutMillis * expBackOffBound
*
* time1与time2取两者最小值作为下一次请求的超时时间
*/
long currentDelay = delay.get();
long newDelay = Math.min(maxDelay, currentDelay * 2);
delay.compareAndSet(currentDelay, newDelay);
} catch (RejectedExecutionException e) {
if (executor.isShutdown() || scheduler.isShutdown()) {
logger.warn("task supervisor shutting down, reject the task", e);
} else {
logger.warn("task supervisor rejected the task", e);
}
rejectedCounter.increment();
} catch (Throwable e) {
if (executor.isShutdown() || scheduler.isShutdown()) {
logger.warn("task supervisor shutting down, can't accept the task");
} else {
logger.warn("task supervisor threw an exception", e);
}
throwableCounter.increment();
} finally {
if (future != null) {
future.cancel(true);
}
if (!scheduler.isShutdown()) {
// delay.get() --> 设置下一次请求的超时时间
scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
}
}
}
关于这里的Future等不多介绍,这里有一个值得我们借鉴的点,就是超时阶梯重试机制:
当请求发生超时时,下一次请求并不是继续沿用之前的设置的超时时间,而是通过结算得出一个阶梯时间,以减少网络请求。其计算方式已经在注释中写明,不在赘述。
4.1.2 HeartbeatThread
/**
* 在给定间隔内更新租约的心跳任务。
* The heartbeat task that renews the lease in the given intervals.
*/
private class HeartbeatThread implements Runnable {
public void run() {
if (renew()) {
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
}
/**
* 通过REST请求实现续租
* Renew with the eureka service by making the appropriate REST call
*/
boolean renew() {
EurekaHttpResponse<InstanceInfo> httpResponse;
try {
// 发送心跳请求
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
// 404 Not Found ,则尝试重新执行注册操作
if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
REREGISTER_COUNTER.increment();
logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
long timestamp = instanceInfo.setIsDirtyWithTime();
boolean success = register();
if (success) {
instanceInfo.unsetIsDirty(timestamp);
}
return success;
}
// 返回心跳请求是否成功
return httpResponse.getStatusCode() == Status.OK.getStatusCode();
} catch (Throwable e) {
logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
return false;
}
}
续约任务主要通过两个操作来完成:
- 正常发送续约心跳
/**
* 发送心跳请求
*/
@Override
public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
String urlPath = "apps/" + appName + '/' + id;
ClientResponse response = null;
logger.info("== sendHeartBeat serviceUrl:" +serviceUrl+ " urlPath:" +urlPath);
try {
WebResource webResource = jerseyClient.resource(serviceUrl)
.path(urlPath)
.queryParam("status", info.getStatus().toString())
.queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
if (overriddenStatus != null) {
webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
}
Builder requestBuilder = webResource.getRequestBuilder();
addExtraHeaders(requestBuilder);
response = requestBuilder.put(ClientResponse.class);
EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));
if (response.hasEntity()) {
eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));
}
return eurekaResponseBuilder.build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
- 当请求404时,尝试执行注册
/**
* 通过REST请求注册Eureka服务
* Register with the eureka service by making the appropriate REST call.
*/
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
// 执行注册
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
/**
* 执行注册
*/
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
logger.info("== register serviceUrl:" +serviceUrl +" urlPath:" +urlPath);
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
4.2 创建更新和复制本地instanceinfo到远程服务器的任务
该任务会创建InstanceInfoReplicator实例,并通过手动调用start方法开启任务。EurekaClient实例的首次注册,即在该方法中完成相关设置。
public void start(int initialDelayMs) {
if (started.compareAndSet(false, true)) {
// 首次注册,设置应用实例数据信息不一致
instanceInfo.setIsDirty(); // for initial register
// 提交任务,并设置该任务的 Future
Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
关于instanceInfo.setIsDirty()的作用,看下面的代码:
public void run() {
try {
// 刷新应用实例信息
discoveryClient.refreshInstanceInfo();
// 判断应用实例信息是否数据不一致
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
// 发起注册
discoveryClient.register();
// 设置应用实例信息数据一致
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
InstanceInfoReplicator实现了Runnable,在其run方法中做了以下事情:
- 刷新应用实例信息
- 判断应用实例信息是否数据不一致
- 如不一致,则发起注册,并将应用实例信息是否数据设置为一致
当EurekaClient实例化并首次调用该任务时,应用实例信息肯定没有发生变化,那么就有必要手动设置应用实例信息数据不一致,以便完成首次注册。接下来我们再看看refreshDataCenterInfoIfRequired方法都刷新了哪些信息。
void refreshInstanceInfo() {
// 刷新DataCenter
applicationInfoManager.refreshDataCenterInfoIfRequired();
// 刷新租约信息
applicationInfoManager.refreshLeaseInfoIfRequired();
InstanceStatus status;
try {
status = getHealthCheckHandler().getStatus(instanceInfo.getStatus());
} catch (Exception e) {
logger.warn("Exception from healthcheckHandler.getStatus, setting status to DOWN", e);
status = InstanceStatus.DOWN;
}
if (null != status) {
applicationInfoManager.setInstanceStatus(status);
}
}
- 刷新DataCenter
public void refreshDataCenterInfoIfRequired() {
String existingAddress = instanceInfo.getHostName();
String existingSpotInstanceAction = null;
if (instanceInfo.getDataCenterInfo() instanceof AmazonInfo) {
existingSpotInstanceAction = ((AmazonInfo) instanceInfo.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.spotInstanceAction);
}
// 获取最新的主机名、IP地址
String newAddress;
if (config instanceof RefreshableInstanceConfig) {
// Refresh data center info, and return up to date address
// 刷新数据中心信息,并返回最新地址
newAddress = ((RefreshableInstanceConfig) config).resolveDefaultAddress(true);
} else {
newAddress = config.getHostName(true);
}
String newIp = config.getIpAddress();
// hostName发生变化,更新主机名和IP地址
if (newAddress != null && !newAddress.equals(existingAddress)) {
logger.info("The address changed from : {} => {}", existingAddress, newAddress);
logger.info("The address changed from : {} => {}", existingAddress, newAddress);
updateInstanceInfo(newAddress, newIp);
}
if (config.getDataCenterInfo() instanceof AmazonInfo) {
String newSpotInstanceAction = ((AmazonInfo) config.getDataCenterInfo()).get(AmazonInfo.MetaDataKey.spotInstanceAction);
if (newSpotInstanceAction != null && !newSpotInstanceAction.equals(existingSpotInstanceAction)) {
logger.info(String.format("The spot instance termination action changed from: %s => %s",
existingSpotInstanceAction,
newSpotInstanceAction));
updateInstanceInfo(null , null );
}
}
}
private void updateInstanceInfo(String newAddress, String newIp) {
// :( in the legacy code here the builder is acting as a mutator.
// This is hard to fix as this same instanceInfo instance is referenced elsewhere.
// We will most likely re-write the client at sometime so not fixing for now.
InstanceInfo.Builder builder = new InstanceInfo.Builder(instanceInfo);
if (newAddress != null) {
builder.setHostName(newAddress);
}
if (newIp != null) {
builder.setIPAddr(newIp);
}
builder.setDataCenterInfo(config.getDataCenterInfo());
// 设置应用实例信息数据不一致
instanceInfo.setIsDirty();
}
刷新DataCenter完成了对主机名、IP地址的刷新工作。
- 刷新租约信息
public void refreshLeaseInfoIfRequired() {
// 获取租约信息
LeaseInfo leaseInfo = instanceInfo.getLeaseInfo();
if (leaseInfo == null) {
return;
}
// 契约超时时间,超过currentLeaseDuration秒后没有续约,Server就认为他不可用了,随之就会将其剔除。
int currentLeaseDuration = config.getLeaseExpirationDurationInSeconds();
// 续约间隔时间(单位:秒)
int currentLeaseRenewal = config.getLeaseRenewalIntervalInSeconds();
/**
* 如果租约信息的契约超时时间和续约间隔时间不等于配置中的时间,则更新着两个时间
* 并将DirtyFlag设置为true,以便在下一次心跳时将实例信息传递到 discovery server
*/
if (leaseInfo.getDurationInSecs() != currentLeaseDuration || leaseInfo.getRenewalIntervalInSecs() != currentLeaseRenewal) {
logger.info("==The LeaseInfo changed from : {} => {}", leaseInfo.getDurationInSecs(),currentLeaseDuration);
logger.info("==The LeaseInfo changed from : {} => {}", leaseInfo.getRenewalIntervalInSecs(),currentLeaseRenewal);
LeaseInfo newLeaseInfo = LeaseInfo.Builder.newBuilder()
.setRenewalIntervalInSecs(currentLeaseRenewal)
.setDurationInSecs(currentLeaseDuration)
.build();
// 设置租约信息
instanceInfo.setLeaseInfo(newLeaseInfo);
// 设置应用实例数据信息不一致
instanceInfo.setIsDirty();
}
}
刷新租约信息刷新了契约超时时间、续约间隔时间
4.3 应用实例状态变更监听器
改监听器创建了StatusChangeListener接口的实例,并重写了notify方法,通过手动方式注册到applicationInfoManager,当发生StatusChangeEvent事件时,将触发notify方法。
前文介绍过refreshInstanceInfo方法,在该方法中有设置应用实例状态的一句代码:applicationInfoManager.setInstanceStatus(status);这句代码并不仅仅简单的设置了应用实例状态,而且发布了StatusChangeEvent通知
public synchronized void setInstanceStatus(InstanceStatus status) {
InstanceStatus next = instanceStatusMapper.map(status);
if (next == null) {
return;
}
InstanceStatus prev = instanceInfo.setStatus(next);
if (prev != null) {
for (StatusChangeListener listener : listeners.values()) {
try {
// 发布StatusChangeEvent通知
listener.notify(new StatusChangeEvent(prev, next));
} catch (Exception e) {
logger.warn("failed to notify listener: {}", listener.getId(), e);
}
}
}
}
发布了StatusChangeEvent后,会调用InstanceInfoReplicator的onDemandUpdate方法:
public boolean onDemandUpdate() {
if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
if (!scheduler.isShutdown()) {
scheduler.submit(new Runnable() {
@Override
public void run() {
logger.debug("Executing on-demand update of local InstanceInfo");
Future latestPeriodic = scheduledPeriodicRef.get();
// 取消最近一次任务
if (latestPeriodic != null && !latestPeriodic.isDone()) {
logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");
latestPeriodic.cancel(false);
}
// 再次执行任务,因为这里用到了ScheduledExecutorService,所以如果取消任务后,会导致整个定时任务被取消。
InstanceInfoReplicator.this.run();
}
});
return true;
} else {
logger.warn("Ignoring onDemand update due to stopped scheduler");
return false;
}
} else {
logger.warn("Ignoring onDemand update due to rate limiter");
return false;
}
}
该方法中只是判断了一下Future的状态,并取消了最后一次任务,以防止错误的应用信息实例状态注册到Eureka注册中心。这里用到了ScheduledExecutorService来执行定时任务,所以在调用了Future的cancel方法之后,要重新执行任务,并在run方法中重新开启定时任务。
EurekaClient实例化过程中相关的知识点就分析到这里。