一、Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。
二、Spring为什么选择Eureka。在Spring Cloud中可选的注册中心其实包含:Consul、Zookeeper和Eureka,为什么选择Eureka。
1)完全开源:经过Netflix公司的生存环境的考验,以及这么年时间的不断迭代,在功能和性能上都非常稳定,可以放心使用。
2)无缝对接:Eureka是Spring Cloud的首选推荐的服务注册与发现组件,能够达到无缝对接。
3)相互配合:Eureka 和其他组件,比如负载均衡组件 Ribbon、熔断器组件Hystrix、熔断器监控组件Hystrix Dashboard 组件、熔断器聚合监控Turbine 组件,以及网关 Zuul 组件相 配合,能够很容易实现服务注册、负载均衡、熔断和智能路由等功能。
三、Eureka基本架构:
1)Register Service :服务注册中心,它是一个 Eureka Server ,提供服务注册和发现的功能。
2)Provider Service :服务提供者,它是 Eureka Client ,提供服务
3)Consumer Service :服务消费者,它是 Eureka Cient ,消费服务
四、编写Eureka Server
1)加入spring cloud基础依赖,官方配置如下
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
知识兔官方提供的版本选择:
我这里用的是2.0.X的版本,所以直接使用的是Finchley的版本,具体版本号查看官方
2)加入server依赖(有些地方配置成spring-cloud-starter-eureka-server)但是官方建议配置成
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
知识兔3)编写启动项加入@EnableEurekaServer注解
package com.cetc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
知识兔4)编写配置文件application.yaml
server:
port: 8670
eureka:
instance:
appname: server
client:
register-with-eureka: false # 关闭本身注册
fetch-registry: false # 是否从server获取注册信息
service-url:
defaultZone:
http://127.0.0.1:8670/eureka/ # 实际开发中建议使用域名的方式
spring:
application:
name: server
知识兔5)启动项目浏览器查看http://127.0.0.1:8670/
五、编写Eureka client
1)加入依赖,cloud基础配置和server一样
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
知识兔2)编写启动类加入@EnableEurekaClient注解
package com.cetc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
知识兔3)配置application.yaml
server:
port: 8673
eureka:
instance:
appname: client
client:
service-url:
defaultZone:
http://127.0.0.1:8670/eureka/ # 实际开发中建议使用域名的方式
spring:
application:
name: client
知识兔4)启用项目浏览器输入http://127.0.0.1:8670/
六、源码解析
1)Eureka的一些概念
Registe 一一服务注册:Eureka Client向Eureka Server 注册时,Eureka Client 提供自身的元数据,比如 IP 地址、端口、运行状况H1标的 Uri 主页地址等信息。
Renew一一服务续约:Eureka client 在默认的情况下会每隔 30 秒发送一次心跳来进行服务续约。通过服务续约来告知 Eureka Server。Eureka Client 仍然可用,没有出现故障。正常情况下,如果 Eureka Server90 秒内没有收到 Eureka Client 的心跳, Eureka Server 会将 Eureka Client 实例从注册列表中删除。注意:’官网建议不要更改服务续约的间隔时间。
Fetch Registries一一获取服务注册列表信息:Eureka Client从Eureka Server 获取服务注册表信息,井将其缓存在本地。 Eureka Client使用服务注册列表信息查找其他服务的信息,从而进行远程调用。该注册列表信息定时(每30 秒) 更新1次,每次返回注册列表信息可能与 Eureka Client 的缓存信息不同, Eureka Client会自己处理这些信息。如过由于某种原因导致注册列表信息不能及时匹配, Eureka Client 会重新获取整个注册表信息。Eureka Server 缓存了所有的服务注册列表信息,并将整个注册列表以及每个应用程序信息进行了压缩,压缩内容和没有压缩的内容完全相同。 Eureka Client和Eureka Server 可以使用 JSON/XML 数据格式进行通信。在默认的情况下, Eureka Client使用JSON 格式的方式来获取服务注册列表的信息。
Cancel——服务下线:Eureka Client 在程序关闭时可以向 Eureka Server 发送下线请求。发送请求后,该客户端的实例信息将从 Eureka Server 的服务注册列表中删除。
DiscoveryManager.getinstance().shutdownComponent();
知识兔Eviction一一服务剔除:在默认情况下,当 Eureka Client连续90秒没有向 Eureka Server 发送服务续约(即心跳〉时, Eureka Server 会将该服务实例从服务注册列表删除,即服务剔除。
2)Eureka的高可用架构
在这个架构图中有两个角色 ,即 Eureka Server和Eureka Client。而EurekaClient 又分为 Applicaton Service和Application Client 即服务提供者和服务消费者。每个区域有一个Eureka 集群, 并且每个区域至少有一个Eureka Server 以处理区域故障 以防服务器瘫痪。
Eureka Client向Eureka Server注册时, 将自己客户端信息提交给 Eureka Server 然后,Eureka Client 通过向 Eureka Server 发送心跳 (每 30 次)来续约服务。 如果某个客户端不能持续续约,那 Eureka Server 定该客户端不可用 该不可用的客户端将在大约 90 秒后从Eureka Server 服务注册列表中删除 ,服务注册列表信息和服务续约信息会被复 到集群中的每Eureka Server 节点。来自任何区域 Eureka Client 都可 获取整个系统的服务注册列表信息。根据这些注册列表信息, Application Client 远程调用 Applicaton Service 来消费服务。
3)Register服务注册
服务注册,即 Eureka Client向Eureka Server 提交自己服务信息 包括 IP 地址、 端口、Serviceld 等信息。 Eureka Client配置文件中 没有配置 Serviceld ,则默认为配置文件中配置的服务名 ,即$ {spring application.name }的值。
Eureka Client 启动时, 会将自身 的服务信息发送到 Eureka Server 这个过程其实非常简单,现在从源码角度分析服务注册的过程,在Maven 的依赖包下,找到eureka-client-1.6.2.jar 包。在 com.netflix.discovery 包下有 DiscoveryClient 类,该类包含了Eureka Client和Eureka Server注册的相关方法。其中, DiscoveryClient 实现了 EurekaClient并且它是单例模式,而 EurekaClient 继承了 LookupServic 接口。
在DiscoveryClient 类中有个服务注册的方法register(), 该方法 通过Http 请求向Eureka Server注册。
boolean register() throws Throwable {
logger.info("DiscoveryClient_{}: registering service...", this.appPathIdentifier);
EurekaHttpResponse httpResponse;
try {
httpResponse = this.eurekaTransport.registrationClient.register(this.instanceInfo);
} catch (Exception var3) {
logger.warn("DiscoveryClient_{} - registration failed {}", new Object[]{this.appPathIdentifier, var3.getMessage(), var3});
throw var3;
}
if (logger.isInfoEnabled()) {
logger.info("DiscoveryClient_{} - registration status: {}", this.appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
知识兔通过查询register()调用情况可以知道,在InstanceInfoReplicator被调用,并且InstanceInfoReplicator实现了Runnable,可以看出执行在run()方法中
public void run() {
boolean var6 = false;
ScheduledFuture next;
label53: {
try {
var6 = true;
this.discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = this.instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
this.discoveryClient.register();
this.instanceInfo.unsetIsDirty(dirtyTimestamp.longValue());
var6 = false;
} else {
var6 = false;
}
break label53;
} catch (Throwable var7) {
logger.warn("There was a problem with the instance info replicator", var7);
var6 = false;
} finally {
if (var6) {
ScheduledFuture next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
this.scheduledPeriodicRef.set(next);
}
}
next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
this.scheduledPeriodicRef.set(next);
return;
}
next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
this.scheduledPeriodicRef.set(next);
}
知识兔上面是具体的执行类,那具体的调用类在哪里呢?通过在DiscoveryClient搜索可以得知在initScheduledTasks()方法,initScheduledTasks()的调用就是在构造函数中实现的
private void initScheduledTasks() {
int renewalIntervalInSecs;
int expBackOffBound;
if (this.clientConfig.shouldFetchRegistry()) {
//获取默认时间配置,默认30秒
renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();
expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
//定时任务
this.scheduler.schedule(new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
}
if (this.clientConfig.shouldRegisterWithEureka()) {
renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: renew interval is: {}", renewalIntervalInSecs);
//定时任务
this.scheduler.schedule(new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread(null)), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
this.instanceInfoReplicator = new InstanceInfoReplicator(this, this.instanceInfo, this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
this.statusChangeListener = new StatusChangeListener() {
public String getId() {
return "statusChangeListener";
}
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN != statusChangeEvent.getStatus() && InstanceStatus.DOWN != statusChangeEvent.getPreviousStatus()) {
DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
} else {
DiscoveryClient.logger.warn("Saw local status change event {}", statusChangeEvent);
}
DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate();
}
};
if (this.clientConfig.shouldOnDemandUpdateStatusChange()) {
this.applicationInfoManager.registerStatusChangeListener(this.statusChangeListener);
}
this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
知识兔然后跳出来,跟中Eureka Server发现EurekaBootStrap,我们可以得知EurekaBootStrap继承具有初始化的权限,跟踪得知
ServletContextListener:存在两个方法:contextInitialized和contextDestroyed,意思就是容器初始化执行和容器销毁时执行。
知识兔protected void initEurekaServerContext() throws Exception { EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig(); JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), 10000); XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(), 10000); logger.info("Initializing the eureka client..."); logger.info(eurekaServerConfig.getJsonCodecName()); ServerCodecs serverCodecs = new DefaultServerCodecs(eurekaServerConfig); ApplicationInfoManager applicationInfoManager = null; Object registry; if (this.eurekaClient == null) { registry = this.isCloud(ConfigurationManager.getDeploymentContext()) ? new CloudInstanceConfig() : new MyDataCenterInstanceConfig(); applicationInfoManager = new ApplicationInfoManager((EurekaInstanceConfig)registry, (new EurekaConfigBasedInstanceInfoProvider((EurekaInstanceConfig)registry)).get()); EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig(); this.eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig); } else { applicationInfoManager = this.eurekaClient.getApplicationInfoManager(); } if (this.isAws(applicationInfoManager.getInfo())) { registry = new AwsInstanceRegistry(eurekaServerConfig, this.eurekaClient.getEurekaClientConfig(), serverCodecs, this.eurekaClient); this.awsBinder = new AwsBinderDelegate(eurekaServerConfig, this.eurekaClient.getEurekaClientConfig(), (PeerAwareInstanceRegistry)registry, applicationInfoManager); this.awsBinder.start(); } else { registry = new PeerAwareInstanceRegistryImpl(eurekaServerConfig, this.eurekaClient.getEurekaClientConfig(), serverCodecs, this.eurekaClient); } PeerEurekaNodes peerEurekaNodes = this.getPeerEurekaNodes((PeerAwareInstanceRegistry)registry, eurekaServerConfig, this.eurekaClient.getEurekaClientConfig(), serverCodecs, applicationInfoManager); this.serverContext = new DefaultEurekaServerContext(eurekaServerConfig, serverCodecs, (PeerAwareInstanceRegistry)registry, peerEurekaNodes, applicationInfoManager); EurekaServerContextHolder.initialize(this.serverContext); this.serverContext.initialize(); logger.info("Initialized server context"); int registryCount = ((PeerAwareInstanceRegistry)registry).syncUp(); ((PeerAwareInstanceRegistry)registry).openForTraffic(applicationInfoManager, registryCount); EurekaMonitors.registerAllStats(); }