微服务注册中心 Eureka解析
随着时代的发展,应用服务的架构也随之进行不停的变迁,从单一的架构到面向服务的SOA、以及正在流行的微服务架构,甚至正在火热的服务网格,无论何种架构,适合于技术的需要以及业务的发展才是最重要的。因此,无论是以Spring Cloud和Dubbo第一代的微服务框还是以Service Mesh下一代的微服务框架,对于服务的治理仍然是微服务架构中最核心、最重要的一部分,服务治理最基础的组件是注册中心。
针对注册中心的解决方案,目前以阿里系的Dubbo 和Spring系的Spring Cloud 2分天下,Dubbo目前支持Zookeeper、Redis、Multicast 和 Simple,其官方推荐 Zookeeper。而Spring Cloud 支持Zookeeper、Consul 和 Eureka,其官方推荐 Eureka。
本文将基于Spring家族的Spring Cloud生态,主要聚焦其核心组件Eureka 的内部实现原理,简要解析Eureka 的总体架构,然后剖析服务信息的存储结构,最后探究跟服务生命周期相关的典型机制。首先我们了解一下Eureka的总体架构图:
从上述架构图中我们可以看到:整个Eureka体系中,主要包括2个Roles:
1、Eureka Client:分为Applicaton Service(服务提供方)和Application Client(服务消费方)。每个区域有一个Eureka集群,并且每个区域至少有一个Eureka服务器可以处理区域故障,以防服务器瘫痪。
2、Eureka Server:Eureka Client向Eureka Server注册,并将自己的一些客户端信息发送Eureka Server。然后,Eureka Client通过向Eureka Server发送心跳(每30秒)来续约服务的。如果客户端持续不能续约,那么,它将在大约90秒内从服务器注册表中删除。
注册信息和续订被复制到集群中的Eureka Server所有节点。来自任何区域的Eureka Client都可以查找注册表信息(每30秒发生一次)。根据这些注册表信息,Application Client可以远程调用Applicaton Service来消费服务
大家有没有想过一个问题:既然作为服务注册中心,必定会存储服务的相关信息,Eureka的数据是如何存储的呢?我们知道,ZooKeeper 是将服务信息保存在树形节点上。对Eureka而言,其数据存储分为两层:数据存储层和缓存层。
Eureka Client 在拉取服务信息时,先从缓存层获取,如果获取不到,先把数据存储层的数据加载到缓存中,再从缓存中获取。值得注意的是,数据存储层的数据结构是服务信息,而缓存中保存的是经过处理加工过的、可以直接传输到 Eureka Client 的数据结构。
为什么要这样设计呢?Eureka 数据结构设计初衷是为了把内部的数据存储结构与对外的数据结构隔离开,就像是我们平时在进行接口设计一样,对外输出的数据结构和数据库中的数据结构往往都是不一样的。
基本原理:
Client服务启动后向Eureka注册,Eureka Server会将注册信息借助“Replicate”向其他Eur
当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服
务注册中心将服务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。
服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,以证明当前服务
是可用状态。Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例。
基于Eureka架构原理,此处针对其核心活动进行源码解析,此处主要解析在实际项目维护过程中存在的关键流程:服务注册与服务剔除,以便于大家能够再一次深入了解其内部具体实现:
1、Register:服务注册
此功能由DiscoveryClient类开始:该类包含了Eureka Client向Eureka Server的相关方法。其中DiscoveryClient实现了EurekaClient接口,并且它是一个单例模式,而EurekaClient继承了LookupService接口。我们先看下DiscoveryClient类源码,在代码中我们看到服务注册的方法register(),该方法是通过Http请求向Eureka Client注册,具体详情如下所示:
boolean register() throws Throwable {
logger.info(PREFIX + appPathIdentifier + ": registering service...");
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
继续阅读,我们发现:register()方法被InstanceInfoReplicator 类的run()方法调用,其中InstanceInfoReplicator实现了Runnable接口,run()方法具体源码如下所示:
public void run() {
try {
discoveryClient.refreshInstanceInfo();
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 {
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
沿着线索一路追踪,我们发现:在InstanceInfoReplicator类初始化过程中,有一个initScheduledTasks()方法,该方法开启了获取服务注册列表的信息,如果需要向Eureka Server注册,则开启,同时开启定时向Eureka Server服务续约的定时任务,具体源码参考如下所示:
private void initScheduledTasks() {
...//省略了任务调度获取注册列表的代码
if (clientConfig.shouldRegisterWithEureka()) {
...
// Heartbeat timer
scheduler.schedule(
new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
),
renewalIntervalInSecs, TimeUnit.SECONDS);
// InstanceInfo replicator
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
public String getId() {
return "statusChangeListener";
}
public void notify(StatusChangeEvent statusChangeEvent) {
instanceInfoReplicator.onDemandUpdate();
}
};
...
}
基于相同的思路,我们可以在Eureka server端的源码,进行相关调用的探索,,有兴趣的可以自行去官网下载源码进行查阅。
2、Eviction 服务剔除
在默认的情况下,当Eureka客户端连续90秒没有向Eureka服务器发送服务续约,即心跳,Eureka服务器会将该服务实例从服务注册列表删除,即服务剔除。针对服务下线,目前的常用场景主要涉及以下:
直接Kill -9 服务
默认情况下,如果Eureka Server在90秒没有收到Eureka客户的续约,它会将实例从其注册表中删除。但这种做法的不好之处在于, 客户端已经停止了运行,但仍然在注册中心的列表中。 虽然通过一定的负载均衡策略或使用熔断器可以让服务正常进行,但不是一种很好的管控机制。
执行Delete请求
通过向eureka 注册中心发送Delete请求进行信号通知,以使得服务端能够及时知晓其操作含义。此种方式简单粗暴,立马见效,具体操作命令行为:
http://ip:port/eureka/apps/{application.name}/{instanst-id}
例如,我们发起一个delete请求:具体可参考如下:
luga@OutOfMemory~% http://192.168.0.51:7001/eureka/apps/GASS-AUTH/localhost:gass-auth:8531
备注: Eureka客户端每隔一段时间(默认30秒)会发送一次心跳到注册中心续约。如果通过这种方式下线了一个服务,而没有及时停掉的话,该服务很快又会回到服务列表中。
客户端主动告知
如果我们的Eureka客户端是是一个Spring Boot应用,可以通过调用以下代码通知注册中心下线。可以实现优雅关机放一个钩子,具体代码可参考如下:
public class HelloController {
private DiscoveryClient client;
"/hello", method = RequestMethod.GET) (value =
public String index() {
java.util.List<ServiceInstance> instances = client.getInstances("hello-service");
return "Hello World";
}
"/offline", method = RequestMethod.GET) (value =
public void offLine(){
DiscoveryManager.getInstance().shutdownComponent();
}
}
其他几个活动暂不在这里进行源码跟踪解读,因为和服务注册和剔除类似,大家可以自己看下源码,深入理解。总之,借助源码,我们可以发现:整个源码基本上都是围绕架构进行。
最后,回到前面,关于注册中心的选型问题,其本质在于生态组件的相关特性以及其所应用的场景。具体描述如下:
ZooKeeper 的设计原则是基于 CP,即强一致性和分区容错性。其能够保证数据的强一
致性,但舍弃了可用性,如果出现网络问题可能会影响 ZooKeeper的选举,导致 ZooKeeper注册中心的不可用。
Eureka 的设计原则是基于AP,即可用性和分区容错性。其能够保证注册中心的可用性
,但舍弃了数据一致性,各节点上的数据有可能是不一致的(会最终一致)。
- EOF -