【SpringCloud系列】服务注册与发现 - Eureka Client源码分析(3)

上一节讲述了 Eureka Server 的原理及部分源码,今天咱们来看看 Eureka Client 端的源码,功能点类似 Eureka Server

3.7、Eureka Client 源码分析

Eureka Client 通过 Starter 的方式引入依赖, SpringBoot 将会为项目使用以下的自动配置类:

  • EurekaClientAutoConfigurationEureka Client 自动配置类,负责 Eureka Client 中关键Bean的配置和初始化;
  • RibbonEurekaAutoConfigurationRibbon 负载均衡相关配置;
  • EurekaDiscoveryClientConfiguration:配置自动注册、服务发现和应用的健康检查器。
3.7.1、读取应用自身配置信息

DiscoveryClientSpring Cloud 中用于进行服务发现的顶级接口,也是核心接口,在 Netflix Eureka 或者 Alibaba Nacos 或者 Consul 中都有相应的具体实现类。

public interface DiscoveryClient extends Ordered {

	/**
	 * Default order of the discovery client.
	 */
	int DEFAULT_ORDER = 0;

	/**
	 * A human-readable description of the implementation, used in HealthIndicator.
	 * @return The description.
	 */
    //获取实现类的描述
	String description();

	/**
	 * Gets all ServiceInstances associated with a particular serviceId.
	 * @param serviceId The serviceId to query.
	 * @return A List of ServiceInstance.
	 */
    //通过服务id获取服务实例的信息
	List<ServiceInstance> getInstances(String serviceId);

	/**
	 * @return All known service IDs.
	 */
    //获取所有服务的实例id
	List<String> getServices();

	/**
	 * Default implementation for getting order of discovery clients.
	 * @return order
	 */
	@Override
	default int getOrder() {
		return DEFAULT_ORDER;
	}

}

而在 Eureka 方面的实现,主要的实现类即为 EurekaDiscoveryClient。但是仔细看 EurekaDiscoveryClient 代码中会发现它会使用原生的 Eureka 中的代码:

public class EurekaDiscoveryClient implements DiscoveryClient {
    //other...
    
    //引入原生的EurekaClient接口
    private final EurekaClient eurekaClient;

    @Override
    public String description() {
        return DESCRIPTION;
    }

    @Override
    public List<ServiceInstance> getInstances(String serviceId) {
        List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,
                                                                              false);
        List<ServiceInstance> instances = new ArrayList<>();
        for (InstanceInfo info : infos) {
            instances.add(new EurekaServiceInstance(info));
        }
        return instances;
    }

    @Override
    public List<String> getServices() {
        Applications applications = this.eurekaClient.getApplications();
        if (applications == null) {
            return Collections.emptyList();
        }
        List<Application> registered = applications.getRegisteredApplications();
        List<String> names = new ArrayList<>();
        for (Application app : registered) {
            if (app.getInstances().isEmpty()) {
                continue;
            }
            names.add(app.getName().toLowerCase());

        }
        return names;
    }
}

此时的 EurekaClient 接口所在的包为 com.netflix.discovery,也就是说 Spring Cloud 通过内部组合方式调用了原生 Eureka 中的服务发现方法。而该 EurekaClient 接口的实现类默认是 DiscoveryClient 类,而该类属于原生 Eureka 中的服务发现类,所在的包为com.netflix.discovery,是不是有点迷糊了。

DiscoveryClient接口

仔细看代码,就会发现 Spring CloudDiscoveryClient 接口中的几个方法都是依靠 Eureka 原生接口 EurekaClient 来实现的,而原生 EurekaClient 默认指定的实现类为 DiscoveryClient ,所以归根到底主要看 DiscoveryClient 源码。

3.7.2、服务发现:DiscoveryClient

在讲解 Eureka Server 的时候,InstanceRegistry 也实现了 LookupService 接口, 同样原生的 EurekaClient 也实现了该接口,并在原来的基础上新增了很多检索服务的方法,有兴趣的朋友可以查看:

  • 提供了多种方式获取 InstanceInfo,例如根据区域、地址等方式;
  • 提供了为客户端注册和获取服务健康检查处理器的能力。

除去一般的检索服务的接口,主要关注 EurekaClient中的两个接口方法,分别是:

//DiscoveryClient#registerHealthCheck
// 为Eureka Client注册健康检查处理器
public void registerHealthCheck(HealthCheckHandler healthCheckHandler) {
    if (instanceInfo == null) {
        logger.error("Cannot register a healthcheck handler when instance info is null!");
    }
    if (healthCheckHandler != null) {
        this.healthCheckHandlerRef.set(healthCheckHandler);
        // schedule an onDemand update of the instanceInfo when a new healthcheck handler is registered
        if (instanceInfoReplicator != null) {
            instanceInfoReplicator.onDemandUpdate();
        }
    }
}

//DiscoveryClient#registerEventListener
// 监听Client服务实例信息的更新
public void registerEventListener(EurekaEventListener eventListener) {
    this.eventListeners.add(eventListener);
}

Eureka Server 一般通过心跳 (heartbeat)来识别一个实例的状态。 Eureka Client 中存在一个定时任务定时通过 HealthCheckHandlerClient 检测当前 Client 的状态 ,如 Client 的状态发生改变, 将会触发新的注册事件 ,更新 Eureka Server 注册表中该服务实例的相关信息。

HealthCheckHandler接口代码如下:

public interface HealthCheckHandler {
    InstanceInfo.InstanceStatus getStatus(InstanceInfo.InstanceStatus currentStatus);
}

spring-cloud-netflix-eureka-client中的实现主要是EurekaHealthCheckHandler, 它主要使用了spring-cloud-actuator中的 HealthAggregatorHealthIndicator,用于监测服务实例的状态。

EurekaEventListener注册的事件监听模式属于观察者模式,当服务实例的状态发生改变的时候,就会触发事件,仔细观察 EurekaClient中有个方法:

//DiscoveryClient#fireEvent
protected void fireEvent(final EurekaEvent event) {
    for (EurekaEventListener listener : eventListeners) {
        try {
            listener.onEvent(event);
        } catch (Exception e) {
            logger.info("Event {} throw an exception for listener {}", event, listener, e.getMessage());
        }
    }
}

fireEvent方法即为触发的事件。

3.7.3、DiscoveryClient构造函数

DiscoveryClient 构造函数中,Eureka Client 会执行从 Eureka Server 中拉取注册表信息、服务注册、 初始化发送心跳、缓存刷新( 重新拉取注册表信息 )和按需注册定时任务等操作,可以说 DiscoveryClient 的构造函数贯穿 Eureka Client 启动阶段的各项工作。

@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
    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.endpointRandomizer = endpointRandomizer;
    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());

    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
        scheduler = Executors.newScheduledThreadPool(2,
                                                     new ThreadFactoryBuilder()
                                                     .setNameFormat("DiscoveryClient-%d")
                                                     .setDaemon(true)
                                                     .build());

        heartbeatExecutor = new ThreadPoolExecutor(
            1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(),
            new ThreadFactoryBuilder()
            .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
            .setDaemon(true)
            .build()
        );  // use direct handoff

        cacheRefreshExecutor = new ThreadPoolExecutor(
            1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(),
            new ThreadFactoryBuilder()
            .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
            .setDaemon(true)
            .build()
        );  // use direct handoff

        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();
    }

    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
    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.7.4、读取应用配置信息

忽略掉构造函数中的大部分赋值操作,逐步分析:

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;
}

上述代码看到了熟悉的配置,eureka.client.fetch-registryeureka.client.register-with-eureka。如果eureka.client.fetch-registrytrue的时候表示 Eureka Client 将从 Eureka Server 中拉取注册表信息。而eureka.client.register-with-eurekatrue表示 Eureka Client 将注册到 **Eureka Server **中。所以如果上述的两个配置均为false,那么 DiscoveryClient 的初始化将直接结束,表示客户端既不进行服务注册,也不进行服务发现。

// default size of 2 - 1 each for heartbeat and cacheRefresh
scheduler = Executors.newScheduledThreadPool(2,
	new ThreadFactoryBuilder()
	setNameFormat("DiscoveryClient-%d")
	.setDaemon(true)
	.build());

heartbeatExecutor = new ThreadPoolExecutor(
    1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>(),
    new ThreadFactoryBuilder()
    .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
    .setDaemon(true)
    .build()
);  // use direct handoff

cacheRefreshExecutor = new ThreadPoolExecutor(
    1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>(),
    new ThreadFactoryBuilder()
    .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
    .setDaemon(true)
    .build()
);  // use direct handoff

接着定义了基于线程池的定时器线程池 ScheduledExecutorService ,线程池的大小为2,一个线程用于发送心跳,一个线程用于缓存刷新,同时定义了发送心跳和缓存刷新的线程池。

if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
    fetchRegistryFromBackup();
}
3.7.5、拉取注册表信息

如果 EurekaClientConfigshouldFetchRegistrytrue 时, fetchRegistry 方法将会被调用 。在Eureka ClientEureka Server 注册前,需要先从 Eureka Server 拉取注册表中的信息,这是服务发现的前提。 通过将 Eureka Server 中的注册表信息缓存到本地,就可以就近获取其它服务的相关信息, 从而减少与 Eureka Server 的网络通信。

//DiscoveryClient#fetchRegistry
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();

        //判断增量式拉取被禁止,或者Applications为null,将进行全量式拉取
        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);
        }
        //计算应用集合一致性hashcode
        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;
}

一般来说,在 Eureka 客户端,除了第一次拉取全量注册表信息,之后的信息拉取都会尝试只进行增量式拉取。

1)、全量式拉取的方法如下:

//DiscoveryClient#getAndStoreFullRegistry
private void getAndStoreFullRegistry() throws Throwable {
    long currentUpdateGeneration = fetchRegistryGeneration.get();

    logger.info("Getting all instance registry info from the eureka server");

    Applications apps = null;
    EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
        ? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
        : eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
    //如果响应码为200,表示成功
    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");
        //使用CAS判断更新版本是否发生变化,以免拉取的脏数据覆盖本地注册表信息
    } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        //从apps中筛选出状态为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(...),还是调用eurekaTransport.queryClient.getVip(...),两者内部都是调用同一个方法getApplicationsInternal,而且交给 Jersey 客户端实现:

//AbstractJerseyEurekaHttpClient#getApplications
@Override
public EurekaHttpResponse<Applications> getApplications(String... regions) {
    //请求路径为/eureka/apps/
    return getApplicationsInternal("apps/", regions);
}

//AbstractJerseyEurekaHttpClient#getDelta
@Override
public EurekaHttpResponse<Applications> getDelta(String... regions) {
    //请求路径为/eureka/apps/delta
    return getApplicationsInternal("apps/delta", regions);
}

//AbstractJerseyEurekaHttpClient#getVip
@Override
public EurekaHttpResponse<Applications> getVip(String vipAddress, String... regions) {
    //请求路径为/eureka/vips/
    return getApplicationsInternal("vips/" + vipAddress, regions);
}

查看getApplicationsInternal方法:

//AbstractJerseyEurekaHttpClient#getApplicationsInternal
private EurekaHttpResponse<Applications> getApplicationsInternal(String urlPath, String[] regions) {
    ClientResponse response = null;
    String regionsParamValue = null;
    try {
        //使用Jersey客户端发送请求
        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();
        }
    }
}

通过跟踪调试,在该方法内会发现会发送相关的请求url,接口路径为/eureka/apps,请求方式为GET,如图所示:

拉取全量注册表信息的请求url

2)、增量式拉取注册表信息代码如下:

//DiscoveryClient#getAndUpdateDelta
private void getAndUpdateDelta(Applications applications) throws Throwable {
    long currentUpdateGeneration = fetchRegistryGeneration.get();

    Applications delta = null;
    //发送增量式拉取注册表信息请求
    EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
    if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
        delta = httpResponse.getEntity();
    }

    if (delta == null) {
        logger.warn("The server does not allow the delta revision to be applied because it is not safe. "
                    + "Hence got the full registry.");
        getAndStoreFullRegistry();
    } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
        String reconcileHashCode = "";
        if (fetchRegistryUpdateLock.tryLock()) {
            try {
                updateDelta(delta);
                reconcileHashCode = getReconcileHashCode(applications);
            } finally {
                fetchRegistryUpdateLock.unlock();
            }
        } else {
            logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
        }
        // There is a diff in number of instances for some reason
        if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
            //如果hashCode不一致,则进行全量式拉取
            reconcileAndLogDifference(delta, reconcileHashCode);  // this makes a remoteCall
        }
    } else {
        logger.warn("Not updating application delta as another thread is updating it already");
        logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode());
    }
}

增量式拉取方式,一般发生在第一次全量拉取注册表信息之后,拉取的信息定义为从某一段时间之后发生的所有变更信息。增量式拉取的目的是为了维护 Eureka Client 本地的注册表信息俞 Eureka Server 注册表信息的一致性,防止数据过久而失效,同时采用增量式拉取的方式减少了拉取注册表信息的通信量。Eureka Client 中有一个注册表缓存刷新定时器TimedSupervisorTask类型的 cacheRefreshTask专门负责维护两者之间信息的同步性。但是当增量式拉取出现意外时,定时器将执行全量拉取以更新本地缓存的注册表信息。

回到上述的增量式拉取注册表信息的代码中getDelta同样调用getApplicationsInternal方法,请求路径为/eureka/delta。如果获取失败,会进行全量拉取注册表信息,否则就通过CAS判断一致性,如果一致则更新本地缓存并计算应用的一致性hashCode。最后再判断计算出来的hashCode和 Eureka Server传递的delta上的appsHashCode进行比较,比对客户端和服务端上注册表的差异。如果不一致,将再次调用reconcileAndLogDifference全量式拉取注册表数据保证 Eureka ServerEureka Client 之间注册表数据的一致。

//DiscoveryClient#reconcileAndLogDifference
private void reconcileAndLogDifference(Applications delta, String reconcileHashCode) throws Throwable {
    logger.debug("The Reconcile hashcodes do not match, client : {}, server : {}. Getting the full registry",
                 reconcileHashCode, delta.getAppsHashCode());

    RECONCILE_HASH_CODES_MISMATCH.increment();

    long currentUpdateGeneration = fetchRegistryGeneration.get();

    //全量式拉取注册表信息
    EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
        ? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
        : eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
    Applications serverApps = httpResponse.getEntity();

    if (serverApps == null) {
        logger.warn("Cannot fetch full registry from the server; reconciliation failure");
        return;
    }

    if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
        localRegionApps.set(this.filterAndShuffle(serverApps));
        getApplications().setVersion(delta.getVersion());
        logger.debug(
            "The Reconcile hashcodes after complete sync up, client : {}, server : {}.",
            getApplications().getReconcileHashCode(),
            delta.getAppsHashCode());
    } else {
        logger.warn("Not setting the applications map as another thread has advanced the update generation");
    }
}

仔细观察reconcileAndLogDifference就会发现,它同getAndStoreFullRegistry的逻辑非常相似,在此就不累赘了。

3.7.6、服务注册

拉取完 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);
    }
}

//DiscoveryClient#register
//注册方法
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());
    }
    //Status.NO_CONTENT.getStatusCode()==204
    return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}

Eureka Client 会将自身服务实例元数据封装在 InstanceInfo对象中,并发送到 Eureka Server中进行服务注册请求。

InstanceInfo元数据

Eureka Server 返回 204 状态码时,说明服务注册成功。

进入到eurekaTransport.registrationClient.register(...)方法内,可以观察到 Eureka Client 的请求路径为/eureka/apps/${APP_NAME},使用POST请求方式。

在这里插入图片描述

3.7.7、初始化定时器

接着,服务注册完成之后,代码到了下面这一行:

// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
initScheduledTasks();

从英文翻译可以获取到它进行了初始化定时任务。Eureka Client为了维持自己在 Eureka Server 注册表上的租约,需要通过发送心跳的方式与 Eureka Server 进行通信。同时 Eureka Server 注册表中的服务实例也是动态变化的,为了保持 Eureka ClientEureka Server 的注册表信息的一致性, Eureka Client 要定时向Eureka Server 拉取注册表信息并更新本地缓存。 为了监控Eureka Client 应用信息和状态的变化, Eureka Client 设置了一个按需注册定时器,定时检查应用信息或者状态的变化, 并在发生变化时向 Eureka Server 重新注册,避免注册表中的本服务实例信息不可用。

//DiscoveryClient#initScheduledTasks
private void initScheduledTasks() {
    if (clientConfig.shouldFetchRegistry()) {
        // registry cache refresh timer
        //注册表缓存刷新定时器
        int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
        int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        //缓存刷新定时器
        cacheRefreshTask = new TimedSupervisorTask(
            "cacheRefresh",
            scheduler,
            cacheRefreshExecutor,
            registryFetchIntervalSeconds,
            TimeUnit.SECONDS,
            expBackOffBound,
            new CacheRefreshThread()
        );
        scheduler.schedule(
            cacheRefreshTask,
            registryFetchIntervalSeconds, TimeUnit.SECONDS);
    }

    if (clientConfig.shouldRegisterWithEureka()) {
        int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);

        // Heartbeat timer
        //心跳定时器
        heartbeatTask = new TimedSupervisorTask(
            "heartbeat",
            scheduler,
            heartbeatExecutor,
            renewalIntervalInSecs,
            TimeUnit.SECONDS,
            expBackOffBound,
            new HeartbeatThread()
        );
        scheduler.schedule(
            heartbeatTask,
            renewalIntervalInSecs, TimeUnit.SECONDS);

        // InstanceInfo replicator
       	//按需注册定时器
        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();
            }
        };

        if (clientConfig.shouldOnDemandUpdateStatusChange()) {
            applicationInfoManager.registerStatusChangeListener(statusChangeListener);
        }

        instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

DiscoveryClientinitScheduledTasks方法中初始化了三个定时器任务,一个用于向 Eureka Server 拉取注册表信息刷新本地缓存;一个用于向Eureka Server 发送心跳;一个用于进行按需注册的操作。

通过 ScheduledExecutorServiceschedule的方式提交缓存刷新定时任务和发送心跳定时任务,任务执行的方式为延时执行并且不循环,而这两个任务的定时循环逻辑由TimedSupervisorTask来实现。而TimedSupervisorTask继承了TimerTask,提供了执行定时任务的功能,具体定时任务的逻辑在run方法中:

//TimedSupervisorTask#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();

        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()) {
            scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
        }
    }
}

run方法中存在以下的任务调度过程:

  • scheduler初始化并延迟执行TimedSupervisorTask
  • TimedSupervisorTasktask 提交给executor中执行,taskexecutor在初始化TimedSupervisorTask时传入;
  • task正常执行,TimedSupervisorTask将自己提交到scheduler,延迟delay时间后再次执行;
  • task执行超时,计算新的delay,TimedSupervisorTask将自己提交到scheduler,延迟delay时间后再执行。

TimedSupervisorTask通过这种不断循环提交任务的方式,完成定时执行任务的要求。

DiscoveryClientinitScheduledTasks方法中,提交缓存刷新的定时任务的线程为CacheRefreshThread,提交发送心跳定时任务的线程为HeartbeatThread,两者均继承自Runnable

//缓存刷新定时任务线程
class CacheRefreshThread implements Runnable {
    public void run() {
        refreshRegistry();
    }
}

//发送心跳定时任务线程
private class HeartbeatThread implements Runnable {
    public void run() {
        if (renew()) {
            lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
        }
    }
}
3.7.7.1、缓存刷新定时任务&发送心跳定时任务

缓存刷新定时任务执行的逻辑代码为:

//DiscoveryClient#refreshRegistry
void refreshRegistry() {
    try {
        boolean isFetchingRemoteRegionRegistries = isFetchingRemoteRegionRegistries();

        boolean remoteRegionsModified = false;
        // This makes sure that a dynamic change to remote regions to fetch is honored.
        String latestRemoteRegions = clientConfig.fetchRegistryForRemoteRegions();
        if (null != latestRemoteRegions) {
            String currentRemoteRegions = remoteRegionsToFetch.get();
            if (!latestRemoteRegions.equals(currentRemoteRegions)) {
                // Both remoteRegionsToFetch and AzToRegionMapper.regionsToFetch need to be in sync
                synchronized (instanceRegionChecker.getAzToRegionMapper()) {
                    if (remoteRegionsToFetch.compareAndSet(currentRemoteRegions, latestRemoteRegions)) {
                        String[] remoteRegions = latestRemoteRegions.split(",");
                        remoteRegionsRef.set(remoteRegions);
                        instanceRegionChecker.getAzToRegionMapper().setRegionsToFetch(remoteRegions);
                        remoteRegionsModified = true;
                    } else {
                        logger.info("Remote regions to fetch modified concurrently," +
                                    " ignoring change from {} to {}", currentRemoteRegions, latestRemoteRegions);
                    }
                }
            } else {
                // Just refresh mapping to reflect any DNS/Property change
                instanceRegionChecker.getAzToRegionMapper().refreshMapping();
            }
        }

        boolean success = fetchRegistry(remoteRegionsModified);
        if (success) {
            registrySize = localRegionApps.get().size();
            lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
        }

        if (logger.isDebugEnabled()) {
            StringBuilder allAppsHashCodes = new StringBuilder();
            allAppsHashCodes.append("Local region apps hashcode: ");
            allAppsHashCodes.append(localRegionApps.get().getAppsHashCode());
            allAppsHashCodes.append(", is fetching remote regions? ");
            allAppsHashCodes.append(isFetchingRemoteRegionRegistries);
            for (Map.Entry<String, Applications> entry : remoteRegionVsApps.entrySet()) {
                allAppsHashCodes.append(", Remote region: ");
                allAppsHashCodes.append(entry.getKey());
                allAppsHashCodes.append(" , apps hashcode: ");
                allAppsHashCodes.append(entry.getValue().getAppsHashCode());
            }
            logger.debug("Completed cache refresh task for discovery. All Apps hash code is {} ",
                         allAppsHashCodes);
        }
    } catch (Throwable e) {
        logger.error("Cannot fetch registry from server", e);
    }
}

CacheRefreshThread 依托fetchRegistry方式进行缓存刷新,具体逻辑可以在之前的章节中(#3.7.5)查看。

HeartbeatThread通过定时发送心跳请求,维持在 Eureka Server注册表中的租约。

//DiscoveryClient#renew
boolean renew() {
    EurekaHttpResponse<InstanceInfo> httpResponse;
    try {
        //发送心跳请求,参数为服务名,服务id和服务实例
        httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
        logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
        
        //如果响应码为404,则将当前实例进行注册
        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;
    }
}

方法内一开始使用Jersey客户端进行发送心跳的请求,根据续租提交的appName,instanceId来更新注册表中的服务实例信息。当注册表中不存在当前该服务实例时,将返回404状态码,同时发送请求的 Eureka Client 会进行重新注册;如果续约成功,则返回200状态码。

//AbstractJerseyEurekaHttpClient#sendHeartBeat
public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
    String urlPath = "apps/" + appName + '/' + id;
    ClientResponse response = null;
    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() &&
            !HTML.equals(response.getType().getSubtype())) { //don't try and deserialize random html errors from the server
            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();
        }
    }
}

sendHeartBeat方法中,可以发现服务续约调用的接口以及传递的参数:

Eureka Client续约请求

接口地址为apps/${APP_NAMAE}/${INSTANCEINFO_ID},方法为PUT请求,参数主要有statuslastDirtyTimestampoverriddenStatus

3.7.7.2、按时注册定时任务

按需注册定时任务的作用是,当 Eureka Client 中的 InstanceInfo 或者 status 发生变化时,重新向 Eureka Server 发起注册请求,更新注册表中的服务实例信息,保证 Eureka Server 注册表中服务实例信息有效性和可用性。按需注册定时任务的代码如下:

// InstanceInfo replicator
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();
    }
};

if (clientConfig.shouldOnDemandUpdateStatusChange()) {
    applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}

instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());

按需注册定义了一个定时任务,同时也注册了状态改变监控器,在应用状态发生变化时,刷新服务实例信息和检查应用状态的变化,在服务实例信息发生改变的情况下向 Eureka Server 重新发起注册操作。而状态改变监控器的主要逻辑在 InstanceInfoReplicatorrun方法中:

//InstanceInfoReplicator#run
public void run() {
    try {
        //服务实例刷新
        discoveryClient.refreshInstanceInfo();

        Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
        if (dirtyTimestamp != null) {
            //如果有dirty标记,则对服务进行注册
            discoveryClient.register();
            //将dirty标记清除掉
            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);
    }
}
//DiscoveryClient#refreshInstanceInfo
void refreshInstanceInfo() {
    //刷新数据中心
    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);
    }
}

run方法中,首先调用了DiscoveryClient中的refreshInstanceInfo方法刷新当前服务实例信息,查看当前服务实例信息和状态是否发生了改变。如果发生变化则会向 Eureka Server 重新对服务实例进行注册,同时变更状态,最后在finally代码块中,定义了下一次的延时任务,用于再次调用run方法。还记得在 Eureka 原生接口中定义了HealthCheckHandler健康检查器,通过getStatus方法,结合Spring Cloud中的 Actuator进行状态检测。

//EurekaHealthCheckHandler#getStatus
public InstanceStatus getStatus(InstanceStatus instanceStatus) {
    return getHealthStatus();
}

//EurekaHealthCheckHandler#getHealthStatus
protected InstanceStatus getHealthStatus() {
    final Status status;
    if (statusAggregator != null) {
        status = getStatus(statusAggregator);
    }
    else {
        status = getStatus(getHealthIndicator());
    }
    return mapToInstanceStatus(status);
}

refreshInstanceInfo方法中,如果状态发生改变,则会触发事件:

//ApplicationInfoManager.java
public synchronized void setInstanceStatus(InstanceStatus status) {
    InstanceStatus next = instanceStatusMapper.map(status);
    if (next == null) {
        return;
    }

    InstanceStatus prev = instanceInfo.setStatus(next);
    if (prev != null) {
        //循环遍历事件变更监听器的notify方法
        for (StatusChangeListener listener : listeners.values()) {
            try {
                listener.notify(new StatusChangeEvent(prev, next));
            } catch (Exception e) {
                logger.warn("failed to notify listener: {}", listener.getId(), e);
            }
        }
    }
}

记得在initScheduledTasks方法中声明了一个状态变更监听器,重写了getIdnotify方法,上述服务一旦状态发生了变更,则会触发该监听器。监听器的逻辑中会触发InstanceInfoReplicatoronDemandUpdate方法,方法中提交了一个线程,而该线程其实还是通过InstanceInfoReplicatorrun方法来实现。

//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);
                    }

                    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;
    }
}

同时为了防止重复执行run方法,该方法内会判断上次已提交的但未完成的任务,如果未完成则会先执行cancel方法取消上次的任务,再执行最新的按需注册任务。

initScheduledTasks最后会启动按需注册的定时任务,

//DiscoveryClient#initScheduledTasks
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());

//InstanceInfoReplicator#start
public void start(int initialDelayMs) {
    if (started.compareAndSet(false, true)) {
        instanceInfo.setIsDirty();  // for initial register
        Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
        scheduledPeriodicRef.set(next);
    }
}

//InstanceInfo#setIsDirty
public synchronized void setIsDirty() {
    isInstanceInfoDirty = true;//dirty标记
    lastDirtyTimestamp = System.currentTimeMillis();
}
3.7.8、服务下线

当应用服务在关闭的时候, Eureka Client 会向 Eureka Server 注销自身在注册表中的信息,由**@PreDestroy** 注解标识 DiscoveryClient 中对象销毁前执行的清理方法,代码如下:

//DiscoveryClient#shutdown
@PreDestroy
@Override
public synchronized void shutdown() {
    //同步操作,确保只会执行一次
    if (isShutdown.compareAndSet(false, true)) {
        logger.info("Shutting down DiscoveryClient ...");

        //注销状态变更监听器StatusChangeListener
        if (statusChangeListener != null && applicationInfoManager != null) {    applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
        }

        //取消定时任务
        cancelScheduledTasks();

        // If APPINFO was registered
        if (applicationInfoManager != null
            && clientConfig.shouldRegisterWithEureka()
            && clientConfig.shouldUnregisterOnShutdown()) {
            //标记服务状态为DOWN
            applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
            //服务实例注销
            unregister();
        }

        if (eurekaTransport != null) {
            eurekaTransport.shutdown();
        }

        heartbeatStalenessMonitor.shutdown();
        registryStalenessMonitor.shutdown();

        Monitors.unregisterObject(this);

        logger.info("Completed shut down of DiscoveryClient");
    }
}

在销毁DiscoveryClient之前,会进行一系列清理工作,包括注销 ApplicationInfoManager中的 StatusChangeListener、取消定时任务、服务下线和关闭Jersey客户端。主要关注unregister服务下线方法,

//DiscoveryClient#unregister
void unregister() {
    // It can be null if shouldRegisterWithEureka == false
    if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
        try {
            logger.info("Unregistering ...");
            EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
            logger.info(PREFIX + "{} - deregister  status: {}", appPathIdentifier, httpResponse.getStatusCode());
        } catch (Exception e) {
            logger.error(PREFIX + "{} - de-registration failed{}", appPathIdentifier, e.getMessage(), e);
        }
    }
}
//AbstractJerseyEurekaHttpClient#cancel
public EurekaHttpResponse<Void> cancel(String appName, String id) {
    String urlPath = "apps/" + appName + '/' + id;
    ClientResponse response = null;
    try {
        Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
        addExtraHeaders(resourceBuilder);
        response = resourceBuilder.delete(ClientResponse.class);
        return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP DELETE {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}

通过跟踪调试,可以发现服务下线调用的接口地址为apps/${APP_NAME}/${INSTANCEINFO_ID},HTTP请求方式为DELETE


欢迎关注 微信公众号:Java知识集训

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值