Eureka之Client端注册


引言

本篇文章会基于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端启动成功

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

先总结一下该配置类做了什么工作,再分析源码

  1. 注册了Eureka 客户端的配置bean EurekaClientConfigBean , 该类承载所有在application.yml文件中配置的以eureka.client 开头的参数值,如

  2. 注册了Eureka 实例的配置bean EurekaInstanceConfigBean, 该类承载了所有在application.yml文件中以eureka.instance开头的配置参数

  3. 注册了EurekaServiceRegistry 实例,该实例实现了ServiceRegistry接口,主要用途是负责将(注册元数据)注册/取消注册从服务注册中心,即注册/取消注册的执行者

  4. 注册了EurekaRegistration 实例,该实例表示Eureka实例的服务注册数据信息

  5. 注册了EurekaAutoServiceRegistration 实例,看类名称定义,我们知道该类用于Eureka自动服务注册,其主要实现了ApplicationListener 接口,用于监听springboot启动事件,比如当WebServerInitializedEvent 事件触发时,其会调用内部的start()方法去注册,当ContextClosedEvent事件触发时,其会调用内部的close()方法去deregister()取消注册等,其含有两个重要的属性:

// spring应用上下文
private ApplicationContext context;

// 服务注册执行器
private EurekaServiceRegistry serviceRegistry; 

// 服务注册元数据信息
private EurekaRegistration registration;

其中3, 4, 5类的作用放在一起的话会比较好理解

  1. 注册了EurekaClient 实例,实际上是new了一个springcloud封装的CloudEurekaClient实例,该类实现了netflix的EurekaClient接口,用于和Eureka Server交互的客户端

  2. 注册了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启动流程图

通过流程图我们发现,其实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类
  1. InstanceInfoReplicator实现了Runnable接口,是个任务类,负责将自身的信息周期性的上报到远程的Eureka server
  2. 有两个场景触发上报:周期性任务、服务状态变化(开启了onDemandUpdateStatusChange配置,默认开启,同时onDemandUpdate被调用),因此,在同一时刻有可能有两个同时上报的任务同时出现
  3. 单线程执行上报的操作,如果有多个上报任务,也能确保是串行的;
  4. 服务状态变化上报任务有频率限制,通过burstSize参数来控制,默认的计算出来是一分钟最多4次
  5. 先创建的任务总是先执行,但是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注册流程走完了


谁来负责注册

  1. 如果开启强制初始化注册(shouldEnforceRegistrationAtInit)开关(默认false),Eureka Client会通过实例化CloudEurekaClient时候调用基类DiscoveryClient构造器时(启动定时scheduler之前)就注册

  2. 若开启了状态改变按需更新开关(onDemandUpdateStatusChange,默认true),注册动作会在spring上下文的finishRefresh方法调用后触发EurekaAutoServiceRegistration的start方法去执行注册逻辑,该注册逻辑是利用了ApplicationInfo.setInstanceStatus方法,进而触发实例InstanceInfo状态发生改变(由STARTING 改为UP),再进而触发了状态变化监听器去调用了InstanceInfoReplicator的onDemandUpdate方法,触发按需更新任务,从而执行了注册

  3. 若2中开关关闭,那么注册任务将由CloudEurekaClient 实例化时创建的定时scheduler去触发注册行为,Heartbeat 定时器为默认延迟30秒创建并每隔30秒定时执行,InstanceInfoReplicator定时器或默认延迟40秒并每隔30秒执行一次

其中心跳监测定时器会在第一次执行时,若返回404,会触发注册请求调用

而InstanceInfoReplicator定时器只要实例InstanceInfo发生dirty 时就会发起注册行为,而EurekaAutoServiceRegistration的start方法的执行就会导致InstanceInfo的状态由STARTING变为UP

故3中的两个执行器谁先首次执行,谁就会去发起注册行为,通常由于默认延迟时间不同,Heartbeat 定时器会发起首次注册行为


注册调用链

注册调用链

总结

整个客户端注册流程分析完了,后续会分析Eureka Server的服务端是如何处理的,以及心跳续约和缓存刷新的源码分析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容