白菜Java自习室 涵盖核心知识
Spring Cloud 组件原理系列(一)Eureka篇
Spring Cloud 组件原理系列(二)Hystrix篇
Spring Cloud 组件原理系列(三)Feign篇
1. Eureka 简介
Eureka 是 Netflix 公司开发的,Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务注册与发现,也就是说 Spring Cloud 对 Netflix Eureka 进行了二次封装。
Eureka 是一个 服务治理组件,它主要包括服务注册与发现,主要用来搭建服务注册中心;
Eureka 是一个 基于 REST 的服务,用来定位服务进行中间层服务的负载均衡和故障转移;
Eureka 相关三大角色
- Eureka Server:提供服务的注册与发现。
- Eureka Client - Service Provider:将自身的服务注册到 Eureka Server 中,从而使消费者能够找到。
- Eureka Clinet - Service Consumer:服务消费方从 Eureka Server 中获取服务注册列表,从而找到消费服务。
2. Eureka 核心功能点
服务注册 (register):Eureka Client 会通过发送 REST 请求的方式向 Eureka Server 注册自己的服务,提供自身的元数 据,比如 ip 地址、端口、运行状况指标的 url、主页地址等信息。Eureka Server 接收到注册请求后,就会把这些元数 据信息存储在一个双层的 Map 中(注册表)。
服务续约 (renew):在服务注册后,Eureka Client 会维护一个心跳来持续通知 Eureka Server,说明服务一直处于可 用状态,防止被剔除。Eureka Client 在默认的情况下会每隔30秒 (eureka.instance.leaseRenewallIntervalInSeconds) 发送一次心跳来进行服务续约。
服务同步 (replicate):Eureka Server 之间会互相进行注册,构建 Eureka Server 集群,不同 Eureka Server 之间会进 行服务同步,用来保证服务信息的一致性。
获取服务 (get registry):服务消费者(Eureka Client)在启动的时候,会发送一个 REST 请求给 Eureka Server,获 取上面注册的服务清单,并且缓存在 Eureka Client 本地,默认缓存 30 秒 (eureka.client.registryFetchIntervalSeconds)。同时,为了性能考虑,Eureka Server 也会维护一份只读的服务清 单缓存,该缓存每隔 30 秒更新一次。
服务调用:服务消费者在获取到服务清单后,就可以根据清单中的服务列表信息,查找到其他服务的地址,从而进行 远程调用。Eureka 有 Region 和 Zone 的概念,一个 Region 可以包含多个 Zone,在进行服务调用时,优先访问处于同 一个 Zone 中的服务提供者。
服务下线 (cancel):当 Eureka Client 需要关闭或重启时,就不希望在这个时间段内再有请求进来,所以,就需要提前 先发送 REST 请求给 Eureka Server,告诉 Eureka Server 自己要下线了,Eureka Server 在收到请求后,就会把该服务 状态置为下线(DOWN),并把该下线事件传播出去。
服务剔除 (evict):有时候,服务实例可能会因为网络故障等原因导致不能提供服务,而此时该实例也没有发送请求给 Eureka Server 来进行服务下线,所以,还需要有服务剔除的机制。Eureka Server 在启动的时候会创建一个定时任务,每隔一段时间(默认60秒),从当前服务清单中把超时没有续约(默认90秒, 但实际代码中是180秒(eureka 的 bug,因为 eureka 已经大量使用所以没去修改)eureka.instance.leaseExpirationDurationInSeconds)的服务剔除。
自我保护 (self-preservation):既然 Eureka Server 会定时剔除超时没有续约的服务,那就有可能出现一种场景,网络一段时间内发生了 异常,所有的服务都没能够进行续约,Eureka Server 就把所有的服务都剔除了,这样显然不太合理。所以,就有了自我保护机制,当短时间内,统计续约失败的比例,如果达到一定阈值,则会触发自我保护的机制,在该机制下, Eureka Server 不会剔除任何的微服务,等到正常后,再退出自我保护机制。自我保护开关 (eureka.server.enableself-preservation: false)(默认 15 分钟低于 85% 可用则进入自我保护机制,可以设置不开启)。
上面是对于 Eureka 的一些核心知识进行字面表达,此时会不会觉得记忆点比较少?有点模糊?那么下面我会对于整个 Eureka 进行形象化的解释描述。下图就是对于 client 端是怎么和 server 做交互从而实现的核心功能的形象化:
Eureka Server REST API 介绍
| Operation | HTTP action | Description |
|---|---|---|
| Register new application instance | POST /eureka/v2/apps/appID | Input: JSON/XML payload HTTP Code: 204 on success |
| De-register application instance | DELETE /eureka/v2/apps/appID/instanceID | HTTP Code: 200 on success |
| Send application instance heartbeat | PUT /eureka/v2/apps/appID/instanceID | HTTP Code: * 200 on success * 404 if instanceID doesn’t exist |
| Query for all instances | GET /eureka/v2/apps | HTTP Code: 200 on success Output: JSON/XML |
| Query for all appID instances | GET /eureka/v2/apps/appID | HTTP Code: 200 on success Output: JSON/XML |
| Query for a specific appID/instanceID | GET /eureka/v2/apps/appID/instanceID | HTTP Code: 200 on success Output: JSON/XML |
| Query for a specific instanceID | GET /eureka/v2/instances/instanceID | HTTP Code: 200 on success Output: JSON/XML |
| Take instance out of service | PUT /eureka/v2/apps/appID/instanceID/status?value=OUT_OF_SERVICE | HTTP Code: * 200 on success * 500 on failure |
| Move instance back into service (remove override) | DELETE /eureka/v2/apps/appID/instanceID/status?value=UP (The value=UP is optional, it is used as a suggestion for the fallback status due to removal of the override) | HTTP Code: * 200 on success * 500 on failure |
| Update metadata | PUT /eureka/v2/apps/appID/instanceID/metadata?key=value | HTTP Code: * 200 on success * 500 on failure |
| Query for all instances under a particular vip address | GET /eureka/v2/vips/vipAddress | * HTTP Code: 200 on success Output: JSON/XML * 404 if the vipAddress does not exist. |
| Query for all instances under a particular secure vip address | GET /eureka/v2/svips/svipAddress | * HTTP Code: 200 on success Output: JSON/XML * 404 if the svipAddress does not exist. |
3. Server 端如何实现核心功能
Eureka 分为两个部分,一个 是 server 端、一个 client 端,而 server 很像一个 web 程序,对外提供一些 api 接口,client 端通过调用 server 端的 api 接口从而实现对 server 的注册、获取实例等功能。
3.1. @EnableEurekaServer 注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 这里会导入一个 EurekaServerMarkerConfiguration 类,该类中会注入一个 Marker 的 bean
@Import({EurekaServerMarkerConfiguration.class})
public @interface EnableEurekaServer {
}
进入 EurekaServerMarkerConfiguration 类中,这里要注入一个 Maker 的 bean,该 bean 的作用是激活EurekaServerAutoConfiguration(重要:即 server 端的自动配置类)
@Bean
public EurekaServerMarkerConfiguration.Marker eurekaServerMarkerBean() {
return new EurekaServerMarkerConfiguration.Marker();
}
class Marker {
Marker() {
}
}
此时我们激活 server 端的自动配置类的条件已经满足了,进入 EurekaServerAutoConfiguration 看看里面有什么?这里我们只关注几个 bean,其他代码省略。
@Configuration(
proxyBeanMethods = false
)
@Import({EurekaServerInitializerConfiguration.class})
@ConditionalOnBean({Marker.class})
@EnableConfigurationProperties({EurekaDashboardProperties.class, InstanceRegistryProperties.class})
@PropertySource({"classpath:/eureka/server.properties"})
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
//(1)加载EurekaController, spring‐cloud提供了一些额外的接口,用来获取eurekaServer的信息
@Bean
@ConditionalOnProperty(
prefix = "eureka.dashboard",
name = {"enabled"},
matchIfMissing = true
)
public EurekaController eurekaController() {
return new EurekaController(this.applicationInfoManager);
}
//(2) 初始化集群注册表
@Bean
public PeerAwareInstanceRegistry peerAwareInstanceRegistry(ServerCodecs serverCodecs) {
this.eurekaClient.getApplications();
return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.eurekaClient, this.instanceRegistryProperties.getExpectedNumberOfClientsSendingRenews(), this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
}
//(3)配置服务节点信息,这里的作用主要是为了配置Eureka的peer节点,
// 也就是说当有收到有节点注册上来的时候,需要通知给那些服务节点(互为一个集群)
@Bean
@ConditionalOnMissingBean
public PeerEurekaNodes peerEurekaNodes(PeerAwareInstanceRegistry registry, ServerCodecs serverCodecs, ReplicationClientAdditionalFilters replicationClientAdditionalFilters) {
return new EurekaServerAutoConfiguration.RefreshablePeerEurekaNodes(registry, this.eurekaServerConfig, this.eurekaClientConfig, serverCodecs, this.applicationInfoManager, replicationClientAdditionalFilters);
}
//(4)EurekaServer 的上下文
@Bean
public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs, PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs, registry, peerEurekaNodes, this.applicationInfoManager);
}
//(5)这个类的作用是spring‐cloud和原生eureka的胶水代码,通过这个类来启动EurekaSever,
// 后面这个类会在EurekaServerInitializerConfiguration被调用,进行eureka启动
@Bean
public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry registry, EurekaServerContext serverContext) {
return new EurekaServerBootstrap(this.applicationInfoManager, this.eurekaClientConfig, this.eurekaServerConfig, registry, serverContext);
}
//(6)配置拦截器,ServletContainer里面实现了jersey框架,通过他来实现eurekaServer对外的restFull接口
@Bean
public FilterRegistrationBean<?> jerseyFilterRegistration(Application eurekaJerseyApp) {
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean();
bean.setFilter(new ServletContainer(eurekaJerseyApp));
bean.setOrder(2147483647);
bean.setUrlPatterns(Collections.singletonList("/eureka/*"));
return bean;
}
}
整个过程:主要即注册一个 Maker 的 bean 去激活 EurekaServerAutoConfiguration,然后 EurekaServerAutoConfiguration 类中分别注册了多个初始化 bean。
Eureka 自动配置流程:
@EnableEurekaServer 注解;
@Import 导入 EurekaServerMarkerConfiguration;
注入 Marker,初始化 @Bean;
激活 EurekaServerAutoConfiguration:
- EurekaServerConfig:初始化 eurekaServer 的相关配置
- EurekaController:初始化一些接口,用来获取 eurekaServer 的信息
- PeerAwareInstanceRegistry:初始化集群注册表
- PeerEurekaNodes:初始化集群节点集合
- EurekaServerContext:eurekaServer 的上下文信息
- EurekaServerBootstrap:初始化 spring cloud 包装的 eureka 启动类
- FilterRegistrationBean:初始化 jersey 过滤器
3.2. EurekaServerInitializerConfiguration 类
注意这个 EurekaServerInitializerConfiguration 类,因为他是 服务同步和服务剔除 的调用关键。
@Configuration(
proxyBeanMethods = false
)
@Import({EurekaServerInitializerConfiguration.class})
@ConditionalOnBean({Marker.class})
@EnableConfigurationProperties({EurekaDashboardProperties.class, InstanceRegistryProperties.class})
@PropertySource({"classpath:/eureka/server.properties"})
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
// ......
}
其中会使用前面注册的 eurekaServerBootstrap 去调用 contextInitialized() 方法(该方法回去初始化 server 端的运行环境和上下文)
public class EurekaServerInitializerConfiguration
implements ServletContextAware, SmartLifecycle, Ordered {
// 因为实现了SmartLifecycle接口,
// 所以会在初始化完后去调用isAutoStartup如果为true,则会去调用该start方法
public void start() {
(new Thread(() -> {
try {
this.eurekaServerBootstrap.contextInitialized(this.servletContext);
log.info("Started Eureka Server");
this.publish(new EurekaRegistryAvailableEvent(this.getEurekaServerConfig()));
this.running = true;
this.publish(new EurekaServerStartedEvent(this.getEurekaServerConfig()));
} catch (Exception var2) {
log.error("Could not initialize Eureka servlet context", var2);
}
})).start();
}
public boolean isAutoStartup() {
return true;
}
}
所以我们需要进去 contextInitialized() 看看,这里的 server 端上下文很重要,里面就有关于服务同步和服务剔除的设置。
public void contextInitialized(ServletContext context) {
try {
// 1、初始化server端的运行环境
this.initEurekaEnvironment();
// 2、初始化server端的上下文
this.initEurekaServerContext();
context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
} catch (Throwable var3) {
log.error("Cannot bootstrap eureka server :", var3);
throw new RuntimeException("Cannot bootstrap eureka server :", var3);
}
}
我们需要进入 initEurekaServerContext() 上下文方法中看看,服务同步和服务剔除是怎么进行设置的。
protected void initEurekaServerContext() throws Exception {
// 1、初始化eureka server上下文
EurekaServerContextHolder.initialize(this.serverContext);
log.info("Initialized server context");
// 2、从相邻的eureka节点复制注册表(服务同步方法)
int registryCount = this.registry.syncUp();
// 3、 默认每30秒发送心跳,1分钟就是2次 (服务续约、服务剔除)
// 修改 eureka 状态为 up,同时开启一个定时任务,用于清理60秒没有心跳的客户端,自动下线
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
EurekaMonitors.registerAllStats();
}
SpringBoot 自动配置 Eureka 流程:
@SpringBootApplication
组合注解 @EnableAutoConfiguration
@Import 注解 指向 AutoConfigurationImportSelector
扫描所有包里的 spring.factories 文件
扫描到了 spring-cloud-netfix-eureka-server.jar 里的 spring.factories 文件,文件里的 EnableAutoConfiguration 对应 EurekaServerAutoConfiguration,此时就会进行如下初始化:
EurekaServerConfig:初始化 eurekaServer 的相关配置
EurekaController:初始化一些接口,用来获取 eurekaServer 的信息
PeerAwareInstanceRegistry:初始化集群注册表
PeerEurekaNodes:初始化集群节点集合
EurekaServerContext:eurekaServer 的上下文信息
EurekaServerBootstrap:初始化 spring cloud 包装的 eureka 启动类
FilterRegistrationBean:初始化 jersey 过滤器
@Import 注解 指向 EurekaServerInitializerConfiguration
EurekaServerInitializerConfiguration 实现了接口 SmartLifecycle,根据 isAutoStartup() 方法的返回值,去自动调用 start() 方法
初始化 EurekaServer,调用 eurekaServerBootstrap.contextInitialized(),此时主要做几件事:
- initEurekaEnvironment():初始化 EurekaServer 的运行环境
- initEurekaServerContext():初始化 EurekaServer 的上下文: (1)syncUp():从相邻的eureka节点复制注册表到本地(服务同步方法)
(2)openForTraffic():默认每30秒发送心跳,1分钟就是2次 (服务续约、服务剔除),修改 eureka 状态为 up,同时开启一个定时任务,用于清理60秒没有心跳的客户端,自动下线
我们需要知道 syncUp() 和 openForTraffic() 这两个方法看看到底是怎么实现的,点进去发现是接口,此时找其实现类 PeerAwareInstanceRegistryImpl 的
syncUp() 服务同步方法:
public int syncUp() {
// 省略代码
while(var4.hasNext()) {
Application app = (Application)var4.next();
Iterator var6 = app.getInstances().iterator();
while(var6.hasNext()) {
InstanceInfo instance = (InstanceInfo)var6.next();
try {
if (this.isRegisterable(instance)) {
// 只需要关注该方法:将其他节点的实例注册到本节点
this.register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
++count;
}
} catch (Throwable var9) {
logger.error("During DS init copy", var9);
}
}
}
}
return count;
}
注意:这里的注册节点由于和 client 注册时是同一个方法,所以等到讲 client 端时再去说明该方法的实现(eureka 的各个 server 是同级别的,各自都把其他节点当成 client 来看待)。
openForTraffic() 服务续约、剔除方法:
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
this.expectedNumberOfClientsSendingRenews = count;
this.updateRenewsPerMinThreshold();
logger.info("Got {} instances from neighboring DS node", count);
logger.info("Renew threshold is: {}", this.numberOfRenewsPerMinThreshold);
this.startupTime = System.currentTimeMillis();
if (count > 0) {
this.peerInstancesTransferEmptyOnStartup = false;
}
Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
boolean isAws = Name.Amazon == selfName;
if (isAws && this.serverConfig.shouldPrimeAwsReplicaConnections()) {
logger.info("Priming AWS connections for all replicas..");
this.primeAwsReplicas(applicationInfoManager);
}
logger.info("Changing status to UP");
// 1、设置实例的状态为UP
applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
// 2、 开启定时任务,默认60秒执行一次,用于清理60秒之内没有续约的实例
// 这里是每60秒对90秒内没有续约的数据进行清除
super.postInit();
}
我们进入 postInit() 方法:
protected void postInit() {
this.renewsLastMin.start();
if (this.evictionTaskRef.get() != null) {
((AbstractInstanceRegistry.EvictionTask)this.evictionTaskRef.get()).cancel();
}
this.evictionTaskRef.set(new AbstractInstanceRegistry.EvictionTask());
// 定时任务中进行服务剔除任务,定时时间是getEvictionIntervalTimerInMs,默认是60秒
// 60秒进行一次剔除90秒没续约的服务
this.evictionTimer.schedule((TimerTask)this.evictionTaskRef.get(), this.serverConfig.getEvictionIntervalTimerInMs(), this.serverConfig.getEvictionIntervalTimerInMs());
}
至此,我们的 server 端的主线进行分析。在 server 端中我们主要对于服务同步(具体实现后面在说)、服务剔除(用定时任务每 60 秒剔除一次失效的服务(90 秒没续约的服务))(默认 30 秒续约一次)两个核心功能。
4. Client 端如何实现核心功能
上面对于 eureka 的 server 端自动配置流程的解析,这里对于 eureka 的 client 端进行分析:
(SpringBoot 自动配置加载不再重复说明)
针对客户端,扫描到 spring-cloud-netfix-eureka-client.jar 里的 spring.factories 文件,文件里的 EnableAutoConfiguration 对应 EurekaClientAutoConfiguration, 此时就会进行如下初始化:
EurekaClientConfigBean:初始化 eurekaClient 的相关配置
EurekaClient 的初始化:注入 DiscoveryClient(Spring Cloud 对原生 EurekaClient 的包装类)
- @AutoConfigurationAfter, 同理使用 Marker 来 EnableDiscoveryClientConfiguration;
其中 client 是在 DiscoveryClient 中注入 EurekaClient(这个为原生 Eureka 的客户端,用来调用Eureka server 的一些 API),这时我们则可以用 DiscoveryClient 来调用 server 端的 API。
我们这里直接进入其中的 initScheduledTasks() 方法,该方法涉及服务获取、续约、注册等核心:
private void initScheduledTasks() {
int renewalIntervalInSecs;
int expBackOffBound;
if (this.clientConfig.shouldFetchRegistry()) {
// 服务注册列表更新的周期时间
renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();
expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
// 1、定时更新服务注册列表(获取服务列表)
// 其中CacheRefreshThread()方法是该线程执行更新的具体逻辑
this.scheduler.schedule(new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
}
if (this.clientConfig.shouldRegisterWithEureka()) {
// 2、服务续约的周期时间
renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: renew interval is: {}", renewalIntervalInSecs);
// 服务定时续约,其中HeartbeatThread()方法是对续约的具体方法
this.scheduler.schedule(new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
this.instanceInfoReplicator = new InstanceInfoReplicator(this, this.instanceInfo, this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
this.statusChangeListener = new StatusChangeListener() {
public String getId() {
return "statusChangeListener";
}
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN != statusChangeEvent.getStatus() && InstanceStatus.DOWN != statusChangeEvent.getPreviousStatus()) {
DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
} else {
DiscoveryClient.logger.warn("Saw local status change event {}", statusChangeEvent);
}
DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate();
}
};
if (this.clientConfig.shouldOnDemandUpdateStatusChange()) {
this.applicationInfoManager.registerStatusChangeListener(this.statusChangeListener);
}
// 3、start方法是进行服务注册
this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
4.1. 拉取服务列表
拉取服务列表定时的使用 DiscoveryClient 调用 CacheRefreshThread() 方法:
class CacheRefreshThread implements Runnable {
CacheRefreshThread() {
}
public void run() {
DiscoveryClient.this.refreshRegistry();
}
}
再进去 refreshRegistry() 方法:
@VisibleForTesting
void refreshRegistry() {
// 获取注册信息方法
boolean success = this.fetchRegistry(remoteRegionsModified);
if (success) {
this.registrySize = ((Applications)this.localRegionApps.get()).size();
this.lastSuccessfulRegistryFetchTimestamp = System.currentTimeMillis();
}
}
进入 fetchRegistry() 获取注册信息方法:
private boolean fetchRegistry(boolean forceFullRegistryFetch) {
Stopwatch tracer = this.FETCH_REGISTRY_TIMER.start();
label122: {
boolean var4;
try {
// 取出本地缓存之前获取的服务列表信息
Applications applications = this.getApplications();
// 判断多个条件,确定是否触发全量更新,如下任一个满足都会全量更新:
// 1. 是否禁用增量更新;
// 2. 是否对某个region特别关注;
// 3. 外部调用时是否通过入参指定全量更新;
// 4. 本地还未缓存有效的服务列表信息;
if (!this.clientConfig.shouldDisableDelta() && Strings.isNullOrEmpty(this.clientConfig.getRegistryRefreshSingleVipAddress()) && !forceFullRegistryFetch && applications != null && applications.getRegisteredApplications().size() != 0 && applications.getVersion() != -1L) {
// 1、增量更新
this.getAndUpdateDelta(applications);
} else {
// 2、全量更新
this.getAndStoreFullRegistry();
}
// 3、重新计算和设置一致性Hash码
applications.setAppsHashCode(applications.getReconcileHashCode());
this.logTotalInstances();
break label122;
} catch (Throwable var8) {
logger.error("DiscoveryClient_{} - was unable to refresh its cache! status = {}", new Object[]{this.appPathIdentifier, var8.getMessage(), var8});
var4 = false;
} finally {
if (tracer != null) {
tracer.stop();
}
}
return var4;
}
this.onCacheRefreshed();
this.updateInstanceRemoteStatus();
return true;
}
这里有一个重点,如果一个 client 在对 server 进行拉取注册表时,获取本地缓存找是否有信息,没有则为第一次去拉取,此时调用全量拉取方法 getAndStoreFullRegistry()(即拉取所有列表信息),如果本地有则调用增量拉取 getAndUpdateDelta() 方法。
注意:每次拉取完后会根据 client 端的本地缓存的注册表进行计算出一个 hashcode 值,这个值很重要,再后面会提到。
getAndStoreFullRegistry() 全量拉取方法:
private void getAndStoreFullRegistry() throws Throwable {
long currentUpdateGeneration = this.fetchRegistryGeneration.get();
logger.info("Getting all instance registry info from the eureka server");
// 1、这个Applications 就是client本地缓存的注册表
Applications apps = null;
// 2、eurekaTransport.queryClient.getApplications方法就是调用了server端的全量获取接口
EurekaHttpResponse<Applications> httpResponse = this.clientConfig.getRegistryRefreshSingleVipAddress() == null ? this.eurekaTransport.queryClient.getApplications((String[])this.remoteRegionsRef.get()) : this.eurekaTransport.queryClient.getVip(this.clientConfig.getRegistryRefreshSingleVipAddress(), (String[])this.remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
// 3、如果获取成功,则会放入本地缓存的注册表中
apps = (Applications)httpResponse.getEntity();
}
}
getAndUpdateDelta() 增量拉取方法:
private void getAndUpdateDelta(Applications applications) throws Throwable {
long currentUpdateGeneration = this.fetchRegistryGeneration.get();
// 1、本地缓存注册表
Applications delta = null;
// 2、eurekaTransport.queryClient.getDelta调用server端的增量接口
EurekaHttpResponse<Applications> httpResponse = this.eurekaTransport.queryClient.getDelta((String[])this.remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
// 3、将增量合并到注册表中,注意:不管是全量还是增量,server都会传来以一个hashcode值
// 其值是根据server中的本地注册表实例计算来的(和我们之前说的client端也会根据本地的注册实例注册一个hashcode值)
// 此时如果server传来的hashcode值=client接收到实例后新计算的hashcode值,则证明我们拿到的是最新的实例注册表,
// 如果不等于则表示拿的不是最新的,此时会再进行一次全量拉取请求
delta = (Applications)httpResponse.getEntity();
}
// 4、如果本地缓存的注册表为空,即第一次请求,此时直接调用全量拉取方法
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.");
this.getAndStoreFullRegistry();
} else if (this.fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1L)) {
logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
String reconcileHashCode = "";
if (this.fetchRegistryUpdateLock.tryLock()) {
try {
this.updateDelta(delta);
reconcileHashCode = this.getReconcileHashCode(applications);
} finally {
this.fetchRegistryUpdateLock.unlock();
}
} else {
logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
}
// 5、一致性哈希码不同,就在reconcileAndLogDifference方法中做全量更新
if (!reconcileHashCode.equals(delta.getAppsHashCode()) || this.clientConfig.shouldLogDeltaDiff()) {
this.reconcileAndLogDifference(delta, reconcileHashCode);
}
} 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());
}
}
该方法的主要步骤:
- 如果本地缓存为空,证明是第一次调用,此时执行全量拉取。
- 如果本地缓存不为空,则将调用增量拉取,拉取的结果合并到本地缓存。
- 最后进行一致哈希码比较,如果传来的哈希码不等于本地新算的哈希码,此时证明拿到的不是最新的,则需要进行一次全量拉取(其实是伪增量)。
4.2. 服务续约
定时任务:每30秒执行一次
服务续约比较简单,也是通过 discoveryClient 调用 renew() 方法对 server 进行续约操作。 server 接到请求后会通过 client 的 ip、服务名、请求时间等信息去 server 的服务列表中修改相应服务实例的信息。
4.3. 服务注册
服务注册则通过一个线程中的 run() 方法去调用 server 的一个 regiester() 方法,server 端接到请求后会向注册表中添加相应实例信息。
5. Server 端如何处理注册接口、拉取列表接口
在说明两个接口的处理逻辑前,我们先需要明白 eureka 的内部构造,eureka 中存在两个缓存:
- 只读缓存(concurrenthashmap)(一级缓存):默认30秒失效,失效后会从二级缓存读写缓存中获取(每30秒使用定时任务进行获取),再获取不到则直接到本地注册表中(gMap:也是一个map)获取。
- 读写缓存(谷歌的一个map)(二级缓存):默认180秒失效,失效后会去本地注册表中那信息。
注册续约和拉取列表逻辑:
- 注册接口:接到请求后,会调用 register() 方法,将所有的实例信息写进注册表中。此时写完后要去清理读写缓存(二级缓存)的信息。
- 拉取列表接口:先从只读缓存获取,获取不到去读写缓存中获取,再读不到去本地注册表拿。
- 续约接口:直接去注册表中修改相应实例信息即可。
多级缓存机制的优点:
- 尽可能保证了内存注册表数据不会出现频繁的读写冲突问题;
- 并且进一步保证对Eureka Server的大量请求,都是快速从纯内存走,性能极高。
多级缓存机制常见问题:
就是当我们eureka服务实例有注册或下线或有实例发生故障,内存注册表虽然会及时更新数据,但是客户端不一定能及时感知到,可能会过30秒才能感知到,因为客户端拉 取注册表实例这里面有一个多级缓存机制。
Spring Cloud 组件原理系列(一)Eureka篇
Spring Cloud 组件原理系列(二)Hystrix篇
Spring Cloud 组件原理系列(三)Feign篇