上一节讲述了 Eureka Server 的原理及部分源码,今天咱们来看看 Eureka Client 端的源码,功能点类似 Eureka Server。
3.7、Eureka Client 源码分析
Eureka Client 通过 Starter 的方式引入依赖, SpringBoot 将会为项目使用以下的自动配置类:
- EurekaClientAutoConfiguration:Eureka Client 自动配置类,负责 Eureka Client 中关键Bean的配置和初始化;
- RibbonEurekaAutoConfiguration:Ribbon 负载均衡相关配置;
- EurekaDiscoveryClientConfiguration:配置自动注册、服务发现和应用的健康检查器。
3.7.1、读取应用自身配置信息
DiscoveryClient 是 Spring 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
,是不是有点迷糊了。
仔细看代码,就会发现 Spring Cloud 中 DiscoveryClient 接口中的几个方法都是依靠 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
中的 HealthAggregator 和 HealthIndicator,用于监测服务实例的状态。
而 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-registry
和 eureka.client.register-with-eureka
。如果eureka.client.fetch-registry
为true
的时候表示 Eureka Client 将从 Eureka Server 中拉取注册表信息。而eureka.client.register-with-eureka
为true
表示 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、拉取注册表信息
如果 EurekaClientConfig 的 shouldFetchRegistry
为true
时, fetchRegistry
方法将会被调用 。在Eureka Client 向 Eureka 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
,如图所示:
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 Server 与 Eureka 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中进行服务注册请求。
当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 Client 与 Eureka 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");
}
}
在DiscoveryClient的initScheduledTasks
方法中初始化了三个定时器任务,一个用于向 Eureka Server 拉取注册表信息刷新本地缓存;一个用于向Eureka Server 发送心跳;一个用于进行按需注册的操作。
通过 ScheduledExecutorService 的schedule
的方式提交缓存刷新定时任务和发送心跳定时任务,任务执行的方式为延时执行并且不循环,而这两个任务的定时循环逻辑由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;- TimedSupervisorTask将
task
提交给executor
中执行,task
和executor
在初始化TimedSupervisorTask时传入; - 若
task
正常执行,TimedSupervisorTask将自己提交到scheduler
,延迟delay
时间后再次执行; - 若
task
执行超时,计算新的delay
,TimedSupervisorTask将自己提交到scheduler
,延迟delay
时间后再执行。
TimedSupervisorTask通过这种不断循环提交任务的方式,完成定时执行任务的要求。
在DiscoveryClient的initScheduledTasks
方法中,提交缓存刷新的定时任务的线程为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
方法中,可以发现服务续约调用的接口以及传递的参数:
接口地址为apps/${APP_NAMAE}/${INSTANCEINFO_ID}
,方法为PUT
请求,参数主要有status
、lastDirtyTimestamp
、overriddenStatus
。
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 重新发起注册操作。而状态改变监控器的主要逻辑在 InstanceInfoReplicator 的 run
方法中:
//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
方法中声明了一个状态变更监听器,重写了getId
和notify
方法,上述服务一旦状态发生了变更,则会触发该监听器。监听器的逻辑中会触发InstanceInfoReplicator 的 onDemandUpdate
方法,方法中提交了一个线程,而该线程其实还是通过InstanceInfoReplicator 的run
方法来实现。
//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
。