引言
本篇文章会基于Eureka注册的源码去分析客户端注册的原理
基础搭建
Eureka Server搭建
pom文件
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml 文件
server:
port: 8716
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false # 表示不注册自己,即Eureka Server单点
fetch-registry: false # 表示不注册自己,即Eureka Server单点
启动类
启动类加上@EnableEurekaServer 即表示启用Eureka Server
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
启动Eureka Server,打开http://localhost:8716/
出现以下页面即代表server端启动成功
Eureka Client 搭建
eureka-client 版本 2.1.1.RELEASE
spring-boot 版本 2.1.4.RELEASE
1.pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.application.yml文件
server:
port: 8086
spring:
application:
name: PROVIDER-A # 注册到eureka server的服务名
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8716/eureka/ # eureka server的地址
3.启动类
启动类增加@EnableDisCoveryClient 注解,表示启动eureka client
@SpringBootApplication
@EnableDiscoveryClient
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
Eureka Client 注册源码
1. @EnableDiscoveryClient注解
/**
* Annotation to enable a DiscoveryClient implementation.
* @author Spencer Gibb
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {
/**
* If true, the ServiceRegistry will automatically register the local server.
* @return - {@code true} if you want to automatically register.
*/
boolean autoRegister() default true;
}
启用一个DiscoveryClient 的实现, 可以看到该注解的autoRegister 属性默认为true,代表自动注册到eureka server,另一个重要的点是@Import(EnableDiscoveryClientImportSelector.class)
@Import 注解作用
可以将多个@Configuration注解的类组合在一起形成一个配置类
进入EnableDiscoveryClientImportSelector类,其主要作用是引入了AutoServiceRegistrationConfiguration配置类,而该配置类的作用就是启用生效自动服务注册配置类AutoServiceRegistrationProperties,即@EnableConfigurationProperties(AutoServiceRegistrationProperties.class),
@EnableConfigurationProperties注解作用:就是使@ConfigurationProperties注解生效
如果只配置@ConfigurationProperties注解,在IOC容器中是获取不到properties配置
文件转化的bean的
@Configuration
@EnableConfigurationProperties(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
public class AutoServiceRegistrationConfiguration {
}
以上就是@EnableDisCoveryClient 注解的作用,总结起来就是为了生效自动服务注册配置bean AutoServiceRegistrationProperties 等于在yml文件中写spring.cloud.service-registry.auto-registration.enabled = true
2. META-INF/spring.factories文件
大家都知道springboot的自动装配是通过启动时读取该文件再配合很多注解实现的
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
我们主要关注以下两个类,其余的类和配置中心,Ribbon负载相关,暂不分析
- EurekaDiscoveryClientConfiguration
@Configuration
@EnableConfigurationProperties
@ConditionalOnClass(EurekaClientConfig.class)
@ConditionalOnProperty(value = "eureka.client.enabled", matchIfMissing = true)
@ConditionalOnDiscoveryEnabled
public class EurekaDiscoveryClientConfiguration {
@Bean
public Marker eurekaDiscoverClientMarker() {
return new Marker();
}
该配置类的主要作用就是初始化一个Marker bean,为满足EurekaClientAutoConfiguration配置类的启用条件
- 重点是这个配置类 EurekaClientAutoConfiguration
先总结一下该配置类做了什么工作,再分析源码
注册了Eureka 客户端的配置bean EurekaClientConfigBean , 该类承载所有在application.yml文件中配置的以eureka.client 开头的参数值,如
注册了Eureka 实例的配置bean EurekaInstanceConfigBean, 该类承载了所有在application.yml文件中以eureka.instance开头的配置参数
注册了EurekaServiceRegistry 实例,该实例实现了ServiceRegistry接口,主要用途是负责将(注册元数据)注册/取消注册从服务注册中心,即注册/取消注册的执行者
注册了EurekaRegistration 实例,该实例表示Eureka实例的服务注册数据信息
注册了EurekaAutoServiceRegistration 实例,看类名称定义,我们知道该类用于Eureka自动服务注册,其主要实现了ApplicationListener 接口,用于监听springboot启动事件,比如当WebServerInitializedEvent 事件触发时,其会调用内部的start()方法去注册,当ContextClosedEvent事件触发时,其会调用内部的close()方法去deregister()取消注册等,其含有两个重要的属性:
// spring应用上下文
private ApplicationContext context;
// 服务注册执行器
private EurekaServiceRegistry serviceRegistry;
// 服务注册元数据信息
private EurekaRegistration registration;
其中3, 4, 5类的作用放在一起的话会比较好理解
注册了EurekaClient 实例,实际上是new了一个springcloud封装的CloudEurekaClient实例,该类实现了netflix的EurekaClient接口,用于和Eureka Server交互的客户端
-
注册了ApplicationInfoManager实例,主要用来管理并初始化当前Instance实例的注册信息InstanceInfo实例,并提供了实例状态监听机制,内部重要属性
// 监听实例状态变化的监听器,key为listener的ID,value为监听器自身,其中StatusChangeListener 为ApplicationInfoManager类的内部静态接口 protected final Map<String, StatusChangeListener> listeners // 注册到server端的实例数据 private InstanceInfo instanceInfo; // Eureka客户端实例配置类 private EurekaInstanceConfig config;
3. Client服务启动分析
先上流程图
通过流程图我们发现,其实Client注册并不是发生在scheduler中的,而是在finshRefresh方法执行后,自动触发了EurekaAutoServiceRegistration的start方法,进而触发了其内部属性EurekaServiceRegistry实例的register方法
EurekaServiceRegistry 类的register方法内部会先尝试初始化Client类以及ApplicationInfoManager类的实例
再利用ApplicationInfoManager类实例setInstanceStatus方法更改实例的状态,进而触发了监听器,调用了一次后台同步器的onDemandUpdate方法
在这里会进行第一次的注册操作,后续的心跳续约定时器执行心跳续约时若返回404状态码,也是会进行注册操作的,注册逻辑就是通过这两个地方触发的
源码分析
- EurekaAutoServiceRegistration
public class EurekaAutoServiceRegistration implements AutoServiceRegistration,
SmartLifecycle, Ordered, SmartApplicationListener {
// 应用上下文
private ApplicationContext context;
// 服务注册器
private EurekaServiceRegistry serviceRegistry;
// 服务注册数据信息集合
private EurekaRegistration registration;
public EurekaAutoServiceRegistration(ApplicationContext context,
EurekaServiceRegistry serviceRegistry, EurekaRegistration registration) {
this.context = context;
this.serviceRegistry = serviceRegistry;
this.registration = registration;
}
// 当上下文的finishRefresh方法调用时,会触发调用此处
@Override
public void start() {
// only set the port if the nonSecurePort or securePort is 0 and this.port != 0
if (this.port.get() != 0) {
if (this.registration.getNonSecurePort() == 0) {
this.registration.setNonSecurePort(this.port.get());
}
if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
this.registration.setSecurePort(this.port.get());
}
}
// 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()));
this.running.set(true);
}
}
@Override
public void stop() {
this.serviceRegistry.deregister(this.registration);
this.running.set(false);
}
}
可以看出自动服务注册器的作用就是做了一次中转,即调用器内部服务注册器EurekaServiceRegistry 的 register方法
- EurekaServiceRegistry 类
public class EurekaServiceRegistry implements ServiceRegistry<EurekaRegistration> {
// 注册方法
@Override
public void register(EurekaRegistration reg) {
// 初始化客户端,因为这个时候客户端还未实例化(懒加载的bean)
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));
}
private void maybeInitializeClient(EurekaRegistration reg) {
// force initialization of possibly scoped proxies
reg.getApplicationInfoManager().getInfo();
reg.getEurekaClient().getApplications();
}
// 取消注册
@Override
public void deregister(EurekaRegistration reg) {
if (reg.getApplicationInfoManager().getInfo() != null) {
if (log.isInfoEnabled()) {
log.info("Unregistering application "
+ reg.getApplicationInfoManager().getInfo().getAppName()
+ " with eureka with status DOWN");
}
// 这里取消注册就是设置实例的状态为down
reg.getApplicationInfoManager()
.setInstanceStatus(InstanceInfo.InstanceStatus.DOWN);
// shutdown of eureka client should happen with EurekaRegistration.close()
// auto registration will create a bean which will be properly disposed
// manual registrations will need to call close()
}
}
- ApplicationInfoManager
public class ApplicationInfoManager {
// 监听器
protected final Map<String, StatusChangeListener> listeners;
// 管理的实例信息
private InstanceInfo instanceInfo;
// eureka实例配置信息
private EurekaInstanceConfig config;
/**
* Set the status of this instance. Application can use this to indicate
* whether it is ready to receive traffic. Setting the status here also notifies all registered listeners
* of a status change event.
*
* 设置实例状态,同时如果实例前后状态不一致,会触发监听器的通知方法
* @param status Status of the instance
*/
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 {
listener.notify(new StatusChangeEvent(prev, next));
} catch (Exception e) {
logger.warn("failed to notify listener: {}", listener.getId(), e);
}
}
}
}
// 注册监听器
public void registerStatusChangeListener(StatusChangeListener listener) {
listeners.put(listener.getId(), listener);
}
// 取消注册监听器
public void unregisterStatusChangeListener(String listenerId) {
listeners.remove(listenerId);
}
// 刷新租约信息
public void refreshLeaseInfoIfRequired() {
LeaseInfo leaseInfo = instanceInfo.getLeaseInfo();
if (leaseInfo == null) {
return;
}
int currentLeaseDuration = config.getLeaseExpirationDurationInSeconds();
int currentLeaseRenewal = config.getLeaseRenewalIntervalInSeconds();
if (leaseInfo.getDurationInSecs() != currentLeaseDuration || leaseInfo.getRenewalIntervalInSecs() != currentLeaseRenewal) {
LeaseInfo newLeaseInfo = LeaseInfo.Builder.newBuilder()
.setRenewalIntervalInSecs(currentLeaseRenewal)
.setDurationInSecs(currentLeaseDuration)
.build();
instanceInfo.setLeaseInfo(newLeaseInfo);
instanceInfo.setIsDirty();
}
}
// 内部接口,用于监听实例状态变化的监听器
public static interface StatusChangeListener {
String getId();
void notify(StatusChangeEvent statusChangeEvent);
}
}
- DiscoveryClient
该类主要看其构造方法以及initScheduledTasks方法,其余代码省略掉
public class DiscoveryClient implements EurekaClient {
// eureka client 配置
private static EurekaClientConfig staticClientConfig;
// 定时任务调度器
private final ScheduledExecutorService scheduler;
// 心跳执行器
private final ThreadPoolExecutor heartbeatExecutor;
// 缓存刷新执行器
private final ThreadPoolExecutor cacheRefreshExecutor;
// 应用实例信息管理器
private final ApplicationInfoManager applicationInfoManager;
// 实例信息,用于注册到eureka server的数据封装
private final InstanceInfo instanceInfo;
// eureka 请求传输器,主要封装了HttpClient等
private final EurekaTransport eurekaTransport;
// 构造方法
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> 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;
// 赋值准备注册到eureka server的实例信息,注意一下该实例的初始状态为STARTING
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(","));
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;
}
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 server,并且不从server端获取其他服务的注册列表信息的话,会直接return掉,并打印出Client configured to neither register nor query for data.日志
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
}
// 这里是重点,开始定义定时任务调度器 scheduler
try {
// default size of 2 - 1 each for heartbeat and cacheRefresh
scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-%d")
.setDaemon(true)
.build());
// 定义心跳检测执行器,线程池核心线程数1个,最大线程数为配置参数HeartbeatExecutorThreadPoolSize
heartbeatExecutor = new ThreadPoolExecutor(
1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
// 定义缓存刷新执行器,线程池核心线程数1个,最大线程数为配置参数CacheRefreshExecutorThreadPoolSize的值
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;
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);
}
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();
}
// 注意这里如果开启初始化强制注册开关的话,会直接注册到
// eureka server,
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);
}
}
// 重点是这里,初始化定时任务调度器
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());
}
// 初始化定时任务调度器
private void initScheduledTasks() {
// 配置开启获取注册表信息开关,则调度器去调度CacheRefreshThread 任务,调度频率为registryFetchIntervalSeconds,首次延迟时间也是registryFetchIntervalSeconds ,即默认30秒
if (clientConfig.shouldFetchRegistry()) {
// registry cache refresh timer
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
),
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
// 配置开启是否注册到server的开关,则调度器去调度HeartbeatThread 任务,调度频率为renewalIntervalInSecs,首次延迟时间也是renewalIntervalInSecs ,即默认30秒
if (clientConfig.shouldRegisterWithEureka()) {
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
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);
// 实例信息复制定时器
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
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();
}
};
// 注意这里是开启了在需要的时候更新状态变化的开关才会添加监听器,此处当开关开启时,当状态发生变化,会立即收到通知,即调用onDemandUpdate方法
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
// 启动周期性实例信息复制到远程定时器,默认延迟40秒执行
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
}
- InstanceInfoReplicator类
- InstanceInfoReplicator实现了Runnable接口,是个任务类,负责将自身的信息周期性的上报到远程的Eureka server
- 有两个场景触发上报:周期性任务、服务状态变化(开启了onDemandUpdateStatusChange配置,默认开启,同时onDemandUpdate被调用),因此,在同一时刻有可能有两个同时上报的任务同时出现
- 单线程执行上报的操作,如果有多个上报任务,也能确保是串行的;
- 服务状态变化上报任务有频率限制,通过burstSize参数来控制,默认的计算出来是一分钟最多4次
- 先创建的任务总是先执行,但是onDemandUpdate方法中创建的任务会将周期性任务给丢弃掉即由状态变化引起的上报任务的优先级高于普通的周期性上报任务
class InstanceInfoReplicator implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(InstanceInfoReplicator.class);
private final DiscoveryClient discoveryClient;
// 实例信息
private final InstanceInfo instanceInfo;
// 复制任务执行间隔时间,默认30秒
private final int replicationIntervalSeconds;
private final ScheduledExecutorService scheduler;
// 下一个周期性执行任务的引用
private final AtomicReference<Future> scheduledPeriodicRef;
private final AtomicBoolean started;
// 速率限制单位
private final RateLimiter rateLimiter;
// 突发大小 2
private final int burstSize;
// 允许每分钟任务数
private final int allowedRatePerMinute;
InstanceInfoReplicator(DiscoveryClient discoveryClient, InstanceInfo instanceInfo, int replicationIntervalSeconds, int burstSize) {
this.discoveryClient = discoveryClient;
this.instanceInfo = instanceInfo;
// 实例话一个单线程的任务调度执行器
this.scheduler = Executors.newScheduledThreadPool(1,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-InstanceInfoReplicator-%d")
.setDaemon(true)
.build());
this.scheduledPeriodicRef = new AtomicReference<Future>();
this.started = new AtomicBoolean(false);
this.rateLimiter = new RateLimiter(TimeUnit.MINUTES);
this.replicationIntervalSeconds = replicationIntervalSeconds;
this.burstSize = burstSize;
// 通过复制周期间隔值,以及burstSize值,计算出允许的每分钟按需更新任务数,默认计算出来的是:4
this.allowedRatePerMinute = 60 * this.burstSize / this.replicationIntervalSeconds;
logger.info("InstanceInfoReplicator onDemand update allowed rate per min is {}", allowedRatePerMinute);
}
// 启动周期性复制任务,需要等待延迟默认40秒才执行任务
public void start(int initialDelayMs) {
if (started.compareAndSet(false, true)) {
// 设置实例信息变化标志为true,并设置变化时间
instanceInfo.setIsDirty(); // for initial register
// 调度任务,默认延迟40秒执行第一次周期性复制任务
Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
// 将下一次任务执行引用赋值
scheduledPeriodicRef.set(next);
}
}
public void stop() {
shutdownAndAwaitTermination(scheduler);
started.set(false);
}
private void shutdownAndAwaitTermination(ExecutorService pool) {
pool.shutdown();
try {
if (!pool.awaitTermination(3, TimeUnit.SECONDS)) {
pool.shutdownNow();
}
} catch (InterruptedException e) {
logger.warn("InstanceInfoReplicator stop interrupted");
}
}
// 按需更新任务,由实例状态变化监听器调用方法,若按需更新开关关闭,则不会执行这里,因为不会注册状态变化监听器
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);
}
// 直接执行按需更新任务
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;
}
}
public void run() {
try {
// 刷新实例信息,若发生变化,就会设置dirty flag 并重置lastDirtyTimestamp 值为当前时间戳
discoveryClient.refreshInstanceInfo();
// 获取dirty flag状态下的时间,若不为null,则表示出现了实例信息变化,就重新注册
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
discoveryClient.register();
// 注册完成后执行dirty flag的释放操作,注意这里传入了上一次dirtyTimestamp,相当于乐观锁,即如果当前实例的lastDirtyTimestamp时间小于等于上一次的dirtyTimestamp值,说明在我注册期间实例信息没有再次dirty
// 那么就unset掉,否则unsetIsDirty不作处理
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
// 利用finally实现调度器的周期性调度,replicationIntervalSeconds值为下一次调度的延迟时间
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
}
- 最后看下DiscoveryClient 类中真正的注册方法register()
/**
* 通过发送适当的REST 请求去注册到远程的eureka server
*/
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
// 通过调用eureka传输器中的registrationClient 去注册实例信息
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());
}
// 返回状态码204即代表注册成功
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
- AbstractJerseyEurekaHttpClient
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
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();
}
}
}
至此,整个Eureka Client注册流程走完了
谁来负责注册
如果开启强制初始化注册(shouldEnforceRegistrationAtInit)开关(默认false),Eureka Client会通过实例化CloudEurekaClient时候调用基类DiscoveryClient构造器时(启动定时scheduler之前)就注册
若开启了状态改变按需更新开关(onDemandUpdateStatusChange,默认true),注册动作会在spring上下文的finishRefresh方法调用后触发EurekaAutoServiceRegistration的start方法去执行注册逻辑,该注册逻辑是利用了ApplicationInfo.setInstanceStatus方法,进而触发实例InstanceInfo状态发生改变(由STARTING 改为UP),再进而触发了状态变化监听器去调用了InstanceInfoReplicator的onDemandUpdate方法,触发按需更新任务,从而执行了注册
若2中开关关闭,那么注册任务将由CloudEurekaClient 实例化时创建的定时scheduler去触发注册行为,Heartbeat 定时器为默认延迟30秒创建并每隔30秒定时执行,InstanceInfoReplicator定时器或默认延迟40秒并每隔30秒执行一次
其中心跳监测定时器会在第一次执行时,若返回404,会触发注册请求调用
而InstanceInfoReplicator定时器只要实例InstanceInfo发生dirty 时就会发起注册行为,而EurekaAutoServiceRegistration的start方法的执行就会导致InstanceInfo的状态由STARTING变为UP
故3中的两个执行器谁先首次执行,谁就会去发起注册行为,通常由于默认延迟时间不同,Heartbeat 定时器会发起首次注册行为
注册调用链
总结
整个客户端注册流程分析完了,后续会分析Eureka Server的服务端是如何处理的,以及心跳续约和缓存刷新的源码分析