Download as pdf or txt
Download as pdf or txt
You are on page 1of 14

一手资源尽在:666java.com

本文由 简悦 SimpRead 转码, 原文地址 kaiwu.lagou.com

上一课时,我们全面介绍了服务治理的解决方案,引出了 Spring Cloud Netflix Eureka 组件。


Eureka 分为服务器端组件和客户端组件,今天我们将讨论 Eureka 服务器的构建方式及其实现原理。

基于 Eureka 构建注册中心
基于 Eureka 构建服务注册中心涉及两大部分内容,首先我们将给出构建单个 Eureka 服务器的方法。
但是,Eureka 服务器不能保证高可用,因此在生产环境中,我们一般都还需要构建 Eureka 服务器集
群。

1. 构建单点 Eureka 服务器

我们将创建一个新的 Maven 工程并命名为 eureka-server。eureka-server 是一个 Spring Boot 项目。


同时我们引入了 spring-cloud-starter-eureka-server 依赖,该依赖是 Spring Cloud 中实现 Spring
Cloud Netflix Eureka 功能的主体 jar 包:

<dependency>

<groupId>org.springframework.cloud</groupId>

<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>

</dependency>

引入 Maven 依赖之后就可以创建 Spring Boot 的启动类,在示例代码中,我们把该启动类命名为


EurekaServerApplication,代码如下所示。

@SpringBootApplication

@EnableEurekaServer

public class EurekaServerApplication {

public static void main(String[] args) {

SpringApplication.run(EurekaServerApplication.class, args);

}
一手资源尽在:666java.com
请注意,在上面的代码中,我们在启动类上加了一个 @EnableEurekaServer 注解。在 Spring Cloud
中,包含 @EnableEurekaServer 注解的服务意味着就是一个 Eureka 服务器组件。

我们运行这个 EurekaServerApplication 类并访问网站 http://localhost:8761/,如果得到如下图中所示


的 Eureka 服务监控页面,则意味着 Eureka 服务器已经启动成功。

Eureka 服务监控页面

虽然目前还没有任何一个服务注册到 Eureka 中,但从上图中,我们还是得到了关于 Eureka 服务器内


存、CPU 等的有用信息。

同时,Eureka 也为开发人员提供了一系列的配置项。这些配置项可以分成三大类,一类用于控制
Eureka 服务器端行为,以 eureka.server 开头;一类则是从客户端角度出发考虑配置需求,以
eureka.client 开头;而最后一类则关注于注册到 Eureka 的服务实例本身,以 eureka.instance 开
头。请注意,Eureka 除了充当服务器端组件之外,实际上也可以作为客户端注册到 Eureka 本身,这时
候它使用的就是客户端配置项。

Eureka 的配置项很多,我们无意一一进行展开。在日常开发过程中,使用的最多的还是客户端相关的
配置,所以这里以客户端配置为例。现在,我们尝试在 eureka-server 工程的 application.yml 文件中
添加了如下配置信息。

server:

port: 8761

eureka:

client:

registerWithEureka: false

fetchRegistry: false
一手资源尽在:666java.com
serviceUrl:

defaultZone: http:

在这些配置项中,我们看到了三个以 eureka.client 开头的客户端配置项,它们分别是


registerWithEureka、fetchRegistry 和 serviceUrl。从配置项的命名上我们不难看出,
registerWithEureka 用于指定是否把当前的客户端实例注册到 Eureka 服务器中,而 fetchRegistry 则
用于指定是否从 Eureka 服务器上拉取已注册的服务信息。这两个配置项默认都是 true,但这里都将其
设置为 false。因为在微服务体系中,包括 Eureka 服务在内的所有服务对于注册中心来说都可以算作客
户端,而 Eureka 服务显然不同于业务服务,我们不希望 Eureka 服务对自身进行注册。而 serviceUrl
配置项用于服务地址,这个配置项在构建 Eureka 服务器集群是很有用,让我们一起来看一下。

2. 构建 Eureka 服务器集群

前面我们介绍了构建单个 Eureka 服务器的方法,这种运行 Eureka 服务的方式一般称为 Standalone 模


式。考虑到单个 Eureka 服务可能存在的单点失效问题,我们通常都需要构建一个 Eureka 服务器集群
来确保注册中心本身的可用性。与传统的集群构建方式不同,如果我们把 Eureka 也视为一个服务,也
就是说 Eureka 服务自身也能注册到其他 Eureka 服务上,从而实现相互注册,并构成一个集群。在
Eureka 中,这种实现高可用的部署方式被称为 Peer Awareness 模式。

现在我们准备两个 Eureka 服务实例 eureka1 和 eureka2。在 Spring Boot 中,我们分别提供


application-eureka1.yml 和 application-eureka2.yml 这两个配置文件来设置相关的配置项。其中
application-eureka1.yml 配置文件的内容如下:

server:

port: 8761

eureka:

instance:

hostname: eureka1

client

serviceUrl

defaultZone: http:

对应的,application-eureka2.yml 配置文件的内容如下:

server:
一手资源尽在:666java.com
port: 8762

eureka:

instance:

hostname: eureka2

client

serviceUrl

defaultZone: http:

这里就出现了一个 Eureka 实例管理类配置项 eureka.instance.hostname,用于指定当前 Eureka


服务的主机名称。然后,我们注意到 application-eureka1.yml 和 application-eureka2.yml 中的配置
项完全一致,区别只是调整了端口和地址的引用。构建 Eureka 集群模式的关键点在于使用客户端配置
项 eureka.client.serviceUrl.defaultZone 用于指向集群中的其他 Eureka 服务器。所以 Eureka 集群的
构建方式实际上就是将自己作为服务并向其他注册中心注册自己,这样就形成了一组互相注册的服务注
册中心以实现服务列表的同步。显然,这个场景下 registerWithEureka 和 fetchRegistry 配置项应该都
使用其默认的 true 值,所以我们不需要对其进行显式的设置。

如果你尝试使用本机搭建集群环境,显然 eureka.instance.hostname 配置项中的 eureka1 和 eureka2


是无法访问的,所以需要在本机 hosts 文件中添加以下信息。

127.0.0.1 eureka1

127.0.0.1 eureka2

现在启动这两个 Eureka 服务,然后分别打开 http://127.0.0.1:8761/ 和 http://127.0.0.1:8762/ 端点可


以看到各自的服务注册效果。你可以根据这里的步骤在自己的电脑上演练这个过程,并通过两个
Eureka 服务的启动日志以及控制台界面来验证高可用架构的效果。

理解 Eureka 服务器实现原理
在介绍完 Eureka 服务器的构建方式之后,我们重点来讲解 Eureka 服务器的实现原理。

Eureka 核心概念

我们在对 Eureka 的内部结构做进一步展开,可以得到如下所示的注册中心细化模型图。


一手资源尽在:666java.com

Eureka 细化架构图

在上图中,Eureka 有以下几个概念与服务治理直接相关,首当其冲的是服务注册。服务注册
(Register)是服务治理的最基本概念,内嵌了 Eureka 客户端的各个微服务通过向 Eureka 服务器提供
IP 地址、端点等各项与服务发现相关的基本信息完成服务注册操作。

因为 Eureka 客户端与服务器端通过短连接完成交互,所以在服务续约(Renew)中,Eureka 客户端需


要每隔一定时间主动上报自己的运行时状态,从而进行服务续约。

服务取消(Cancel)的意思就是 Eureka 客户端主动告知 Eureka 服务器自己不想再注册到 Eureka


中。当 Eureka 客户端连续一段时间没有向 Eureka 服务器发送服务续约信息时,Eureka 服务器就会认
为该服务实例已经不再运行,从而将其从服务列表中进行剔除(Evict)。

显然,对于一个注册中心而言,想要理解它的设计理念和实现原理,我们需要分别关注 Eureka 中如何


对服务注册信息的存储和管理的具体机制。在接下来的内容中,我们将重点从 Eureka 的服务存储和缓
存处理这两个维度出发,基于源码来深入剖析原理。

Eureka 服务存储源码解析

对于一个注册中心而言,我们首先需要关注它的数据存储方法。在 Eureka 中,我们发现


InstanceRegistry 接口及其实现类(位于 com.netflix.eureka.registry 包中)承接了这部分职能。
InstanceRegistry 的类层结构如下所示:

InstanceRegistry 类层结构图

从上图中,不难看出 Spring Cloud 中同样存在一个 InstanceRegistry(位于


org.springframework.cloud.netflix.eureka.server 包中),它实际上是基于 Netflix 中
InstanceRegistry 实现的一种包装。我们在上图中 InstanceRegistry 接口的实现类
AbstractInstanceRegistry 中发现了 Eureka 用于保存注册信息的数据结构,如下所示:
一手资源尽在:666java.com
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>
registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

可以看到这是一个双层的 HashMap,采用的是 JDK 中线程安全的 ConcurrentHashMap。其中第一


层的 ConcurrentHashMap 的 Key 为 spring.application.name,也就是服务名,Value 为一个
ConcurrentHashMap;而第二层的 ConcurrentHashMap 的 Key 为 instanceId,也就是服务的唯一实
例 ID,Value 为 Lease 对象。Eureka 采用 Lease(租约)这个词来表示对服务注册信息的抽象,
Lease 对象保存了服务实例信息以及一些实例服务注册相关的时间,如注册时间
registrationTimestamp、最新的续约时间 lastUpdateTimestamp 等。如果用图形化的表达方式来展
示这种数据结构,可以参考下图:

服务注册信息的存储结构示意图

而对于 InstanceRegistry 本身,它也继承了 Eureka 中两个非常重要的接口,即 LeaseManager 接口


和 LookupService 接口。其中 LeaseManager 接口定义如下:

public interface LeaseManager<T> {

void register(T r, int leaseDuration, boolean isReplication);

boolean cancel(String appName, String id, boolean isReplication);

boolean renew(String appName, String id, boolean isReplication);

void evict();

显然 LeaseManager 做的事情就是 Eureka 注册中心模型中的服务注册、服务续约、服务取消和服务


剔除等核心操作,关注于对服务注册过程的管理。而 LookupService 接口定义如下,关注于对应用程序
与服务实例的管理:

public interface LookupService<T> {


一手资源尽在:666java.com
Application getApplication(String appName);

Applications getApplications();

List<InstanceInfo> getInstancesById(String id);

InstanceInfo getNextServerFromEureka(String virtualHostname, boolean secure);

在内部实现上,实际上对于注册中心服务器而言,服务注册、续约、取消和剔除等不同操作所执行的工
作流程基本一致,即都是对服务存储的操作,并把这一操作同步到其他 Eureka 节点。我们这里选择用
于服务注册操作的 register 方法进行展开,register 方法非常长,我们对源码进行裁剪,得出如下所示
的核销处理流程:

public void register(InstanceInfo registrant, int leaseDuration, boolean


isReplication) {

try {

Map<String, Lease<InstanceInfo>> gMap =


registry.get(registrant.getAppName());

REGISTER.increment(isReplication);

if (gMap == null) {

Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());

if (existingLease != null && (existingLease.getHolder() != null)) {

} else {

Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant,


leaseDuration);
一手资源尽在:666java.com
gMap.put(registrant.getId(), lease);

registrant.setActionType(ActionType.ADDED);

registrant.setLastUpdatedTimestamp();

invalidateCache(registrant.getAppName(), registrant.getVIPAddress(),
registrant.getSecureVipAddress());

AbstractInstanceRegistry 中其他的 cancel、renew 方法也是同样的处理逻辑,这里不再展开。

Eureka 服务缓存源码解析

Eureka 服务器端组件的另一个核心功能是提供服务列表。为了提高性能,Eureka 服务器会缓存一份所


有已注册的服务列表,并通过一定的定时机制对缓存数据进行更新。

我们知道为了获取注册到 Eureka 服务器上具体某一个服务实例的详细信息,可以访问如下地址:

http://<eureka-server-ip>:8761/eureka/apps/<APPID>

该地址代表的就是一个普通的 HTTP GET 请求。Eureka 中所有对服务器端的访问都是通过 RESTful 风


格的资源(Resource) 进行获取,ApplicationResource 类(位于 com.netflix.eureka.resources 包
中)提供了根据应用获取注册信息的入口。我们来看该类的 getApplication 方法,核心代码如下所示:

Key cacheKey = new Key(

Key.EntityType.Application,

appName,

keyType,

CurrentRequestVersion.get(),

EurekaAccept.fromString(eurekaAccept)

);
一手资源尽在:666java.com
String payLoad = responseCache.get(cacheKey);

if (payLoad != null) {

logger.debug("Found: {}", appName);

return Response.ok(payLoad).build();

} else {

logger.debug("Not Found: {}", appName);

return Response.status(Status.NOT_FOUND).build();

可以看到这里是构建了一个 cacheKey,并直接调用了 responseCache.get(cacheKey) 方法来返回一个


字符串并构建响应。从命名上看,不难想象这里使用了缓存机制。我们来看 ResponseCache 的定义,
如下所示,其中最核心的就是这里的 get 方法:

public interface ResponseCache {

void invalidate(String appName, @Nullable String vipAddress, @Nullable String


secureVipAddress);

AtomicLong getVersionDelta();

AtomicLong getVersionDeltaWithRegions();

String get(Key key);

byte[] getGZIP(Key key);

从类层关系上看,ResponseCache 只有一个实现类 ResponseCacheImpl,我们来看它的 get 方法,


发现该方法使用了如下处理策略:

Value getValue(final Key key, boolean useReadOnlyCache) {


一手资源尽在:666java.com
Value payload = null;

try {

if (useReadOnlyCache) {

final Value currentPayload = readOnlyCacheMap.get(key);

if (currentPayload != null) {

payload = currentPayload;

} else {

payload = readWriteCacheMap.get(key);

readOnlyCacheMap.put(key, payload);

} else {

payload = readWriteCacheMap.get(key);

} catch (Throwable t) {

logger.error("Cannot get value for key : {}", key, t);

return payload;

}
一手资源尽在:666java.com
可以看到上述代码中有两个缓存,一个是 readOnlyCacheMap,一个是 readWriteCacheMap。其
中 readOnlyCacheMap 就是一个 JDK 中的 ConcurrentMap,而 readWriteCacheMap 使用的则是
Google Guava Cache 库中的 LoadingCache 类型。在创建 LoadingCache 过程中,缓存数据的来源是
调用 generatePayload 方法来生成。而在这个 generatePayload 方法中,就会调用前面介绍的
AbstractInstanceRegistry 中的 getApplications 方法获取应用信息并放到缓存中。这样我们就实现了
把注册信息与缓存信息进行关联。

这里有一个设计和实现上的技巧。把缓存设计为一个只读的 readOnlyCacheMap 以及一个可读写的


readWriteCacheMap,可以更好地分离职责。但因为两个缓存中保存的实际上是同一份数据,所以,
我们在不断更新 readWriteCacheMap 时,也需要确保 readOnlyCacheMap 中的数据得到同步。为此
ResponseCacheImpl 提供了一个定时任务 CacheUpdateTask,如下所示:

private TimerTask getCacheUpdateTask() {

return new TimerTask() {

@Override

public void run() {

for (Key key : readOnlyCacheMap.keySet()) {

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) {

}
一手资源尽在:666java.com
}

};

显然,这个定时任务主要是从 readWriteCacheMap 更新数据到 readOnlyCacheMap。

Eureka 高可用源码解析

我们已经在前面的内容中了解到 Eureka 的高可用部署方式被称为 Peer Awareness 模式。对应的,我


们在 InstanceRegistry 的类层结构中也已经看到了它的一个扩展接口 PeerAwareInstanceRegistry
以及该接口的实现类 PeerAwareInstanceRegistryImpl。

我们还是围绕服务注册这个场景展开讨论,在 PeerAwareInstanceRegistryImpl 中同样存在一个


register 方法,如下所示:

@Override

public void register(final InstanceInfo info, final boolean isReplication) {

int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;

if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0)


{

leaseDuration = info.getLeaseInfo().getDurationInSecs();

super.register(info, leaseDuration, isReplication);

replicateToPeers(Action.Register, info.getAppName(), info.getId(), info,


null, isReplication);

我们在这里看到了一个非常重要的 replicateToPeers 方法,该方法作就是用来实现服务器节点之间的


状态同步。replicateToPeers 方法的核心代码如下所示:

for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {

if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
一手资源尽在:666java.com
continue;

replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);

为了理解这个操作,我们首先需要理解 Eureka 中的集群模式,这部分代码位于


com.netflix.eureka.cluster 包中,其中包含了代表节点的 PeerEurekaNode 和 PeerEurekaNodes
类,以及用于节点之间数据传递的 HttpReplicationClient 接口。而 replicateInstanceActionsToPeers
方法中则根据不同的 Action 来调用 PeerEurekaNode 的不同方法。例如,如果是 StatusUpdate
Action,则会调动 PeerEurekaNode 的 statusUpdate 方法,而该方法又会执行如下代码;

replicationClient.statusUpdate(appName, id, newStatus, info);

这句代码完成了 PeerEurekaNode 之间的通信,而 replicationClient 是 HttpReplicationClient 接口的


实例,该接口定义如下:

public interface HttpReplicationClient extends EurekaHttpClient {

EurekaHttpResponse<Void> statusUpdate(String asgName, ASGStatus newStatus);

EurekaHttpResponse<ReplicationListResponse> submitBatchUpdates(ReplicationList
replicationList);

HttpReplicationClient 接口继承自 EurekaHttpClient 接口,而 EurekaHttpClient 接口属于 Eureka 客


户端组件,我们会在下一课时介绍 Eureka 客户端基本原理时进行详细介绍。在这里,我们只需要明白
Eureka 提供了 JerseyReplicationClient(位于 com.netflix.eureka.transport 包下)这一基于 Jersey
框架实现的 HttpReplicationClient。以 statusUpdate 方法为例,它的实现过程如下:

@Override

public EurekaHttpResponse<Void> statusUpdate(String asgName, ASGStatus


newStatus) {

ClientResponse response = null;

try {
一手资源尽在:666java.com
String urlPath = "asg/" + asgName + "/status";

response = jerseyApacheClient.resource(serviceUrl)

.path(urlPath)

.queryParam("value", newStatus.name())

.header(PeerEurekaNode.HEADER_REPLICATION, "true")

.put(ClientResponse.class);

return EurekaHttpResponse.status(response.getStatus());

} finally {

if (response != null) {

response.close();

这是典型的基于 Resource 的 RESTful 风格的调用方法,用到了 ApacheHttpClient4 工具类。通过以上


分析,我们已经从主要维度上掌握了整个 Eureka 服务器端内部的运行机制。

小结与预告
今天我们讨论的是 Eureka 服务器端组件的相关内容,可以看到基于 Spring Cloud 框架,构建一个
Eureka 注册中心所需要做的事情仅仅只是添加一个注解。但在内部实现上,Eureka 服务器端需要考虑
各个微服务实例的存储和获取等核心流程,也需要考虑如何确保注册中心本身的高可用问题。我们基于
源码,对这些流程和问题底层的原理进行了详细的分析。

这里给你留一道思考题:Eureka 是如何实现自身的高可用架构的?

讲完 Eureka 服务器端组件,下一课时,我将和你一起继续讨论 Eureka 的客户端组件的使用方法和实


现原理。

You might also like