【SpringCloud】Eureka Client源码分析

上一节Eureka Server 源码分析讲述了 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. */    //获取所有服务的实例idList<String> getServices();/** * Default implementation for getting order of discovery clients. * @return order */@Overridedefault 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,是不是有点迷糊了。

SpringCloud源码构建 dockerfile模版 springcloud源码分析_ide

仔细看代码,就会发现 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#fireEventprotected 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 启动阶段的各项工作。

@InjectDiscoveryClient(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 cacheRefreshscheduler = 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 handoffcacheRefreshExecutor = 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#fetchRegistryprivate 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#getAndStoreFullRegistryprivate 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@Overridepublic EurekaHttpResponse<Applications> getApplications(String... regions) {    //请求路径为/eureka/apps/    return getApplicationsInternal("apps/", regions);}//AbstractJerseyEurekaHttpClient#getDelta@Overridepublic EurekaHttpResponse<Applications> getDelta(String... regions) {    //请求路径为/eureka/apps/delta    return getApplicationsInternal("apps/delta", regions);}//AbstractJerseyEurekaHttpClient#getVip@Overridepublic EurekaHttpResponse<Applications> getVip(String vipAddress, String... regions) {    //请求路径为/eureka/vips/    return getApplicationsInternal("vips/" + vipAddress, regions);}

查看getApplicationsInternal方法:

//AbstractJerseyEurekaHttpClient#getApplicationsInternalprivate 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,如图所示:

SpringCloud源码构建 dockerfile模版 springcloud源码分析_ide_02

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

//DiscoveryClient#getAndUpdateDeltaprivate 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#reconcileAndLogDifferenceprivate 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中进行服务注册请求。

SpringCloud源码构建 dockerfile模版 springcloud源码分析_Server_03

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

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

SpringCloud源码构建 dockerfile模版 springcloud源码分析_ide_04

3.7.7、初始化定时器

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

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

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

//DiscoveryClient#initScheduledTasksprivate 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#runpublic 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#refreshRegistryvoid 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#renewboolean 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#sendHeartBeatpublic 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方法中,可以发现服务续约调用的接口以及传递的参数:

SpringCloud源码构建 dockerfile模版 springcloud源码分析_List_05

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

3.7.7.2、按时注册定时任务

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

// InstanceInfo replicatorinstanceInfoReplicator = new InstanceInfoReplicator(    this,    instanceInfo,    clientConfig.getInstanceInfoReplicationIntervalSeconds(),    2); // burstSizestatusChangeListener = 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#runpublic 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#refreshInstanceInfovoid 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#getStatuspublic InstanceStatus getStatus(InstanceStatus instanceStatus) {    return getHealthStatus();}//EurekaHealthCheckHandler#getHealthStatusprotected InstanceStatus getHealthStatus() {    final Status status;    if (statusAggregator != null) {        status = getStatus(statusAggregator);    }    else {        status = getStatus(getHealthIndicator());    }    return mapToInstanceStatus(status);}

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

//ApplicationInfoManager.javapublic 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#onDemandUpdatepublic 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#initScheduledTasksinstanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());//InstanceInfoReplicator#startpublic 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#setIsDirtypublic synchronized void setIsDirty() {    isInstanceInfoDirty = true;//dirty标记    lastDirtyTimestamp = System.currentTimeMillis();}
3.7.8、服务下线

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

//DiscoveryClient#shutdown@PreDestroy@Overridepublic 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#unregistervoid 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#cancelpublic 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

SpringCloud源码构建 dockerfile模版 springcloud源码分析_源码分析_06

更多分享正在路上,尽情期待...