Spring Cloud Netflix之Eureka源码系列文章一共分为六个片段
Spring Cloud Netflix-Eureka(一)、服务注册与发现
Spring Cloud Netflix-Eureka(二)、信息存储原理
Spring Cloud Netflix-Eureka(三)、自我保护机制
Spring Cloud Netflix-Eureka(四)、心跳续约机制
Spring Cloud Netflix-Eureka(五)、多级缓存机制
Spring Cloud Netflix-Eureka(六)、集群数据同步
Spring Cloud Netflix-Eureka、服务注册
一、服务注册
1.1 服务注册机制
Spring Cloud 是一个生态,它提供了一套标准,这套标准可以通过不同的组件来实现,其中就包含服务注册/发现、熔断、负载均衡等,在 spring-cloud-common
这个包中,org.springframework.cloud.client.serviceregistry
路径下,可以看到一个服务注册的接口定义 ServiceRegistry。它就是定义了 Spring Cloud 中服务注册的一个接口。
package org.springframework.cloud.client.serviceregistry;
public interface ServiceRegistry<R extends Registration> {
// 服务上线
void register(R registration);
// 服务下线
void deregister(R registration);
void close();
void setStatus(R registration, String status);
<T> T getStatus(R registration);
}
我们看一下它的类关系图,这个接口有一个唯一的实现 EurekaServiceRegistry。表示采用的是 Eureka Server 作为服务注册中心。
通过调用链路我们会发现,EurekaServiceRegistry.register(R registration)
是在一个叫 EurekaAutoServiceRegistration 的类中被调用,EurekaAutoServiceRegistration 类实现了 SmartLifecycle 接口,会在 Spring 容器加载完成后,调用当前类中的 start()
方法,完成扩展组件的一些自定义逻辑。
1.2 EurekaAutoServiceRegistration
EurekaAutoServiceRegistration 通过 Spring 的 SPI 机制的 EurekaClientAutoConfiguration 类被装载到 Spring 容器中,并在容器初始化完成后,进行 start()
方法调用。
@Configuration(proxyBeanMethods = false)
public class EurekaClientAutoConfiguration {
@Bean
public EurekaServiceRegistry eurekaServiceRegistry() {
return new EurekaServiceRegistry();
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(
value = "spring.cloud.service-registry.auto-registration.enabled",
matchIfMissing = true)
public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(
ApplicationContext context, EurekaServiceRegistry registry,
EurekaRegistration registration) {
return new EurekaAutoServiceRegistration(context, registry, registration);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(
value = "spring.cloud.service-registry.auto-registration.enabled",
matchIfMissing = true)
public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient,
CloudEurekaInstanceConfig instanceConfig,
ApplicationInfoManager applicationInfoManager, @Autowired(
required = false) ObjectProvider<HealthCheckHandler> healthCheckHandler) {
return EurekaRegistration.builder(instanceConfig).with(applicationInfoManager)
.with(eurekaClient).with(healthCheckHandler).build();
}
}
1.3 EurekaAutoServiceRegistration.start()
EurekaAutoServiceRegistration 接口实现了 Spring 的扩展接口 SmartLifecycle,会在 Spring 容器启动完成后,调用 start() 方法。
public class EurekaAutoServiceRegistration implements AutoServiceRegistration, SmartLifecycle, Ordered, SmartApplicationListener {
@Override
public void start() {
// only set the port if the nonSecurePort or securePort is 0 and this.port != 0
if (this.port.get() != 0) {
if (this.registration.getNonSecurePort() == 0) {
this.registration.setNonSecurePort(this.port.get());
}
if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
this.registration.setSecurePort(this.port.get());
}
}
// only initialize if nonSecurePort is greater than 0 and it isn't already running
// because of containerPortInitializer below
if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
// 进行服务注册
this.serviceRegistry.register(this.registration);
// 发布一个事件
this.context.publishEvent(new InstanceRegisteredEvent<>(this,
this.registration.getInstanceConfig()));
this.running.set(true);
}
}
@Override
public void stop() {
this.serviceRegistry.deregister(this.registration);
this.running.set(false);
}
}
this.serviceRegistry.register(this.registration);
实际调用的是 EurekaServiceRegistry 这个对象中的 register() 方法,具体代码如下。
public class EurekaServiceRegistry implements ServiceRegistry<EurekaRegistration> {
@Override
public void register(EurekaRegistration reg) {
maybeInitializeClient(reg);
if (log.isInfoEnabled()) {
log.info("Registering application "
+ reg.getApplicationInfoManager().getInfo().getAppName()
+ " with eureka with status "
+ reg.getInstanceConfig().getInitialStatus());
}
// 设置当前实例的状态,一旦这个实例的状态发生变化,只要状态不是DOWN,那么就会被监听器监听并且执行服务注册。
reg.getApplicationInfoManager().setInstanceStatus(reg.getInstanceConfig().getInitialStatus());
// 设置健康检查的处理
reg.getHealthCheckHandler().ifAvailable(healthCheckHandler -> reg.getEurekaClient()
.registerHealthCheck(healthCheckHandler));
}
}
从上述代码来看,注册方法中并没有真正调用 Eureka 的方法去执行注册,而是仅仅设置了一个状态以及设置健康检查处理器。下面看一下 reg.getApplicationInfoManager().setInstanceStatus(InstanceStatus status)
方法,setInstanceStatus()
方法通过监听器来发布一个状态变更事件,服务的注册核心逻辑在 StatusChangeListener 中的 notify()
方法中执行。
public synchronized void setInstanceStatus(InstanceStatus status) {
InstanceStatus next = instanceStatusMapper.map(status);
if (next == null) {
return;
}
InstanceStatus prev = instanceInfo.setStatus(next);
if (prev != null) {
for (StatusChangeListener listener : listeners.values()) {
try {
// 通过监听器来发布一个状态变更事件
listener.notify(new StatusChangeEvent(prev, next));
} catch (Exception e) {
logger.warn("failed to notify listener: {}", listener.getId(), e);
}
}
}
}
1.4 StatusChangeListener.notify()
StatusChangeListener 是一个静态内部接口。
public static interface StatusChangeListener {
String getId();
void notify(StatusChangeEvent statusChangeEvent);
}
所以在上面进行 StatusChangeListener 遍历调用时,我们需要找到 StatusChangeListener 的具体对象是那一个,下面看一下 listeners 是怎么被赋值的。
public class ApplicationInfoManager {
protected final Map<String, StatusChangeListener> listeners;
@Inject
public ApplicationInfoManager(EurekaInstanceConfig config, InstanceInfo instanceInfo, OptionalArgs optionalArgs) {
this.config = config;
this.instanceInfo = instanceInfo;
// 进行 listeners 赋值,初始为一个空的 ConcurrentHashMap。
this.listeners = new ConcurrentHashMap<String, StatusChangeListener>();
if (optionalArgs != null) {
this.instanceStatusMapper = optionalArgs.getInstanceStatusMapper();
} else {
this.instanceStatusMapper = NO_OP_MAPPER;
}
// Hack to allow for getInstance() to use the DI'd ApplicationInfoManager
instance = this;
}
}
看到这里小伙伴是不是有点儿懵?不要慌,下面我们看看 Map<String, StatusChangeListener> listeners
是需要在哪里被 put 值的。
public class ApplicationInfoManager {
protected final Map<String, StatusChangeListener> listeners;
// 注册状态变化监听器
public void registerStatusChangeListener(StatusChangeListener listener) {
listeners.put(listener.getId(), listener);
}
}
通过调用链路你会发现,ApplicationInfoManager.registerStatusChangeListener(StatusChangeListener listener)
只有在一个叫 DiscoveryClient 的对象中被调用。
1.5 DiscoveryClient
上面我们跟踪到了 ApplicationInfoManager.registerStatusChangeListener(StatusChangeListener listener)
方法会在 DiscoveryClient 的构造函数中进行调用,辣么 DiscoveryClient 又是在哪里初始化的呢???经过寻找,我们发现 DiscoveryClient 并没有在那个地方进行创建,根据自己的开发经验总结,是不是 DiscoveryClient 的子类中进行了 super()
构造函数的调用呢???下面我们先看一下 DiscoveryClient 的类结构图。
会发现 DiscoveryClient 只有一个子类 CloudEurekaClient,通过查找,我们会在 EurekaClientAutoConfiguration 中发现 CloudEurekaClient 被注入的代码逻辑。
@Configuration(proxyBeanMethods = false)
public class EurekaClientAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingRefreshScope
protected static class EurekaClientConfiguration {
@Autowired
private ApplicationContext context;
@Autowired
private AbstractDiscoveryClientOptionalArgs<?> optionalArgs;
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(value = EurekaClient.class,
search = SearchStrategy.CURRENT)
public EurekaClient eurekaClient(ApplicationInfoManager manager,
EurekaClientConfig config) {
return new CloudEurekaClient(manager, config, this.optionalArgs,
this.context);
}
@Bean
@ConditionalOnMissingBean(value = ApplicationInfoManager.class,
search = SearchStrategy.CURRENT)
public ApplicationInfoManager eurekaApplicationInfoManager(
EurekaInstanceConfig config) {
InstanceInfo instanceInfo = new InstanceInfoFactory().create(config);
return new ApplicationInfoManager(config, instanceInfo);
}
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(
value = "spring.cloud.service-registry.auto-registration.enabled",
matchIfMissing = true)
public EurekaRegistration eurekaRegistration(EurekaClient eurekaClient,
CloudEurekaInstanceConfig instanceConfig,
ApplicationInfoManager applicationInfoManager, @Autowired(
required = false) ObjectProvider<HealthCheckHandler> healthCheckHandler) {
return EurekaRegistration.builder(instanceConfig).with(applicationInfoManager)
.with(eurekaClient).with(healthCheckHandler).build();
}
}
}
CloudEurekaClient 的构造方法
public CloudEurekaClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config,
AbstractDiscoveryClientOptionalArgs<?> args, ApplicationEventPublisher publisher) {
super(applicationInfoManager, config, args);
this.applicationInfoManager = applicationInfoManager;
this.publisher = publisher;
this.eurekaTransportField = ReflectionUtils.findField(DiscoveryClient.class,
"eurekaTransport");
ReflectionUtils.makeAccessible(this.eurekaTransportField);
}
果不其然,我们在 CloudEurekaClient 构造函数中发现了 super(applicationInfoManager, config, args);
的代码,也就是 DiscoveryClient 的构造函数调用。
DiscoveryClient 为 Eureka 启动的核心类,进行了一系列初始化,如初始化心跳的线程池、缓存刷新的线程池、心跳检测的线程、实例数据更新的线程等等。
下面我们看一下 DiscoveryClient 构造函数的具体代码。
@Singleton
public class DiscoveryClient implements EurekaClient {
@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(","));
// 是否要从 Eureka Server 上获取服务地址信息
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;
}
// 当前服务启动后,是否要注册到 Eureka Server上
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();
initRegistrySize = this.getApplications().size();
registrySize = initRegistrySize;
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
initTimestampMs, initRegistrySize);
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);
}
// 如果需要获取 Eureka Server 中的服务地址,则调用 fetchRegistry() 方法进行拉取
if (clientConfig.shouldFetchRegistry()) {
try {
// 从 Eureka Server 中拉取注册地址信息
// 注意:服务发现的核心流程
boolean primaryFetchRegistryResult = fetchRegistry(false);
if (!primaryFetchRegistryResult) {
logger.info("Initial registry fetch from primary servers failed");
}
// 从备用地址拉取服务注册信息
boolean backupFetchRegistryResult = true;
if (!primaryFetchRegistryResult && !fetchRegistryFromBackup()) {
backupFetchRegistryResult = false;
logger.info("Initial registry fetch from backup servers failed");
}
// 如果还是没有拉取到,并且配置了强制拉取注册表的话,就会抛异常
if (!primaryFetchRegistryResult && !backupFetchRegistryResult && clientConfig.shouldEnforceFetchRegistryAtInit()) {
throw new IllegalStateException("Fetch registry error at startup. Initial fetch failed.");
}
} catch (Throwable th) {
logger.error("Fetch registry error at startup: {}", th.getMessage());
throw new IllegalStateException(th);
}
}
// call and execute the pre registration handler before all background tasks (inc registration) is started
// 判断是否存在预注册处理器,存在就执行
if (this.preRegistrationHandler != null) {
this.preRegistrationHandler.beforeRegistration();
}
// 如果需要注册到Eureka server并且是开启了初始化的时候强制注册,则调用 register()发起服务
// 注册(默认情况下,shouldEnforceRegistrationAtInit为false)
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();
initRegistrySize = this.getApplications().size();
registrySize = initRegistrySize;
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
initTimestampMs, initRegistrySize);
}
}
1.6 DiscoveryClient.initScheduledTasks()
上面 DiscoveryClient 的构造函数中,会调用 initScheduledTasks()
方法,初始化一个定时任务,负责心跳、实例数据更新,具体代码如下。
@Singleton
public class DiscoveryClient implements EurekaClient {
/**
* 初始化一个定时任务,负责心跳、实例数据更新
*/
private void initScheduledTasks() {
// 如果需要获取 Eureka Server 中的服务地址,则会开启 cacheRefreshExecutor 这个定时任务,用于进行缓存数据的刷新
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);
}
// 开启了服务注册到 Eureka Server
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 实例信息复制器
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 (statusChangeEvent.getStatus() == InstanceStatus.DOWN) {
logger.error("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
// 判断实例数据是否发生变化,如果有变化,更新服务注册中心的数据
instanceInfoReplicator.onDemandUpdate();
}
};
// 注册实例状态变化的监听
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
// 启动一个实例信息复制器,主要就是为了开启一个定时线程,每40秒判断实例信息是否变更,如果变更了则重新注册
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
}
在上述代码中,我们发现了一个很重要的逻辑:初始化一个StatusChangeListener,并保存到 ApplicationInfoManager 中的 listeners 集合中。
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
还记得前面源码分析中的服务注册逻辑吗?当服务器启动或者停止时,会遍历 ApplicationInfoManager 中的 protected final Map<String, StatusChangeListener> listeners;
,逐个调用 listener.notify(StatusChangeEvent statusChangeEvent)
方法。
1.7 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");
// 取出之前已经提交的任务,也就是在 EurekaAutoServiceRegistration.start() 方法中提交的更新任务
// 如果任务还没有执行完成,则取消之前的任务
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);
}
// 调用run方法,令任务在延时后执行,相当于周期性任务中的一次
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方法调用register方法进行服务注册,并且在finally中,每30s会定时执行一下当前的run 方法进行检查。
*/
public void run() {
try {
// 刷新实例信息
discoveryClient.refreshInstanceInfo();
// 判断是否有状态更新,如果有,则获取更新的时间
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {// 有脏数据,重新注册
// 注册服务
discoveryClient.register();
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
// 每隔30s,执行一次当前的 run() 方法
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
}
1.9 DiscoveryClient.register()
历尽千辛万苦,我们最终找到了 Eureka 的服务注册方法 DiscoveryClient.register()
中的 eurekaTransport.registrationClient.register(instanceInfo)
@Singleton
public class DiscoveryClient implements EurekaClient {
/**
* 服务注册
*/
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());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
}
通过断点的方式,我们发现最终会调用 AbstractJerseyEurekaHttpClient 中的 EurekaHttpResponse<Void> register(InstanceInfo info)
方法。
public abstract class AbstractJerseyEurekaHttpClient implements EurekaHttpClient {
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
}
很显然,这里是发起了一次 Http 请求,访问 Eureka Server 的 apps/${APP_NAME} 接口,将当 前服务实例的信息发送到 Eureka Server 进行保存。 至此,我们基本上已经知道 Spring Cloud Eureka 是如何在启动的时候把服务信息注册到 Eureka Server上的了。
1.10 总结
服务注册的过程分几个个步骤:
- Spring Cloud 应用在启动时,基于 SmartLifeCycle 接口回调,触发 StatusChangeListener 事件通知。
- StatusChangeListener 对象会在 CloudEurekaClient 对象注入的时候,调用父类(DiscoveryClient)的构造方法进行创建
- 在 StatusChangeListener 的回调方法
notify()
中,通过调用instanceInfoReplicator.onDemandUpdate()
方法,去更新客户端的地址信息,从而完成服务注册。
二、服务发现
上面分析了 Eureka 服务注册的流程,下面我们继续分析一下服务发现的流程。
客户端针对服务发现这个需求点,需要满足以下两个功能:
- 服务启动的时候获取 Eureka Server 中的服务地址列表;
- 在 Eureka Server 端地址发现变化后,需要动态更新本地服务列表;
基于以上两个需求,下面我们具体分析一下 Eureka 是怎么实现的。Eureka 在服务启动是,如果开启了eureka.client.fetch-registry=true
(默认为true)配置,那么 DiscoveryClient 在启动时,就会去服务器上拉取一次地址信息。
2.1 服务启动的时候获取 Eureka Server 中的服务地址列表
2.1.1 DiscoveryClient 构造函数
在上面 DiscoveryClient 构造函数中有这么一段代码 boolean primaryFetchRegistryResult = fetchRegistry(false);
这就是服务发现的核心流程代码,具体如下。
@Singleton
public class DiscoveryClient implements EurekaClient {
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
// 省略其他代码...
// 如果需要获取 Eureka Server 中的服务地址,则调用 fetchRegistry() 方法进行拉取
// 注意:服务发现的核心流程
if (clientConfig.shouldFetchRegistry()) {
try {
// 从 Eureka Server 中拉取注册地址信息
boolean primaryFetchRegistryResult = fetchRegistry(false);
if (!primaryFetchRegistryResult) {
logger.info("Initial registry fetch from primary servers failed");
}
// 从备用地址拉取服务注册信息
boolean backupFetchRegistryResult = true;
if (!primaryFetchRegistryResult && !fetchRegistryFromBackup()) {
backupFetchRegistryResult = false;
logger.info("Initial registry fetch from backup servers failed");
}
// 如果还是没有拉取到,并且配置了强制拉取注册表的话,就会抛异常
if (!primaryFetchRegistryResult && !backupFetchRegistryResult && clientConfig.shouldEnforceFetchRegistryAtInit()) {
throw new IllegalStateException("Fetch registry error at startup. Initial fetch failed.");
}
} catch (Throwable th) {
logger.error("Fetch registry error at startup: {}", th.getMessage());
throw new IllegalStateException(th);
}
}
// 省略其他代码...
}
}
2.1.2 DiscoveryClient.fetchRegistry(boolean forceFullRegistryFetch)
@Singleton
public class DiscoveryClient implements EurekaClient {
/**
* 拉取服务地址
* @param forceFullRegistryFetch 是否需要全量获取
*/
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();
/**
* 判断多个条件,确定是否触发全量更新,如下任一个满足都会全量更新:
* 1. 是否禁用增量更新 clientConfig.shouldDisableDelta()
* 2. 是否对某个region特别关注 (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
* 3. 外部调用时是否通过入参指定全量更新 forceFullRegistryFetch
* 4. 本地还未缓存有效的服务列表信息 (applications == null) || (applications.getRegisteredApplications().size() == 0) || (applications.getVersion() == -1)
*/
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);
}
applications.setAppsHashCode(applications.getReconcileHashCode());
logTotalInstances();
} catch (Throwable e) {
logger.info(PREFIX + "{} - was unable to refresh its cache! This periodic background refresh will be retried in {} seconds. status = {} stacktrace = {}",
appPathIdentifier, clientConfig.getRegistryFetchIntervalSeconds(), e.getMessage(), ExceptionUtils.getStackTrace(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
/**
* 根据缓存中保存的数据更新判断是否需要更新 lastRemoteInstanceStatus
* 1.检查刚刚更新的缓存中,有来自Eureka server的服务列表,其中包含了当前应用的状态
* 2.当前实例的成员变量lastRemoteInstanceStatus,记录的是最后一次更新的当前应用状态
* 上述两种状态在updateInstanceRemoteStatus方法中作比较 ,如果不一致,就更新lastRemoteInstanceStatus,并且广播对应的事件
*/
updateInstanceRemoteStatus();
// registry was fetched successfully, so return true
return true;
}
}
2.1.3 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;
/**
* 判断是否 registryRefreshSingleVipAddress 是否为空,
* 如果为空,调用 eurekaTransport.queryClient.getApplications(remoteRegionsRef.get()) 获取全部服务地址
* 如果不为空,则只获取 registryRefreshSingleVipAddress 地址
*/
EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
: eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
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");
} else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
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(remoteRegionsRef.get())
获取全部服务地址。
public abstract class AbstractJerseyEurekaHttpClient implements EurekaHttpClient {
@Override
public EurekaHttpResponse<Applications> getApplications(String... regions) {
// 调用 /apps 接口获取全部服务地址
return getApplicationsInternal("apps/", regions);
}
private EurekaHttpResponse<Applications> getApplicationsInternal(String urlPath, String[] regions) {
ClientResponse response = null;
String regionsParamValue = null;
try {
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();
}
}
}
}
获取 registryRefreshSingleVipAddress 地址
public abstract class AbstractJerseyEurekaHttpClient implements EurekaHttpClient {
@Override
public EurekaHttpResponse<Applications> getVip(String vipAddress, String... regions) {
// 调用 /vips 接口获取 registryRefreshSingleVipAddress 地址
return getApplicationsInternal("vips/" + vipAddress, regions);
}
private EurekaHttpResponse<Applications> getApplicationsInternal(String urlPath, String[] regions) {
ClientResponse response = null;
String regionsParamValue = null;
try {
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();
}
}
}
}
2.1.4 DiscoveryClient.getAndUpdateDelta(Applications applications) 增量更新
增量更新具体代码如下。
/**
* 增量更新
*/
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();
}
// 如果 delta 为空,则调用全量更新
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()) {
// 协调和记录差异
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());
}
}
2.2 在 Eureka Server 端地址发现变化后,需要动态更新本地服务列表
在分析 Eureka Server 服务注册流程中,DiscoveryClient 的构造函数中,会调用 DiscoveryClient.initScheduledTasks()
方法,初始化一个定时任务,负责心跳、实例数据更新,定时任务更新缓存则在这个定时任务中,具体代码如下。
@Singleton
public class DiscoveryClient implements EurekaClient {
/**
* 初始化一个定时任务,负责心跳、实例数据更新
*/
private void initScheduledTasks() {
// 如果需要获取 Eureka Server 中的服务地址,则会开启 cacheRefreshExecutor 这个定时任务,用于进行缓存数据的刷新
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);
}
// 省略其他代码...
}
}
2.2.1 TimedSupervisorTask
TimedSupervisorTask 是固定间隔的周期性任务(默认30s),一旦遇到超时就会将下一个周期的间隔时间调大,如果连续超时,那么每次间隔时间都会增大一倍,一直到达外部参数设定的上限为止,一旦新任务不再超时,间隔时间又会自动恢复为初始值。这种设计思想采用了衰减重试的思想,非常值得借鉴学习。
@Override
public TimedSupervisorTask(String name, ScheduledExecutorService scheduler, ThreadPoolExecutor executor,
int timeout, TimeUnit timeUnit, int expBackOffBound, Runnable task) {
public void run() {
Future<?> future = null;
try {
// 使用Future,可以设定子线程的超时时间
future = executor.submit(task);
threadPoolLevelGauge.set((long) executor.getActiveCount());
// 指定等待子线程的最长时间
future.get(timeoutMillis, TimeUnit.MILLISECONDS); // block until done or timeout
// 每次执行任务成功都重置delay
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();
// future任务线程超时,delay变量翻倍,但不会超过外部调用时设定的最大延时时间
long newDelay = Math.min(maxDelay, currentDelay * 2);
// 使用了CAS设置为最新的值
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 {
// 调用cancel方法来清理任务
if (future != null) {
future.cancel(true);
}
// 只要调度器没有停止,就再指定等待时间之后在执行一次同样的任务
if (!scheduler.isShutdown()) {
// 周期性任务:只要没有停止调度器,就再创建一次性任务,在dealy后执行
scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
}
}
}
}
2.2.2 CacheRefreshThread
@Singleton
public class DiscoveryClient implements EurekaClient {
class CacheRefreshThread implements Runnable {
public void run() {
// 刷新
refreshRegistry();
}
}
@VisibleForTesting
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);
}
}
}
2.2.3 总结
服务发现的过程分几个个步骤:
- Spring Cloud 应用在启动时,判断是否需要获取远程服务地址,如果需要获取,则调用 /apps 拉取。
- 如果需要获取远程服务地址,则开启一个定时延时任务,定时刷新本地缓存。
- 定时延时任务通过循环调用实现,如果执行错误,则延迟时间加倍,直到外部调用时设定的最大延时时间。