服务注册与发现:Eureka
整体简介
Eureka 整体上可分为两个主体: Eureka Server 和 Eureka Client.
服务端注册
- 服务注册: 服务提供者启动时, 会通过 Eureka Client 向 Eureka Server 注册信息, Eureka Server 会存储该服务的信息, Eureka Server 内部有二层缓存机制来维护整个注册表.
- 提供注册表: 服务消费者在调用服务时, 如果 Eureka Client 没有缓存注册表的话, 会从 Eureka Server 获取最新的注册表.
- 同步状态: Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态.
客户端
Eureka Client 是一个 Java 客户端, 用于简化与 Eureka Server 的交互. Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息. 因此当所有的 Eureka Server 节点都宕掉, 服务消费者依然可以使用缓存中的信息找到服务提供者, 但是当服务有更改的时候会出现信息不一致.
Register: 服务注册
服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。
Renew:服务续约
Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。
# 服务续约任务的调用间隔时间,默认为30秒
eureka.instance.lease-renewal-interval-in-seconds=30
# 服务失效的时间,默认为90秒。
eureka.instance.lease-expiration-duration-in-seconds=90
Eviction: 服务剔除
当 Eureka Client 和 Eureka Server 不再有心跳时, Eureka Server 会将该服务实例从服务注册列表中删除, 即服务剔除.
Cancel: 服务下线
Eureka Client 在程序关闭时向 Eureka Server 发送取消请求. 发送请求后, 该客户端实例信息将从 Eureka Server 的实例注册表中删除. 该下线请求不会自动完成, 它需要调用以下内容:
DiscoveryManager.getInstance().shutdownComponent();
GetRegisty: 获取注册列表信息
Eureka Client 从服务器获取注册表信息, 并将其缓存在本地. 客户端会使用该信息查找其他服务, 从而进行远程调用. 该注册列表信息定期 (每 30 秒钟) 更新一次. 每次返回注册列表信息可能与 Eureka Client 的缓存信息不同, Eureka Client 自动处理.
如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 则会重新获取整个注册表信息. Eureka Server 缓存注册列表信息, 整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同. Eureka Client 和 Eureka Server 可以使用 JSON/XML 格式进行通讯. 在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息.
获取服务是服务消费者的基础,所以必有两个重要参数需要注意:
# 启用服务消费者从注册中心拉取服务列表的功能
eureka.client.fetch-registry=true
# 设置服务消费者从注册中心拉取服务列表的间隔
eureka.client.registry-fetch-interval-seconds=30
Remote Call: 远程调用
当 Eureka Client 从注册中心获取到服务提供者信息后, 就可以通过 Http 请求调用对应的服务; 服务提供者有多个时, Eureka Client 客户端会通过 Ribbon 自动进行负载均衡.
自我保护机制
默认情况下, 若 Eureka Server 在一定的 90s 内没有接收到某个微服务实例的心跳, 会注销该实例.
但是在微服务架构下服务之间通常都是跨进程调用, 网络通信往往会面临着各种问题, 比如微服务状态正常, 网络分区故障, 导致此实例被注销.
固定时间内大量实例被注销, 可能会严重威胁整个微服务架构的可用性. 为了解决此问题, Eureka 开发了自我保护机制,
那么什么是自我保护机制呢?
Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%, 如果低于 85%, Eureka Server 即会进入自我保护机制.
Eureka Server 进入自我保护机制, 会出现以下几种情况:
1. Eureka 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务.
2. Eureka 仍然能够接受新服务的注册和查询请求, 但是不会被同步到其它节点上 (即保证当前节点依然可用).
3. 当网络稳定时, 当前实例新的注册信息会被同步到其它节点中.
Eureka 自我保护机制是为了防止误杀服务而提供的一个机制. 当个别客户端出现心跳失联时, 则认为是客户端的问题, 剔除掉客户端; 当 Eureka 捕获到大量的心跳失败时, 则认为可能是网络问题, 进入自我保护机制; 当客户端心跳恢复时, Eureka 会自动退出自我保护机制.
通过在 Eureka Server 配置如下参数, 启或者关闭保护机制, 生产环境建议打开:
eureka.server.enable-self-preservation=true
Eureka 集群原理
Eureka Server 集群相互之间通过 Replicate 来同步数据. 架构中, 节点具有平等性, 彼此可依赖型.
Eureka Server 集群之间的状态是采用异步方式同步, 所以不保证节点间的状态一定是一致的, 不过基本能保证最终状态是一致的.
Eureka 分区: 提供了 Region 和 Zone 两个概念来进行分区, 其中 Region 可以划分为多个 Zone.
Eurka 保证 AP: 由于 Eureka Server 各个节点具有平等性, 几个节点挂掉不会影响其他节点的正常工作,
- 其依然可以提供注册和查询服务. 则只要有一台 Eureka Server 还在, 就能保证注册服务可用 (保证可用性),
- 不过查到的信息可能不是最新的 (不保证强一致性).
Eurka 工作流程
- Eureka Server 启动成功, 等待服务端注册. 在启动过程中如果配置了集群, 集群之间定时通过 Replicate 同步注册表, 每个 Eureka Server 都存在独立完整的服务注册表信息.
- Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务.
- Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求, 证明客户端服务正常.
- 当 Eureka Server 90s 内没有收到 Eureka Client 的心跳, 注册中心则认为该节点失效, 会注销该实例.
- 单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳, 则认为可能为网络异常, 进入自我保护机制, 不再剔除没有上送心跳的客户端.
- 当 Eureka Client 心跳请求恢复正常之后, Eureka Server 自动退出自我保护模式.
- Eureka Client 定时全量或者增量从注册中心获取服务注册表, 并且将获取到的信息缓存到本地.
- 服务调用时,Eureka Client 会先从本地缓存找寻调取的服务. 如果获取不到, 先从注册中心刷新注册表, 再同步到本地缓存.
- Eureka Client 获取到目标服务器信息, 发起服务调用.
- Eureka Client 程序关闭时向 Eureka Server 发送取消请求, Eureka Server 将实例从注册表中删除.
Eureka 的简单使用
启动报错
解决Eureka启动时候报错:sun.reflect.annotation.TypeNotPresentExceptionProxy
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
服务发现原理
Eureka架构图如下:

Region与Availability Zone
Eureka最初设计的目的是AWS(亚马逊网络服务系统)中用于部署分布式系统,所以首先对AWS上的区域(Regin)和可用区(Availability Zone)进行简单的介绍。
- 区域:AWS根据地理位置把某个地区的基础设施服务集合称为一个区域,区域之间相对独立。在架构图上,us-east-1c、us-east-1d、us-east-1e表示AWS中的三个设施服务区域,这些区域中分别部署了一个Eureka集群。
- 可用区:AWS的每个区域都是由多个可用区组成的,而一个可用区一般都是由多个数据中心(简单理解成一个原子服务设施)组成的。可用区与可用区之间是相互独立的,有独立的网络和供电等,保证了应用程序的高可用性。在上述的架构图中,一个可用区中可能部署了多个Eureka,一个区域中有多个可用区,这些Eureka共同组成了一个Eureka集群。
组件与行为
- Application Service:是一个Eureka Client,扮演服务提供者的角色,提供业务服务,向Eureka Server注册和更新自己的信息,同时能从Eureka Server注册表中获取到其他服务的信息。
- Eureka Server:扮演服务注册中心的角色,提供服务注册和发现的功能。每个Eureka Cient向Eureka Server注册自己的信息,也可以通过Eureka Server获取到其他服务的信息达到发现和调用其他服务的目的。
- Application Client:是一个Eureka Client,扮演了服务消费者的角色,通过Eureka Server获取注册到其上其他服务的信息,从而根据信息找到所需的服务发起远程调用。
- Replicate:Eureka Server之间注册表信息的同步复制,使Eureka Server集群中不同注册表中服务实例信息保持一致。
- Make Remote Call:服务之间的远程调用。
- Register:注册服务实例,Client端向Server端注册自身的元数据以供服务发现。
- Renew:续约,通过发送心跳到Server以维持和更新注册表中服务实例元数据的有效性。当在一定时长内,Server没有收到Client的心跳信息,将默认服务下线,会把服务实例的信息从注册表中删除。
- Cancel:服务下线,Client在关闭时主动向Server注销服务实例元数据,这时Client的服务实例数据将从Server的注册表中删除。
- Get Registry:获取注册表,Client向Server请求注册表信息,用于服务发现,从而发起服务间远程调用。
Eureka Client源码解析
Eureka Client为了简化开发人员的开发工作,将很多与Eureka Server交互的工作隐藏起来,自主完成。在应用的不同运行阶段在后台完成工作如图4-4所示

SpringBoot汇项目启动如下自动装配类:
Eukeka Client通过Starter的方式引入依赖,Spring Boot将会为项目使用以下的自动配置类:
- EurekaClientAutoConfiguration:Eureke Client自动配置类,负责EurekaClient中关键Beans的配置和初始化,如ApplicationInfoManager和EurekaClientConfig等。
- RibbonEurekaAutoConfiguration:Ribbon负载均衡相关配置。
- EurekaDiscoveryClientConfiguration:配置自动注册和应用的健康检查器。
读取应用自身配置
通过EurekaDiscoveryClientConfiguration配置类,Spring Boot帮助EurekaClient完成很多必要Bean的属性读取和配置,表4-1列出了EurekaDiscoveryClientConfiguration中的属性读取和配置类。

服务发现
DiscoveryClient是Spring Cloud中用来进行服务发现的顶级接口,在NetflixEureka或者Consul中都有相应的具体实现类

服务发现客户端
DiscoveryClient是Eureka Client的核心类,包括与Eureka Server交互的关键逻辑,具备了以下职能:
- 注册服务实例到Eureka Server中;
- 发送心跳更新与Eureka Server的租约;
- 在服务关闭时从Eureka Server中取消租约,服务下线;
- 查询在Eureka Server中注册的服务实例列表。
类结构

DiscoveryClient类结构
EurekaDiscoveryClient包含一个EurekaClient实例,EurekaClient集成了LookupService的作用是发现活跃的服务实例,并持有Application服务实例信息列表。


EurekaCient在LookupService的基础上扩充了更多的接口,提供了更丰富的获取服务实例的方式,主要有:
- 提供了多种方式获取InstanceInfo,例如根据区域、EurekaServer地址等获取。
- 提供了本地客户端(所处的区域、可用区等)的数据,这部分与AWS密切相关。
- 提供了为客户端注册和获取健康检查处理器的能力。
Eureka中的事件模式
Eureka中的事件模式属于观察者模式,事件监听器将监听Client的服务实例信息变化,触发对应的处理事件,图为Eureka事件的类图

DiscoveryClient构造函数
在DiscoveryClient构造函数中,Eureka Client会执行从EurekaServer中拉取注册表信息、服务注册、初始化发送心跳、缓存刷新(重新拉取注册表信息)和按需注册定时任务等操作,可以说DiscoveryClient的构造函数贯穿了Eureka Client启动阶段的各项工作
public DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config) {
this(applicationInfoManager, config, null);
}
核心的构造方法如下,比较长
@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(","));
//config#shouldFetchRegistry(对应配置为eureka.client.fetch-register)为true表示Eureka Client将从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;
}
//config#shouldRegisterWithEureka(对应配置为eureka.client.register-with-eureka)为true表示EurekaClient将注册到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());
//如果上述的两个配置均为false,那么Discovery的初始化将直接结束,表示该客户端既不进行服务注册也不进行服务发现。
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 {
// 定义一个基于线程池的定时器线程池ScheduledExecutorService,线程池大小为2,一个线程用于发送心跳,另一个线程用于缓存刷新,同时定义了发送心跳和缓存刷新线程池
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
//初始化Eureka Client与Eureka Server进行HTTP交互的Jersey客户端,将AbstractDiscoveryClientOptionalArgs中的属性用来构建EurekaTransport
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中拉取注册表信息
//在Eureka Client向EurekaServer注册前,需要先从Eureka Server拉取注册表中的信息,这是服务发现的前提。
// 通过将Eureka Server中的注册表信息缓存到本地,就可以就近获取其他服务的相关信息,减少与Eureka Server的网络通信。
if (clientConfig.shouldFetchRegistry()) {
try {
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);
}
}
//拉取完Eureka Server中的注册表信息后,将对服务实例进行注册
// call and execute the pre registration handler before all background tasks (inc registration) is started
if (this.preRegistrationHandler != null) {
this.preRegistrationHandler.beforeRegistration();
}
if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
try {
// 发起注册
if (!register() ) {
//注册失败
throw new IllegalStateException("Registration error at startup. Invalid server response.");
}
} catch (Throwable th) {
logger.error("Registration error at startup: {}", th.getMessage());
throw new IllegalStateException(th);
}
}
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
//构造函数的最后将初始化并启动发送心跳、缓存刷新和按需注册等定时任务。
initScheduledTasks();
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register timers", e);
}
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
initRegistrySize = this.getApplications().size();
registrySize = initRegistrySize;
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",
initTimestampMs, initRegistrySize);
}
最后总结一下,在DiscoveryClient的构造函数中,主要依次做了以下的事情:
1)相关配置的赋值,类似ApplicationInfoManager、EurekaClientConfig等。
2)备份注册中心的初始化,默认没有实现。
3)拉取Eureka Server注册表中的信息。
4)注册前的预处理。
5)向Eureka Server注册自身。
6)初始化心跳定时任务、缓存刷新和按需注册等定时任务。
DiscoveryClient拉取注册表信息
在DiscoveryClient的构造函数中,调用了DiscoveryClient#fetchRegistry方法从Eureka Server中拉取注册表信息。
一般来讲,在Eureka客户端,除了第一次拉取注册表信息,之后的信息拉取都会尝试只进行增量拉取(第一次拉取注册表信息为全量拉取),下面将分别介绍拉取注册表信息的两种实现:
- 全量拉取注册表信息DiscoveryClient#getAndStoreFullRegistry
- 增量式拉取注册表信息DiscoveryClient#getAndUpdateDelta

getAndStoreFullRegistry方法可能被多个线程同时调用,导致新拉取的注册表被旧的注册表覆盖,对此,Eureka通过类型为AtomicLong的currentUpdateGeneration对apps的更新版本进行跟踪。如果更新版本不一致,说明本次拉取注册表信息已过时,不需要缓存到本地。
服务注册
在拉取完Eureka Server中的注册表信息并将其缓存在本地后,Eureka Client将向Eureka Server注册自身服务实例元数据,主要逻辑位于Discovery#register方法中
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
初始化定时任务
在DiscoveryClient#initScheduledTasks方法中初始化了三个定时器任务:
- 一个用于向Eureka Server拉取注册表信息刷新本地缓存;
- 一个用于向Eureka Server发送心跳;
- 一个用于进行按需注册的操作
服务下线
一般情况下,应用服务在关闭的时候,Eureka Client会主动向Eureka Server注销自身在注册表中的信息。DiscoveryClient中对象销毁前执行的清理方法如下所示:
@PreDestroy
@Override
public synchronized void shutdown() {
//同步方法
if (isShutdown.compareAndSet(false, true)) {
logger.info("Shutting down DiscoveryClient ...");
if (statusChangeListener != null && applicationInfoManager != null) {
//注销监听器
applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
}
cancelScheduledTasks();
// If APPINFO was registered
if (applicationInfoManager != null
&& clientConfig.shouldRegisterWithEureka()
&& clientConfig.shouldUnregisterOnShutdown()) {
//服务下线
applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
unregister();
}
//关闭,Jersy客户端
if (eurekaTransport != null) {
eurekaTransport.shutdown();
}
heartbeatStalenessMonitor.shutdown();
registryStalenessMonitor.shutdown();
Monitors.unregisterObject(this);
logger.info("Completed shut down of DiscoveryClient");
}
}
Eureka Server源码解析
Eureka Server作为一个开箱即用的服务注册中心,提供了以下的功能,用以满足与Eureka Client交互的需求:
- 服务注册
- 接受服务心跳
- 服务剔除
- 服务下线
- 集群同步
- 获取注册表中服务实例信息
服务实例注册表
InstanceRegistry是Eureka Server中注册表管理的核心接口。图中出现了两个InstanceRegistry,最下面的InstanceRegistry对EurekaServer的注册表实现类PeerAwareInstanceRegistryImpl进行了继承和扩展,使其适配Spring Cloud的使用环境,主要实现由PeerAwareInstanceRegistryImpl提供。

其中,LeaseManager接口作用是对注册到Eureka Server中的服务实例租约进行管理,分别有服务注册、服务下线、服务租约更新以及服务剔除等操作
LeaseManager中管理的对象是Lease, Lease代表一个Eureka Client服务实例信息的租约,它提供了对其内持有的类的时间有效性操作。Lease持有的类是代表服务实例信息的InstanceInfo。Lease中定义了租约的操作类型,分别是注册、下线、更新,同时提供了对租约中时间属性的各项操作。租约默认有效时长(duration)为90秒。
public class Lease<T> {
enum Action {
Register, Cancel, Renew
};
public static final int DEFAULT_DURATION_IN_SECS = 90;
服务注册
Eureka Client在发起服务注册时会将自身的服务实例元数据封装在InstanceInfo中,然后将InstanceInfo发送到Eureka Server。Eureka Server在接收到Eureka Client发送的InstanceInfo后将会尝试将其放到本地注册表中以供其他Eureka Client进行服务发现。
服务注册的主要实现位于AbstractInstanceRegistry#registry方法中,代码如下所示:
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
//读锁
read.lock();
try {
//获取根据 appName分组后的registry
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
//如果存在阿禁止,则返回已经存在的数据,否则返回null
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
//根据id获取已经存在的实例的租约
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// Retain the last dirty timestamp without overwriting it, if there is already a lease
//租约信息已经存在
if (existingLease != null && (existingLease.getHolder() != null)) {
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
// this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
// InstanceInfo instead of the server local copy.
//如果该实例租约已经存在,则比较更新时间,最大值的注册信息有效
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
" than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
registrant = existingLease.getHolder();
}
} else {//不存在租约信息
// The lease does not exist and hence it is a new registration
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
// Since the client wants to register it, increase the number of clients sending renews
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
updateRenewsPerMinThreshold();
}
}
logger.debug("No previous lease information found; it is new registration");
}
//创建新的租约信息
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
gMap.put(registrant.getId(), lease);
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
// This is where the initial state transfer of overridden status happens
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
+ "overrides", registrant.getOverriddenStatus(), registrant.getId());
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
// Set the status based on the overridden status rules
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
// If the lease is registered with UP status, set lease service up timestamp
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})",
registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
} finally {
//释放锁
read.unlock();
}
}
接受服务心跳
在Eureka Client完成服务注册之后,它需要定时向Eureka Server发送心跳请求(默认30秒一次),维持自己在Eureka Server中租约的有效性。
Eureka Server处理心跳请求的核心逻辑位于AbstractInstanceRegistry#renew方法中
public boolean renew(String appName, String id, boolean isReplication) {
RENEW.increment(isReplication);
//获取集群租约信息
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToRenew = null;
if (gMap != null) {
leaseToRenew = gMap.get(id);
}
//租约不存在则返回false
if (leaseToRenew == null) {
RENEW_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
return false;
} else {
InstanceInfo instanceInfo = leaseToRenew.getHolder();
if (instanceInfo != null) {
// touchASGCache(instanceInfo.getASGName());
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
instanceInfo, leaseToRenew, isReplication);
if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}"
+ "; re-register required", instanceInfo.getId());
RENEW_NOT_FOUND.increment(isReplication);
return false;
}
if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
logger.info(
"The instance status {} is different from overridden instance status {} for instance {}. "
+ "Hence setting the status to overridden status", instanceInfo.getStatus().name(),
overriddenInstanceStatus.name(),
instanceInfo.getId());
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
}
}
renewsLastMin.increment();
//更新租约中的有效时间
leaseToRenew.renew();
return true;
}
}
服务剔除
如果Eureka Client在注册后,既没有续约,也没有下线(服务崩溃或者网络异常等原因),那么服务的状态就处于不可知的状态,不能保证能够从该服务实例中获取到回馈,所以需要服务剔除AbstractInstanceRegistry#evict方法定时清理这些不稳定的服务,该方法会批量将注册表中所有过期租约剔除
服务剔除#evict方法中有很多限制,都是为了保证Eureka Server的可用性:
- 自我保护时期不能进行服务剔除操作。
- 过期操作是分批进行。
- 服务剔除是随机逐个剔除,剔除均匀分布在所有应用中,防止在同一时间内同一服务集群中的服务全部过期被剔除,以致大量剔除发生时,在未进行自我保护前促使了程序的崩溃。
总结
Eureka为Spring Cloud提供了高可用的服务发现与注册组件,利用Eureka, SpringCloud开发者能够更快地融入到微服务的开发中。Eureka Server作为服务注册中心,为Eureka Client提供服务注册和服务发现的能力,它既可单机部署,也可以通过集群的方式进行部署,通过自我保护机制和集群同步复制机制保证Eureka的高可用性和网络分区容忍性,保证Eureka Server集群的注册表数据的最终一致性;Eureka Client方便了与Eureka Server的交互,它与Eureka Server的一切交互,包括服务注册、发送心跳续租、服务下线和服务发现,都是在后台自主完成的,简化了开发者的开发工作。
当然Eureka也存在缺陷。由于集群间的同步复制是通过HTTP的方式进行,基于网络的不可靠性,集群中的Eureka Server间的注册表信息难免存在不同步的时间节点,不满足CAP中的C(数据一致性)。
1763

被折叠的 条评论
为什么被折叠?



