作者 | 李祥
给我一个键盘,我敲敲看~
最近有空的时候读了一下 Eureka 的源码,自己对 Eureka 有了更深层次的一些理解,不再仅仅停留在简单会使用的层面,因此想就自己的浅显的认识整理几篇博文,一方面是自我总结,另外一方面也是分享给大家一起交流讨论。
篇幅所限,本文会主要讨论 Eureka 的总体架构,然后重点关注于 Eureka Server 的启动阶段,详细讨论 Eureka 服务端在初始化所做的工作,主要包括核心组件初始化及作用,然后涉及到一些客户端的交互过程。在本文的基础上,后续文章再对本文详细拆分,希望这样的结构能更容易被大家接受。另外,虽然本文是源码解读,但由于本篇涵盖的范围较广,所以不会在各个分析点都粘贴对应的源码,只会在一些开头或者总结处粘出一些源码用来回顾,我们重点关注思想,源码的跟读大家可以自己在本地跟读一下,续篇关注细节时源码可能出现频率也会相应高一些。通过本文,我们可以学习到:
认识 Eureka 是什么,有什么作用
Eureka 的总体架构
Eureka Server 启动过程:如何初始化核心组件
Eureka Server 各核心组件的作用以及依赖交互关系
宏观上认识 Eureka,理解 Eureka 结构及核心流程,为后续拆分分析做准备
上答案:
"Eureka! I've got it!" “找到了!我找到了!”
After he discovered his principle of buoyancy, the ancient Greek scholar Archimedes allegedly yelled out "Eureka!"
在阿基米德发现浮力原理之后,这位古代希腊学者大声叫着:“我找到了!”
不好意思,走错片场了,这两句话是我在词典上抄的。。。但是,我们仔细看一下就能发现 Eureka 原本的语义是一个语气词,表示我找到了。关于 eureka 谷歌翻译给出的解释是:
a cry of joy or satisfaction when one finds or discovers something.
那么我们再来看一下程序员眼中的 Eureka 是什么意思:
Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers.
Eureka 是基于 REST(代表性状态转移)的服务,主要在 AWS 云中用于查找服务,以实现负载均衡和中间层服务器的故障转移。
一句话:Eureka 可以作为一个服务注册中心,提供服务注册、服务续约、服务下线、实例信息维护、服务发现等能力。
上图描述了 Eureka 的基本架构,总体有三个角色组成:
Eureka Server: 提供服务注册与发现
Service Provider: 服务提供方,将自身注册到 Eureka,从而使服务消费方能够找到
Service Consumer: 服务消费方,从 Eureka 获取服务注册表,从而能够消费服务
上图主要是站在服务角度来看 Eureka,但实际上对 Eureka 来说:不管是服务提供方还是服务消费方都是客户端,因此我们可以引出下图:
通过上图我们可以看出,Eureka 分为 Server 端和 Client 端,而 Server 端本身还是一个 Client。
在 Eureka Server 启动之后本身的客户端会向服务端集群注册,然后其余客户端也可以向 Server 注册、续约以及拉取服务注册表进而实现服务发现。
下一节我们将讨论 Eureka Server 的启动过程以及所提供的能力,关于 Eureka Client 的细节本文将尽量简略。
从这一节开始,我们将会相对详细的讨论 Eureka Server 的一些细节,尤其是其启动过程。
首先,我们需要知道一些 Eureka Server 的细则:
eureka server 是运行在 web 容器(jersey,类似于 tomcat),基于 restful 接口对外提供入口
eureka server 可以集群部署,并且 eureka server 本身既是服务端也是客户端,集群之间的注册表会进行同步
eureka 保存有所有向其注册的客户端实例的租约信息,并提供维护
eureka server 对外提供一系列 http 接口包括服务注册、续约、下线、获取注册表、获取注册表增量、获取应用及实例等接口,eureka client 利用底层的网络组件同 eureka serevr 通过这些接口实现网络通信,进而实现服务注册、发现、下线、更新注册表等。
下面我们开始讨论 eureka serevr 启动过程。
eureka server 运行在容器中,其完成自身的初始化依靠的是 EurekaBootStrap 组件。EurekaBootStrap 是一个 listener,依靠这个监听器在 jersey 容器一启动就会开始 eureka server 的初始化。以下配置来源于 eureka 源码的 eureka-server 模块的 web.xml:
com.netflix.eureka.EurekaBootStrap
说明运行在 jersey 容器中的 eureka server 在容器启动时通过 EurekaBootStrap 开始服务端启动过程。
首先看一下 eureka server 启动的总体流程:
结合核心组件关系再看一下(本图可以略过,为了保证完备性这里把这张图挂在这里,供大家阅读源码参考):
这一部分,我们就前面的 eureka server 启动总体来看,eureka server 的初始化依赖于 jersey 的监听器 EurekaBootStrap,然后初始化大方向上又可以分为两步:
public void contextInitialized(ServletContextEvent event) {
try {
initEurekaEnvironment();
initEurekaServerContext();
ServletContext sc = event.getServletContext();
sc.setAttribute(EurekaServerContext.class.getName(), serverContext);
} catch (Throwable e) {
logger.error("Cannot bootstrap eureka server :", e);
throw new RuntimeException("Cannot bootstrap eureka server :", e);
}
}
初始化环境配置
初始化 eureka server 上下文
这里主要就是初始化当前的环境,初始化配置管理器 ConfigurationManager 供后置处理使用以及提供一些默认配置
protected void initEurekaEnvironment() throws Exception {
logger.info("Setting the eureka configuration..");
String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER);
if (dataCenter == null) {
logger.info("Eureka data center value eureka.datacenter is not set, defaulting to default");
ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, DEFAULT);
} else {
ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, dataCenter);
}
String environment = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT);
if (environment == null) {
ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, TEST);
logger.info("Eureka environment value eureka.environment is not set, defaulting to test");
}
}
所谓的配置管理器 ConfigurationManager 的作用就是统一维护 eureka 的的配置信息,并借助 org.apache.commons.configuration 包下的一些核心类实现多级配置的功能,进而对外提供扩展能力。一句话:初始化环境,引入多级配置实例。
在环境配置加载好之后,就开始进入到 eureka server 上下文初始化,这一步涉及到的东西就比较多了,也是最核心的部分。这一步大致上又可以分为以下几部分(当然每个人的看法可能都不一样,代码部分会在本节最后选择性贴一部分,详细见源码):
第一步:初始化 eureka server 服务端配置
// 第一步,加载 eureka-server.properties 文件中的配置
EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();
public DefaultEurekaServerConfig() {
init();
}
private void init() {
String env = ConfigurationManager.getConfigInstance().getString(
EUREKA_ENVIRONMENT, TEST);
ConfigurationManager.getConfigInstance().setProperty(
ARCHAIUS_DEPLOYMENT_ENVIRONMENT, env);
// eurekaPropsFile 对应的就是 eureka-server
String eurekaPropsFile = EUREKA_PROPS_FILE.get();
try {
// ConfigurationManager
// .loadPropertiesFromResources(eurekaPropsFile);
ConfigurationManager
.loadCascadedPropertiesFromResources(eurekaPropsFile);
} catch (IOException e) {
logger.warn(
"Cannot find the properties specified : {}. This may be okay if there are other environment "
+ "specific properties or the configuration is installed with a different mechanism.",
eurekaPropsFile);
}
}
服务端配置为 eureka server 提供必备的信息以及一些行为准则,默认从 eureka-serevr.properties 文件加载,借助 ConfigationManager 管理配置并支持多级配置、配置属性覆盖,同时通过 EurekaServerConfig 接口暴露配访问取能力(面向接口读取)
通过上图可以看出来,eureka server 服务端配置的加载具备自己的默认数据源、配置管理器、配置读取接口。
当需要获取配置项统一基于 EurekaServerConfig 接口,之后我们看 spring-cloud-eureka 的服务端配置也只是基于该接口做了一层简单的封装。
同样的,客户端配置也是一样的思想,因此我们这里统一看待。
第二步:初始化 eureka client:
eureka 的服务端本身也是一个客户端,因此需要初始化属于自己的客户端组件。在初始化 eureka 客户端时需要完成:
初始化服务实例配置默认从 eureka-client.properties 加载,并借助实例配置初始化实例信息 InstanceInfo,然后将实例配置及实例信息交由 ApplicationManager 管理
初始化 eureka server 客户端配置,默认从 eureka-client.properties 文件加载,和服务端配置类似,通过接口 EurekaClientConfig 接口暴露配置访问
完成DiscoveryClient 的实例化及初始化:通过 ApplicationManager 及 EurekaClientConfig 可以实现客户端配置及实例信息的访问,之后相继完成定时任务分配(包括心跳、缓存刷新等)、初始化同服务端通信的网络组件、拉取注册表到本地、向服务端注册(如果配置允许)等核心初始化步骤。
前面的客户端实例配置及 client 配置加载和服务端配置类似,只是多了一步基于配置初始化服务本身的实例信息 InstanceInfo,这一步依赖组件 EurekaConfigBasedInstanceInfoProvider 实现,基本也就是根据配置去做实例化、属性赋值而已,这里不再赘述。
但在当前步骤,就会完成 eureka client(DiscoveryClient) 的实例化,涉及到的细节也比较多了。核心流程上包含以下几步:
赋值 DiscoveryClient 中持有的 EurekaClientConfig,ApplicationInfoManager,InstanceInfo 分别对应客户端配置、服务信息管理器、实例信息,这些信息已经由前面的初始化过程完成
初始化网络通信组件 EurekaTransport eurekaTransport,客户端依靠该组件调用服务端 restful 接口完成各项功能
首次全量拉取服务注册表到本地注册表中 AtomicReference
初始化一些定时任务以及监听器组件,这些组件主要负责之后的定时发送心跳、拉取注册表、刷新本地注册表、状态更新等(这里只列出其功能,详细请见源码或后续的文章)
向 eureka server 注册自己(是否实则取决于客户端配置,当server为集群部署时应该配置为注册)
开启一些监控项
所以,在 DiscoveryClient 初始化完成以后,其状态看起来应该是下面这样:
从上图我们可以看出来,此时 eureka server 自身的客户端此时已经持有客户端配置、应用管理器、实例信息,还初始化了对应的网络组件具备了同服务端通信的能力,而且还向服务端注册了自己以及全量拉取了服务注册表到自己本地的注册表,然后还开启了一些定时任务供后续的服务续约(发送心跳)、刷新注册表等,最后开启一些本地监控。至此,客户端的功能看起来已经比较完备了。
第三步:完成 PeerAwareInstanceRegistry 的初始化
见名知义,PeerAwareInstanceRegistry 主要负责服务端对于客户端实例的注册、续约、下线、获取注册表等请求的具体实现。所以该组件的核心就是:通过 EurekaServerConfig、EurekaClientConfig、EurekaClient 等组件获取配置及本地实例,其维护的核心数据结构包括一个注册表 ConcurrentHashMap
另外,服务端注册表的读写使用了缓存架构,客户端获取服务注册表的顺序依次是只读缓存、读写缓存、服务端注册表。
当注册实例变更时如客户端注册,服务端除了更新注册表以外,也会更新读写缓存。
而当开启只读缓存时,默写缓存的数据默认每 30 秒同步一次到只读缓存,所以客户端获取到的注册表信息可能是有滞后的,这也侧面反应 Eureka 是AP而不是 CP。
```java
// ...
if (shouldUseReadOnlyResponseCache) {
// 默认每 30 秒将读写缓存同步到只读缓存
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
+ responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
// ...
```
```java
private TimerTask getCacheUpdateTask() {
return new TimerTask() {
@Override
public void run() {
logger.debug("Updating the client cache from response cache");
for (Key key : readOnlyCacheMap.keySet()) {
if (logger.isDebugEnabled()) {
logger.debug("Updating the client cache from response cache for key : {} {} {} {}",
key.getEntityType(), key.getName(), key.getVersion(), key.getType());
}
try {
CurrentRequestVersion.set(key.getVersion());
Value cacheValue = readWriteCacheMap.get(key);
Value currentCacheValue = readOnlyCacheMap.get(key);
if (cacheValue != currentCacheValue) {
readOnlyCacheMap.put(key, cacheValue);
}
} catch (Throwable th) {
logger.error("Error while updating the client cache from response cache for key {}", key.toStringCompact(), th);
} finally {
CurrentRequestVersion.remove();
}
}
}
};
}
```
关于客户端向 Eureka Server 注册,从客户端到服务端接口然后下层 PeerAwareInstanceRegistry 的具体实现的交互流程,后面会串联一个完整的例子,这里不再列出。
第四步:完成 PeerEurekaNodes 初始化
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
registry,
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
applicationInfoManager
);
PeerEurekaNodes 代表了 Eureka Server 的集群节点,主要负责集群信息的维护、管理集群节点的生命周期。
第五步:完成 eureka server上下文(context)的创建
serverContext = new DefaultEurekaServerContext(
eurekaServerConfig,
serverCodecs,
registry,
peerEurekaNodes,
applicationInfoManager
);
这一步主要是将服务配置、注册组件、集群信息、应用信息托管给容器上下文以便容器可以随时获取这些信息,如 eureka server 自带的后台 jsp 页面的展示就是从该上下文中获取的,spring-cloud--netflix-eureka 的后台 ftl 页面的展示基本也和此类似。
第六步:从相邻的 eureka 节点拷贝注册表 & 开启通信统计任务
// Copy registry from neighboring eureka node
int registryCount = registry.syncUp();
registry.openForTraffic(applicationInfoManager, registryCount);
// ...
@Override
public int syncUp() {
// Copy entire entry from neighboring DS node
int count = 0;
for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
if (i > 0) {
try {
Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
} catch (InterruptedException e) {
logger.warn("Interrupted during registry transfer..");
break;
}
}
Applications apps = eurekaClient.getApplications();
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
try {
if (isRegisterable(instance)) {
register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
count++;
}
} catch (Throwable t) {
logger.error("During DS init copy", t);
}
}
}
}
return count;
}
// ...
这一步,eureka server 会从其他节点拷贝注册表完成注册表在集群节点间的同步,首次同步失败后,默认会进行 5 次重试,并且每次重试默认等待 30 秒,以最大可能完成集群节点启动后注册表的同步。
第七步:注册所有监控统计项
// Register all monitoring statistics.
EurekaMonitors.registerAllStats();
至此,Eureka Server 就完成了所有初始化过程,开始向客户端提供完整的服务注册与发现服务。
上一小节我们相对详细的看了一下 Eureke Server 的启动过程,其中最为核心的内容即为 Eureka Server 上下文的初始化,对应源码如下:
/**
* init hook for server context. Override for custom logic.
*/
protected void initEurekaServerContext() throws Exception {
// 第一步,加载 eureka-server.properties文件中的配置
EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();
// For backward compatibility
JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);
XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), XStream.PRIORITY_VERY_HIGH);
logger.info("Initializing the eureka client...");
logger.info(eurekaServerConfig.getJsonCodecName());
ServerCodecs serverCodecs = new DefaultServerCodecs(eurekaServerConfig);
ApplicationInfoManager applicationInfoManager = null;
// 第二步,初始化 eureka-server 内部的一个 eureka-client(用来跟踪其他的 eureka-service 节点进行注册和通信的)
if (eurekaClient == null) {
EurekaInstanceConfig instanceConfig = isCloud(ConfigurationManager.getDeploymentContext())
? new CloudInstanceConfig()
: new MyDataCenterInstanceConfig(); //读取 eureka-client.properties 的属性,并通过 EurekaInstanceConfig 接口暴露配置读取能力
// new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get() 根据属性配置初始化 instanceInfo 并由 ApplicationInfoManager 管理
applicationInfoManager = new ApplicationInfoManager(
instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());
// 这里也会读取 eureka-client.properties 的属性,并通过 EurekaClientConfig 接口暴露 eureka-client 配置读取能力;同时初始化默认 Transport 配置(Transport 配置中保存 instanceConfig 的引用)
EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();
eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);
} else {
applicationInfoManager = eurekaClient.getApplicationInfoManager();
}
// 第三步,初始化 PeerAwareInstanceRegistry,实际为注册、下线、续约等的实现
PeerAwareInstanceRegistry registry;
if (isAws(applicationInfoManager.getInfo())) {
registry = new AwsInstanceRegistry(
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
eurekaClient
);
awsBinder = new AwsBinderDelegate(eurekaServerConfig, eurekaClient.getEurekaClientConfig(), registry, applicationInfoManager);
awsBinder.start();
} else {
registry = new PeerAwareInstanceRegistryImpl(
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
eurekaClient
);
}
// 第四步,处理 peer 节点相关的事情 --> 代表了 eureka server 的集群
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
registry,
eurekaServerConfig,
eurekaClient.getEurekaClientConfig(),
serverCodecs,
applicationInfoManager
);
// 第五步,完成eureka server上下文(context)的创建
serverContext = new DefaultEurekaServerContext(
eurekaServerConfig,
serverCodecs,
registry,
peerEurekaNodes,
applicationInfoManager
);
EurekaServerContextHolder.initialize(serverContext);
serverContext.initialize();
logger.info("Initialized server context");
// 第六步,从相邻的 eureka 节点拷贝注册表
// Copy registry from neighboring eureka node
int registryCount = registry.syncUp();
registry.openForTraffic(applicationInfoManager, registryCount);
// 第七步,注册所有的监控统计项
// Register all monitoring statistics.
EurekaMonitors.registerAllStats();
}
这里的代码可以分别对应到前面分析的几步(当然,这里只是根据代码层次自己做的划分),每一步分别初始化不同的核心组件,具体的初始化逻辑可以跟进源码仔细读一下,限于篇幅这里不对每一步的具体代码做分析。但通过我们前面的分析其实可以看出,这些核心组件如 EurekaServerConfig、ApplicationInfoManager、EurekaClientConfig、PeerAwareInstanceRegistry、PeerEurekaNodes 其实分工明确,虽然初始化有顺序依赖但却是相互独立的,最终靠所有组件的合作完成 Eureka Server 的功能。
其实我们去看不管是 spring-cloud-netflix-eureka-server 还是 spring-cloud-netflix-eureka-client 的源码可以发现,spring cloud eureka 也只是对原生的 eureka 做了一层简单的封装,底层使用的仍然是这些组件,只是 eureka server 的初始化以 spring 的习惯分别初始化不同组件对应的 bean,外加引入自己的配置文件罢了,有兴趣的小伙伴可以先看一下原生 eureka 的源码,再比对一下 spring cloud eureka 的源码,你会发现 spring cloud eureka 真的是薄薄的一层,当然这里也不再赘述。
前面我们分析了 Eureka Server 的初始化过程,以及一些核心组件的功能,这里我以服务注册为例,说明一下 client 端到 server 端的交互流程。其他譬如续约、下线、注册表拉取这里不再展开,组件的交互流程基本是类似的,只是调用不同的方法罢了。
通过上图我们可以看出,Eureka Server 初始化完成后,当一个客户端服务实例上线时,会向 Eureka Server 发起注册,依次:
调用网络组件 EurekaTransport的registrationClient.register(instanceInfo) 向服务端发起注册的网络请求
注册请求打到服务端的 ApplicationResource(类似于 Controller),调用 addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication)
ApplicationResource 调用 PeerAwareInstanceRegistry 的注册方法
PeerAwareInstanceRegistry 注册会将要注册的服务实例写入注册表,并失效读写缓存
最后 PeerAwareInstanceRegistry 将注册的实例信息同步到 PeerEurekaNodes 的所有节点(同步注册信息到其他 Eureka Server 节点)
Eureka Client 向 Server 端发起注册的流程大体上就是这样,这里省略了一些细节,可以看出这里用到了之前提到的一些核心组件。除了注册以外,服务续约、下线、拉取注册表等大家可以自己尝试跟一下源码,更多细节我们留到之后的文章。
至此,关于 Eureka 源码的宏观解读已经完成了。本文介绍了 Eureka 的核心架构,以 Eureka Server 的启动为例,引入 Eureka 的一些核心组件的初始化及功能,最后已服务注册为例简单分析了一下 Eureka 的客户端与服务端交互的流程以及核心组件在注册流程中的角色。
本文提到的 Eureka 源码除特殊说明,均指 Netflix/eureka 项目而不是 spring cloud 的 eureka 模块。另外,本文对一些细节做了简化或者省略,譬如配置含义、Eureka Server 自我保护、注册表拉取时的全量拉取、增量拉取以及服务自动摘除等均没有详细提及,也没有引入具体的 demo,只关注了通用的宏观部分。一方面是因为篇幅所限,另外一方面是担心一次关注太多的细节反而会丢失重点。水平有限,大家感兴趣可以自己细致的研究一下源码。轻喷...
Netflix/eureka 源码:https://github.com/Netflix/eureka
spring-cloud-netflix-eureka-server 源码 https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-netflix-eureka-server
spring-cloud-netflix-eureka-client 源码 https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-netflix-eureka-client
全文完
以下文章您可能也会感兴趣:
后端的缓存系统浅谈
从 React 到 Preact 迁移指南
如何成为一名数据分析师:数据的初步认知
复杂业务状态的处理:从状态模式到 FSM
聊聊移动端跨平台数据库 Realm
苹果在医疗健康领域的三个 Kit
响应式编程(下):Spring 5
响应式编程(上):总览
Web 与 App 数据交互原理和实现
iOS 屏幕适配浅谈
工程师成长的必备技能
四维阅读法 - 我的高效学习“秘技”
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 [email protected] 。