client端功能:
1.注册服务
2.拉取server列表到本地
3.发送心跳,续约;定时拉取注册表
4.发送下线
① spring boot项目引入eureka-client依赖,并注入spring 容器。
在spring-boot项目中pom文件里面添加的依赖中的bean。是如何注册到spring-boot项目的spring容器中的呢?
spring.factories文件是帮助spring-boot项目包以外的bean(即在pom文件中添加依赖中的bean)注册到spring-boot项目的spring容器的。
由于@ComponentScan注解只能扫描spring-boot项目包内的bean并注册到spring容器中,因此需要@EnableAutoConfiguration(在SpringBootApplication下),
注解来注册项目包外的bean。而spring.factories文件,则是用来记录项目包外需要注册的bean类名。
点进去@SpringBootApplication注解,发现@EnableAutoConfiguration。点@EnableAutoConfiguration进去。
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
点AutoConfigurationImportSelector进去
发现下面代码
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
此方法时,向spring ioc容器注入bean。selectImports,返回bean全名。import将bean全名注入。而注入的bean都是些什么呢?
点:getAutoConfigurationEntry进去,有一句
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
点getCandidateConfigurations进去:
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
点SpringFactoriesLoader进去:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
② 找eureka client 配置相关类
在api-listen-order(其他eureka client项目均可)项目中,找到
spring-cloud-netflix-eureka-client-2.1.2.RELEASE下META-INF下spring.factories。此文件中,有如下配置信息:
EurekaClientAutoConfiguration(Eureka client自动配置类,负责Eureka client中关键beans的配置和初始化),
RibbonEurekaAutoConfiguration(Ribbon负载均衡相关配置)
EurekaDiscoveryClientConfiguration(配置自动注册和应用的健康检查器)。
③ EurekaDiscoveryClientConfiguration介绍
找到此类:org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration中的注解@ConditionalOnClass(EurekaClientConfig.class),
④ EurekaClientConfig介绍
点击进去查看EurekaClientConfig是个接口,查看其实现类EurekaClientConfigBean。此类里封装了Eureka Client和Eureka Server交互所需要的配置信息。看此类代码:
public static final String PREFIX = "eureka.client";
表示在配置文件中用eureka.client.属性名配置。
⑤ Eureka 实例相关配置
从org.springframework.cloud.client.discovery.DiscoveryClient顶级接口入手,前面介绍过spring common。看其在Eureka中的实现类org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient。有一个属性:
private final EurekaClient eurekaClient,查看其实现类:com.netflix.discovery.DiscoveryClient。
有一个属性:
private final ApplicationInfoManager applicationInfoManager(应用信息管理器,点进去此类,发现此类总有两个属性:
private InstanceInfo instanceInfo;
private EurekaInstanceConfig config;
服务实例的信息类InstanceInfo和服务实例配置信息类EurekaInstanceConfig)。
⑥ InstanceInfo介绍
打开InstanceInfo里面有instanceId等服务实例信息。
InstanceInfo封装了将被发送到Eureka Server进行注册的服务实例元数据。
它在Eureka Server列表中代表一个服务实例,其他服务可以通过instanceInfo了解到该服务的实例相关信息,包括地址等,从而发起请求。
⑦ EurekaInstanceConfig介绍
EurekaInstanceConfig是个接口,找到它的实现类org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean。
此类封装了EurekaClient自身服务实例的配置信息,主要用于构建InstanceInfo。看到此类有一段代码:@ConfigurationProperties("eureka.instance"),
在配置文件中用eureka.instance.属性配置。EurekaInstanceConfigBean提供了默认值。
⑧ 通过EurekaInstanceConfig构建instanceInfo
在ApplicationInfoManager中有一个方法
public void initComponent(EurekaInstanceConfig config)中有一句:
this.instanceInfo = new EurekaConfigBasedInstanceInfoProvider(config).get();
通过EurekaInstanceConfig构造instanceInfo。
⑨ 顶级接口DiscoveryClient介绍
介绍一下spring-cloud-commons-2.2.1.realease包下,org.springframework.cloud.client.discovery.DiscoveryClient接口。定义用来服务发现的客户端接口,是客户端进行服务发现的核心接口,是spring cloud用来进行服务发现的顶级接口,在common中可以看到其地位。在Netflix Eureka和Consul中都有具体的实现类。
org.springframework.cloud.client.discovery.DiscoveryClient的类注释:
Represents read operations commonly available to discovery services such as Netflix Eureka or consul.io。
代表通用于服务发现的读操作,例如在 eureka或consul中。有
String description();//获取实现类的描述。
List<String> getServices();//获取所有服务实例id。
List<ServiceInstance> getInstances(String serviceId);//通过服务id查询服务实例信息列表。
⑩ Eureka 的实现
接下来我们找Eureka的实现类。org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient。
查看方法。
public List<ServiceInstance> getInstances(String serviceId),
组合了com.netflix.discovery.EurekaClient来实现。
⑩①EurekaClient的实现
EurekaClient有一个注解@ImplementedBy(DiscoveryClient.class),此类的默认实现类:com.netflix.discovery.DiscoveryClient。提供了:
服务注册到server方法register().
续约boolean renew().
下线public synchronized void shutdown().
查询服务列表 功能。
想想前面的图中client的功能。提供了于Eureka Server交互的关键逻辑。
com.netflix.discovery.DiscoveryClient
com.netflix.discovery.DiscoveryClient实现了EurekaClient(继承了LookupService)
com.netflix.discovery.shared.LookupService
LookupService作用:发现活跃的服务实例。
根据服务实例注册的appName来获取封装有相同appName的服务实例信息容器:
Application getApplication(String appName)。
获取所有的服务实例信息:
Applications getApplications();
根据实例id,获取服务实例信息:
List<InstanceInfo> getInstancesById(String id);
上面提到一个Application,它持有服务实例信息列表。它是同一个服务的集群信息。比如api-passenger的所有服务信息,这些服务都在api-passenger服务名下面。
而instanceInfo代表一个服务实例的信息。为了保证原子性,比如对某个instanceInfo的操作,使用了大量同步的代码。比如下面代码:
public void addInstance(InstanceInfo i) {
instancesMap.put(i.getId(), i);
synchronized (instances) {
instances.remove(i);
instances.add(i);
isDirty = true;
}
}
Applications是注册表中,所有服务实例信息的集合。
⑩② 健康检测器和事件监听器
EurekaClient在LookupService上做了扩充。提供了更丰富的获取服务实例的方法。按住不表。我们看一下另外两个方法:
public void registerHealthCheck(HealthCheckHandler healthCheckHandler),向client注册 健康检查处理器,client存在一个定时任务通过HealthCheckHandler检查当前client状态,当client状态发生变化时,将会触发新的注册事件,去更新eureka server的注册表中的服务实例信息。
通过HealthCheckHandler 实现应用状态检测。HealthCheckHandler的实现类org.springframework.cloud.netflix.eureka.EurekaHealthCheckHandler,看其构造函数:
public EurekaHealthCheckHandler(HealthAggregator healthAggregator) {
Assert.notNull(healthAggregator, "HealthAggregator must not be null");
this.healthIndicator = new CompositeHealthIndicator(healthAggregator);
}
private final CompositeHealthIndicator healthIndicator;此类事属于org.springframework.boot.actuate.health包下,可以得出,是通过actuator来实现对应用的检测的。
public void registerEventListener(EurekaEventListener eventListener)注册事件监听器,当实例信息有变时,触发对应的处理事件。
⑩③ 找到com.netflix.discovery.DiscoveryClient
在api-listen-order项目中,找到spring-cloud-netflix-eureka-client-2.1.2.RELEASE下META-INF下spring.factories。此文件中org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration,此类有个注解:
@Import({ EurekaDiscoveryClientConfiguration.class, // this emulates
// @EnableDiscoveryClient, the import
// selector doesn't run before the
// bootstrap phase
EurekaClientAutoConfiguration.class })
注解中有个类: EurekaClientAutoConfiguration,此类中有如下代码:
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,
config, this.optionalArgs, this.context);
(debug可以调试到)
通过CloudEurekaClient找到:public class CloudEurekaClient extends DiscoveryClient。
⑩④ com.netflix.discovery.DiscoveryClient构造函数-不注册不拉取
DiscoveryClient的构造函数:
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer)
此方法中依次执行了 从eureka server中拉取注册表,服务注册,初始化发送心跳,缓存刷新(定时拉取注册表信息),按需注册定时任务等,贯穿了Eureka Client启动阶段的各项工作。
构造函数353行:
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;
}
shouldFetchRegistry,点其实现类EurekaClientConfigBean,找到它其实对应于:eureka.client.fetch-register,true:表示client从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;
}
shouldRegisterWithEureka,点其实现类EurekaClientConfigBean,找到它其实对应于:
eureka.client.register-with-eureka:true:表示client将注册到server。
if (!config.shouldRegisterWithEureka() && !config.shouldFetchRegistry()) {
如果以上两个都为false,则直接返回,构造方法执行结束,既不服务注册,也不服务发现。
⑩⑤ com.netflix.discovery.DiscoveryClient构造函数-两个定时任务
顺着上面代码往下看:
scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-%d")
.setDaemon(true)
.build());
定义了一个基于线程池的定时器线程池,大小为2。
往下:
heartbeatExecutor:用于发送心跳,
cacheRefreshExecutor:用于刷新缓存。
⑩⑥ com.netflix.discovery.DiscoveryClient构造函数-client和server交互的Jersey客户端
接着构建eurekaTransport = new EurekaTransport();它是eureka Client和eureka server进行http交互jersey客户端。点开EurekaTransport,看到许多httpclient相关的属性。
⑩⑦ com.netflix.discovery.DiscoveryClient构造函数-拉取注册信息
if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
fetchRegistryFromBackup();
}
如果判断的前部分为true,执行后半部分fetchRegistry。此时会从eureka server拉取注册表中的信息,将注册表缓存到本地,可以就近获取其他服务信息,减少于server的交互。
⑩⑧ com.netflix.discovery.DiscoveryClient构造函数-服务注册
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);
}
}注册失败抛异常。
⑩⑨ com.netflix.discovery.DiscoveryClient构造函数-启动定时任务
在构造方法的最后initScheduledTasks();此方法中,启动3个定时任务。方法内有statusChangeListener,按需注册是一个事件StatusChangeEvent,状态改变,则向server注册。
②⑩ com.netflix.discovery.DiscoveryClient构造函数-总结
总结DiscoveryClient构造关键过程:
初始化一堆信息。
从拉取注册表信息。
向server注册自己。
初始化3个任务。
详细后面继续讲。源码就是这样,得层层拨开。
②⑩① 拉取注册表信息详解
上面的fetchRegistry(false),点进去,看注释:
// If the delta is disabled or if it is the first time, get all applications。
如果增量式拉取被禁止或第一次拉取注册表,则进行全量拉取:getAndStoreFullRegistry()。
否则进行增量拉取注册表信息getAndUpdateDelta(applications)。
一般情况,在Eureka client第一次启动,会进行全量拉取。之后的拉取都尽量尝试只进行增量拉取。
拉取服务注册表:
全量拉取:getAndStoreFullRegistry();
增量拉取:getAndUpdateDelta(applications);
全量拉取
进入getAndStoreFullRegistry() 方法,有一方法:eurekaTransport.queryClient.getApplications。
通过debug发现 实现类是AbstractJerseyEurekaHttpClient,点开,debug出
webResource地址为:http://root:root@eureka-7900:7900/eureka/apps/,此端点用于获取server中所有的注册表信息。
getAndStoreFullRegistry()可能被多个线程同时调用,导致新拉取的注册表被旧的覆盖(如果新拉取的动作设置apps阻塞的情况下)。
此时用了AutomicLong来进行版本管理,如果更新时版本不一致,不保存apps。
通过这个判断fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1),如果版本一致,并设置新版本(+1),
接着执行localRegionApps.set(this.filterAndShuffle(apps));过滤并洗牌apps。点开this.filterAndShuffle(apps)实现,继续点apps.shuffleAndIndexInstances,继续点shuffleInstances,继续点application.shuffleAndStoreInstances,继续点_shuffleAndStoreInstances,发现if (filterUpInstances && InstanceStatus.UP != instanceInfo.getStatus())。只保留状态为UP的服务。
增量拉取
回到刚才的fetchRegistry方法中,getAndUpdateDelta,增量拉取。通过getDelta方法,看到实际拉取的地址是:apps/delta,如果获取到的delta为空,则全量拉取。
通常来讲是3分钟之内注册表的信息变化(在server端判断),获取到delta后,会更新本地注册表。
增量式拉取是为了维护client和server端 注册表的一致性,防止本地数据过久,而失效,采用增量式拉取的方式,减少了client和server的通信量。
client有一个注册表缓存刷新定时器,专门负责维护两者之间的信息同步,但是当增量出现意外时,定时器将执行,全量拉取以更新本地缓存信息。更新本地注册表方法updateDelta,有一个细节。
if (ActionType.ADDED.equals(instance.getActionType())) ,public enum ActionType {
ADDED, // Added in the discovery server
MODIFIED, // Changed in the discovery server
DELETED
// Deleted from the discovery server
},
在InstanceInfo instance中有一个instance.getActionType(),ADDED和MODIFIED状态的将更新本地注册表applications.addApplication,DELETED将从本地剔除掉existingApp.removeInstance(instance)。
服务注册
好了拉取完eureka server中的注册表了,接着进行服务注册。回到DiscoveryClient构造函数。
拉取fetchRegistry完后进行register注册。由于构造函数开始时已经将服务实例元数据封装好了instanceInfo,所以此处之间向server发送instanceInfo,
通过方法httpResponse = eurekaTransport.registrationClient.register(instanceInfo);看到String urlPath = "apps/" + info.getAppName();又是一个server端点,退上去f7,httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();204状态码,则注册成功。
初始化3个定时任务
接着
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
initScheduledTasks();看注释初始化3个定时任务。
题外话:
client会定时向server发送心跳,维持自己服务租约的有效性,用心跳定时任务实现;
而server中会有不同的服务实例注册进来,一进一出,就需要数据的同步。所以client需要定时从server拉取注册表信息,用缓存定时任务实现;
client如果有变化,也会及时更新server中自己的信息,用按需注册定时任务实现。
就是这三个定时任务。
进 initScheduledTasks()方法中,clientConfig.shouldFetchRegistry(),
从server拉取注册表信息。
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds()拉取的时间间隔,eureka.client.registry-fetch-interval-seconds进行设置。
int renewalIntervalInSecs = nstanceInfo.getLeaseInfo().getRenewalIntervalInSecs();心跳定时器,默认30秒。
心跳定时任务和缓存刷新定时任务是有scheduler 的 schedule提交的,鼠标放到scheduler上,看到一句话 A scheduler to be used for the following 3 tasks:- updating service urls- scheduling a TimedSuperVisorTask。
知道循环逻辑是由TimedSuperVisorTask实现的。
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()看到HeartbeatThread线程。
点进去public void run() {
if (renew()) {
lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
}
}
里面是renew()方法。
scheduler.schedule(
new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
),
看到CacheRefreshThread,进去,发现 class CacheRefreshThread implements Runnable {
public void run() {
refreshRegistry();
}
}是用的refreshRegistry,进去发现fetchRegistry。回到原来讲过的地方。
boolean renew() {
EurekaHttpResponse<InstanceInfo> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
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;
}
}看到如果遇到404,server没有此实例,则重新发起注册。如果续约成功返回 200.
点sendHeartBeat进去String urlPath = "apps/" + appName + '/' + id;
还有一个定时任务,按需注册。当instanceinfo和status发生变化时,需要向server同步,去更新自己在server中的实例信息。保证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());
此定时任务有2个部分,
1:定时刷新服务实例信息和检查应用状态的变化,在服务实例信息发生改变的情况下向server重新发起注册。InstanceInfoReplicator点进去。看到一个方法
public void run() {
try {
discoveryClient.refreshInstanceInfo();//刷新instanceinfo。
//如果实例信息有变,返回数据更新时间。
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 {
//延时执行下一个检查任务。用于再次调用run方法,继续检查服务实例信息和状态的变化。
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
refreshInstanceInfo点进去,看方法注释:如果有变化,在下次心跳时,同步向server。
2.注册状态改变监听器,在应用状态发生变化时,刷新服务实例信息,在服务实例信息发生改变时向server注册。 看这段
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
// log at warn level if DOWN was involved
logger.warn("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
instanceInfoReplicator.onDemandUpdate();
}
};如果状态发生改变,调用onDemandUpdate(),点onDemandUpdate进去,看到InstanceInfoReplicator.this.run();
总结:两部分,一部分自己去检查,一部分等待状态监听事件。
初始化定时任务完成,最后一步启动步骤完成。接下来就是正常服务于业务。然后消亡。
服务下线
服务下线:在应用关闭时,client会向server注销自己,在Discoveryclient销毁前,会执行下面清理方法。
@PreDestroy
@Override
public synchronized void shutdown() ,看此方法上有一个注解,表示:在销毁前执行此方法。unregisterStatusChangeListener注销监听器。cancelScheduledTasks取消定时任务。unregister服务下线。eurekaTransport.shutdown关闭jersy客户端 等。
unregister点进去。cancel点进去。AbstractJerseyEurekaHttpClient。String urlPath = "apps/" + appName + '/' + id;看到url和http请求delete方法。
②⑩② client源码总结
总结:源码其实两部分内容:
1、client自身的操作。
2、server的配合。