微服务注册中心之Eureka,阿里一线架构师技术图谱

启动成功,注册api-driver ->7901
发现只有7901和7902有 api-driver而 7903没有。

简单说:api-driver向 7901注册,7902将api-driver同步到7902,但是不会同步到7903。后面源码会讲到。
多节点建议:设置A->B,A->C其他类似。尽量不要跨 eureka节点。一对多,面面对到。

讲解下图。前置概念peer。清除流程。

功能点:
peer启动:
1、拉取它的peer的注册表。
2、把自己注册到peer上。
3、完成2之后,2中的peer会把它同步到,2中peer的peer。

eureka集群复制流程图

&nbsp

Eureka 原理

  1. 本质:存储了每个客户端的注册信息。EurekaClient从EurekaServer同步获取服务注册列表。通过一定的规则选择一个服务进行调用。
  2. Eureka架构图

Eureka架构图

&nbsp

  1. 详解
  • 服务提供者:是一个eureka client,向Eureka Server注册和更新自己的信息,同时能从Eureka Server注册表中获取到其他服务的信息。
  • 服务注册中心:提供服务注册和发现的功能。每个Eureka Cient向Eureka Server注册自己的信息,也可以通过Eureka Server获取到其他服务的信息达到发现和调用其他服务的目的。
  • 服务消费者:是一个eureka client,通过Eureka Server获取注册到其上其他服务的信息,从而根据信息找到所需的服务发起远程调用。
  • 同步复制:Eureka Server之间注册表信息的同步复制,使Eureka Server集群中不同注册表中服务实例信息保持一致。
  • 远程调用:服务客户端之间的远程调用。
  • 注册:Client端向Server端注册自身的元数据以供服务发现。
  • 续约:通过发送心跳到Server以维持和更新注册表中服务实例元数据的有效性。当在一定时长内,Server没有收到Client的心跳信息,将默认服务下线,会把服务实例的信息从注册表中删除。
  • 下线:Client在关闭时主动向Server注销服务实例元数据,这时Client的服务实例数据将从Server的注册表中删除。
  • 获取注册表:Client向Server请求注册表信息,用于服务发现,从而发起服务间远程调用。

如果让我们自己做,该如何做 ?

客户端:
拉取注册表
从注册表选一个
调用

服务端:
写个web server。
功能:
1、定义注册表:
Map<name,Map<id,InstanceInfo>>。
2、别人可以向我注册自己的信息。
3、别人可以从我这里拉取他人的注册信息。
4、我和我的同类可以共享注册表。

eureka是用:jersey实现,也是个mvc框架。
我们可以自己写个spring boot web实现。

Eureka 源码

Eureka Client源码

  1. eureka 接口

https://github.com/Netflix/eureka/wiki/Eureka-REST-operations
注意地址中的v2 是没有的。

查询所有实例信息:http://localhost:7900/eureka/apps

注册服务:http://localhost:7900/eureka/apps/{applicationName}

Eureka Client工作流程图

&nbsp

  1. 源码解读

下面的讲解按照顺序进行。

  • 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 configurations = getCandidateConfigurations(annotationMetadata, attributes);

点getCandidateConfigurations进去:

List 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 getServices();//获取所有服务实例id。
List getInstances(String serviceId);//通过服务id查询服务实例信息列表。

  • Eureka 的实现

接下来我们找Eureka的实现类。org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient。
查看方法。
public List 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 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 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 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的配合。(https://github.com/Netflix/eureka/wiki/Eureka-REST-operations)。
一切尽在:《Eureka Client工作流程图》

Eureka Server源码

  1. Eureka Server功能复习

接受服务注册
接受服务心跳
服务剔除
服务下线
集群同步
获取注册表中服务实例信息

需要注意的是,Eureka Server同时也是一个Eureka Client,在不禁止Eureka Server的客户端行为时,它会向它配置文件中的其他Eureka Server进行拉取注册表、服务注册和发送心跳等操作。

  1. 源码解读
  • 启动server注册相关bean

注册外部的配置类
spring-cloud-netflix-eureka-server-2.1.2.REALEASE.jar

META-INF/spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
启动时会自动加载:EurekaServerAutoConfiguration
功能:向spring的bean工厂添加eureka-server相关功能的bean。

但是EurekaServerAutoConfiguration的生效时有条件的。
EurekaServerAutoConfiguration上有一个注解:@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class),意思是:只有在Spring容器里有Marker这个类的实例时,才会加载EurekaServerAutoConfiguration,这个就是控制是否开启Eureka Server的关键。

  • 开启eureka server

开关:
而在@EnableEurekaServer中,@Import(EurekaServerMarkerConfiguration.class),意思是:动态注入此bean到spring 容器。引入了EurekaServerMarkerConfiguration.class。所以开启了Server服务。所以注册了前面说的:EurekaServerAutoConfiguration

  • 开启注册

在EurekaServerMarkerConfiguration上有@Import(EurekaServerInitializerConfiguration.class),导入了EurekaServerInitializerConfiguration,
EurekaServerInitializerConfiguration
implements ServletContextAware, SmartLifecycle,SmartLifecycle的作用是:初始化完之后,
执行public void start()方法。

在public void start()中,启动一个线程。看注释:log.info(“Started Eureka Server”);发布事件:publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig())),
告诉client,可以来注册了。

上面提到的 log.info(“Started Eureka Server”) 的上面一行。eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
点contextInitialized进去,看到initEurekaServerContext,初始化eureka 上下文,点initEurekaServerContext进去,看到
// Copy registry from neighboring eureka node
int registryCount = this.registry.syncUp();从相邻的eureka 节点复制注册表,
下一行openForTraffic(主要是和client 交换信息,traffic),查看实现,PeerAwareInstanceRegistryImpl,开启任务postInit,进去之后发现剔除功能(剔除 没有续约的服务)。postInit,点进去,发现new EvictionTask(),点进去,看到run方法中,evict(compensationTimeMs),点进去就到了,具体剔除逻辑,下面剔除的时候讲。

  • PeerAwareInstanceRegistry接口

在EurekaServerAutoConfiguration中 有 public EurekaServerContext eurekaServerContext,中有DefaultEurekaServerContext,点进去找到
@PostConstruct
@Override
public void initialize() {
logger.info(“Initializing …”);
peerEurekaNodes.start();
try {
registry.init(peerEurekaNodes);
} catch (Exception e) {
throw new RuntimeException(e);
}
logger.info(“Initialized”);
},其中peerEurekaNodes.start();启动一个只拥有一个线程的线程池,第一次进去会更新一下集群其他节点信息。registry.init(peerEurekaNodes);鼠标放在registry上,发现是PeerAwareInstanceRegistryImpl , 的 注册信息管理类里面的init方法。PeerAwareInstanceRegistry是个接口,实现类是:PeerAwareInstanceRegistryImpl。PeerAwareInstanceRegistry接口,实现了com.netflix.eureka.registry.InstanceRegistry。

  • 服务实例注册表

Server是围绕注册表管理的。有两个InstanceRegistry。
com.netflix.eureka.registry.InstanceRegistry是euraka server中注册表管理的核心接口。职责是在内存中管理注册到Eureka Server中的服务实例信息。实现类有PeerAwareInstanceRegistryImpl。

org.springframework.cloud.netflix.eureka.server.InstanceRegistry对PeerAwareInstanceRegistryImpl进行了继承和扩展,使其适配Spring cloud的使用环境,主要的实现由PeerAwareInstanceRegistryImpl提供。

com.netflix.eureka.registry.InstanceRegistry extends LeaseManager, LookupService 。LeaseManager是对注册到server中的服务实例租约进行管理。LookupService是提供服务实例的检索查询功能。

LeaseManager接口的作用是对注册到Eureka Server中的服务实例租约进行管理,方法有:服务注册,下线,续约,剔除。此接口管理的类目前是InstanceInfo。InstanceInfo代表服务实例信息。

PeerAwareInstanceRegistryImpl 增加了对peer节点的同步复制操作。使得eureka server集群中注册表信息保持一致。

  • 接受服务注册

eureka服务端注册

我们学过Eureka Client在发起服务注册时会将自身的服务实例元数据封装在InstanceInfo中,然后将InstanceInfo发送到Eureka Server。Eureka Server在接收到Eureka Client发送的InstanceInfo后将会尝试将其放到本地注册表中以供其他Eureka Client进行服务发现。
我们学过:通过 eureka/apps/{服务名}注册

在EurekaServerAutoConfiguration中定义了 public FilterRegistrationBean jerseyFilterRegistration ,表名了 表明eureka-server使用了Jersey实现 对外的 restFull接口。注册一个 Jersey 的 filter ,配置好相应的Filter 和 url映射。

public javax.ws.rs.core.Application jerseyApplication(方法:中。
provider.addIncludeFilter(new AnnotationTypeFilter(Path.class));
provider.addIncludeFilter(new AnnotationTypeFilter(Provider.class));
添加一些过滤器,类似于过滤请求地址,Path类似于@RequestMapping,Provider类似于@Controller

在com.netflix.eureka.resources包下,是Eureka Server对于Eureka client的REST请求的定义。看ApplicationResource类(这是一类请求,应用类的请求),类似于应用@Controller注解:@Produces({“application/xml”, “application/json”}),接受xml和json。见名识意 public Response addInstance。添加实例instanceinfo。 方法中,有一句:
registry.register(info, “true”.equals(isReplication));鼠标放在registry上PeerAwareInstanceRegistry接口,点击void register方法。发现 是PeerAwareInstanceRegistryImpl 的方法:public void register(final InstanceInfo info, final boolean isReplication) ,中有一句:super.register(info, leaseDuration, isReplication);
进入下面正题:
com.netflix.eureka.registry.AbstractInstanceRegistry
register方法

在register中,服务实例的InstanceInfo保存在Lease中,Lease在AbstractInstanceRegistry中统一通过ConcurrentHashMap保存在内存中。在服务注册过程中,会先获取一个读锁,防止其他线程对registry注册表进行数据操作,避免数据的不一致。然后从resgitry查询对应的InstanceInfo租约是否已经存在注册表中,根据appName划分服务集群,使用InstanceId唯一标记服务实例。如果租约存在,比较两个租约中的InstanceInfo的最后更新时间lastDirtyTimestamp,保留时间戳大的服务实例信息InstanceInfo。如果租约不存在,意味这是一次全新的服务注册,将会进行自我保护的统计,创建新的租约保存InstanceInfo。接着将租约放到resgitry注册表中。
之后将进行一系列缓存操作并根据覆盖状态规则设置服务实例的状态,缓存操作包括将InstanceInfo加入用于统计Eureka Client增量式获取注册表信息的recentlyChangedQueue和失效responseCache中对应的缓存。最后设置服务实例租约的上线时间用于计算租约的有效时间,释放读锁并完成服务注册。

  • 接受心跳 续租,renew

Eureka服务端接收心跳

在Eureka Client完成服务注册之后,它需要定时向Eureka Server发送心跳请求(默认30秒一次),维持自己在Eureka Server中租约的有效性。

看另一类请求com.netflix.eureka.resources.InstanceResource。下public Response renewLease(方法。看到一行boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
点击renew的实现。
进入下面正题:

Eureka Server处理心跳请求的核心逻辑位于AbstractInstanceRegistry#renew方法中。renew方法是对Eureka Client位于注册表中的租约的续租操作,不像register方法需要服务实例信息,仅根据服务实例的服务名和服务实例id即可更新对应租约的有效时间。
com.netflix.eureka.registry.AbstractInstanceRegistry

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

和服务实例id即可更新对应租约的有效时间。
com.netflix.eureka.registry.AbstractInstanceRegistry

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-swfrFmHP-1713030472778)]
[外链图片转存中…(img-il6NbadO-1713030472778)]
[外链图片转存中…(img-Z3UsxEp1-1713030472779)]
[外链图片转存中…(img-w8SjNVNW-1713030472779)]
[外链图片转存中…(img-xDUCUs0a-1713030472780)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-4ZzjWKiz-1713030472780)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值