关于SpringCloud的简介在这里就不详细说明。我们都知道SpringCloud有以下常见的几个组件:
- 服务发现——Netflix Eureka
- 客服端负载均衡——Netflix Ribbon
- 断路器——Netflix Hystrix
- 服务网关——Netflix Zuul
- 分布式配置——Spring Cloud Config
今天我们来学习一下Eureka组件,也就是服务注册中心。
1.Eureka介绍
Spring Cloud Eureka是Spring Cloud Netflix项目下的服务治理模块。Eureka可以划分为Eureka Client和Eureka Service,也可以理解为服务提供者和消费者模型。它可以将服务器配置和部署为高可用,每个服务器将注册服务的状态复制到其他服务器。
2.Eureka基本原理
上图是来自eureka的官方架构图,这是基于集群配置的eureka。可以看出在这个体系中,有2个角色,即Eureka Server和Eureka Client。而Eureka Client又分为Applicaton Service和Application Client,即服务提供者何服务消费者。服务注册到Eureka后,每个eureka server会把服务的注册信息同步到其他的eureka server。这样,当一个注册中心出现问题,并不会影响整体的服务。
要想更加深入的理解整体的机制,需要理解几个概念:
服务注册
当Eureka客户端向Eureka Server注册时,它提供自身的服务数据,比如IP地址、端口,应用名称等。服务续约
Eureka客户端每隔(默认)30秒发送一次心跳来续约。 通过续约来告知Eureka Server该Eureka客户端仍然正常运行。 正常情况下,如果Eureka Server在90秒没有收到Eureka客户端的续约,它会将实例从其注册表中删除。服务剔除
在默认的情况下,当Eureka客户端连续90秒没有向Eureka服务器发送服务续约,即心跳,Eureka服务器会将该服务实例从服务注册列表删除,即服务剔除。服务下线
Eureka客户端在程序关闭时向Eureka服务器发送取消请求。我们可以理解为Eureka客户端主动的断绝了和服务端的联系。当发送请求后,该客户端实例信息将从服务器的实例注册表中删除。获取注册服务
Eureka客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。此处的缓存机制则为如果发现缓存的注册服务列表和每次定期更新的列表数据不一致时,Eureka客户端则会重新获取整个注册表信息。-
自我保护
Eureka Server在默认90s没有得到客户端的心跳,则注销该实例。但是也可能在网络分区故障时,Eureka Server注销服务实例则会让大部分微服务不可用,这很危险,因为服务的确是在正常运行。为了解决这个问题,Eureka 有自我保护机制,通过在Eureka Server配置如下参数,可启动保护机制
eureka.server.enable-self-preservation=true
它的原理是,当Eureka Server节点在短时间内丢失过多的客户端时(可能发送了网络故障),那么这个节点将进入自我保护模式,不再注销任何微服务,当网络故障回复后,该节点会自动退出自我保护模式。
3.Eureka配置
1.服务端配置
通过Maven建立父子模块项目,其中父Pom文件如下:
com.springcloud
demo
pom
1.0-SNAPSHOT
eureka1
SpringCloud
这是一个SpringCloud的项目
org.springframework.boot
spring-boot-starter-parent
2.0.2.RELEASE
Finchley.RELEASE
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
import
pom
子Pom文件如下:
demo
com.springcloud
1.0-SNAPSHOT
4.0.0
eureka1
1.0-SNAPSHOT
jar
eureka1
这是一个简单的注册中心
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
org.springframework.boot
spring-boot-maven-plugin
文件的路径如下:
关于application.yml的配置:
spring:
application:
name: eureka1
server:
port: 8001
eureka:
instance:
hostname: localhost
client:
fetch-registry: false
register-with-eureka: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
表示该服务在注册中心列表的名称为eureka1
spring:
application:
name: eureka1
指定了该注册中心的端口号
server:
port: 8001
指定了该注册中心的域名
eureka:
instance:
hostname: localhost
客户端是否获取eureka服务器注册表上的注册信息,默认为true
eureka:
client:
fetch-registry: false
在默认配置下,服务注册中心既是服务端,也是客户端。所以这里也会将自己作为客户端来尝试注册它自己,需要禁用它的客户端注册行为。
eureka:
client:
register-with-eureka: false
指定该注册中心的URL信息,defaultZone可以指定多个地址,以实现高可用。
eureka:
client:
service-url:
defaultZone: http:// ${eureka.instance.hostname}:${server.port}/eureka/
注册中心的引导文件
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args){
SpringApplication.run(EurekaApplication.class,args);
}
}
这里需要记住的是@EnableEurekaServer注解。该注解声明这是一个注册中心服务端。
启动项目访问http:// localhost:8001/eureka/就可以看到服务注册中心的信息了。
2.客户端配置
除了作为服务端,还可以配置为客户端。在上一节父pom的基础上,新建一个子模块。子模块的pom如下:
demo
com.springcloud
1.0-SNAPSHOT
4.0.0
eureka_client
eureka_client
这是一个客户端
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-maven-plugin
它的路径如下即可:
我们先来看一下EurekaClientController ,这里只是一个测试作用,务须过多留意。
@RestController
public class EurekaClientController {
@RequestMapping("/eureka/client")
public String clientTest(){
return "This is a EurekaClient!";
}
}
客户端的引导类和服务端引导类只是有一个注解的区别:
@SpringBootApplication
@EnableEurekaClient
public class EurekaClientApplication {
public static void main(String[] args){
SpringApplication.run(EurekaClientApplication.class,args);
}
}
@EnableEurekaClient将此类注册成客户端。
关于它的application.yml如下配置:
spring:
application:
name: eureka_client
server:
port: 9001
eureka:
client:
service-url:
defaultZone: http://localhost:8001/eureka/
指定该服务的名称,后续的服务调用将根据此名称查找。
spring:
application:
name: eureka_client
指定该服务的端口
server:
port: 9001
指定该服务将要注册到哪个注册中心,这里指向上一节配置的环境。
eureka:
client:
service-url:
defaultZone: http://localhost:8001/eureka/
启动引导类EurekaClientApplication,访问 http://localhost:8001就会看到该服务已作为客户端注册到服务中心。
可以看到注册列表里已经有了名为eureka_client的服务了。
4.Eureka源码
我就随便点一点,看看Eureka的源码。从网上找了两个图片:
我也不知道为什么从DiscoveryClient类切入来看源码,咱也不敢问。就这么来吧。
@Singleton
public class DiscoveryClient implements EurekaClient {
private static final Logger logger = LoggerFactory.getLogger(DiscoveryClient.class);
public static final String HTTP_X_DISCOVERY_ALLOW_REDIRECT = "X-Discovery-AllowRedirect";
private static final String VALUE_DELIMITER = ",";
private static final String COMMA_STRING = ",";
...
可以沿着类集成的路线观察,从LookupService到DiscoveryClient类,慢慢丰富了服务注册的方法。先来看一下DiscoveryClient类。
-
服务注册方法 register
可以看出来,注册的时候会把instanceInfo(应该就是服务提供者的信息吧)作为参数,返回的是一个http的响应。
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;
}
- 续约定时任务
看起来逻辑是先判断是否开启续约,如果开启了,就配置续约定时任务的相关属性。
private void initScheduledTasks() {
int renewalIntervalInSecs;
int expBackOffBound;
if (this.clientConfig.shouldFetchRegistry()) {
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()), (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");
}
}
- 缓存注册列表数据
获取到所有服务的注册信息后会将其缓存到本地
private void getAndStoreFullRegistry() throws Throwable {
long currentUpdateGeneration = this.fetchRegistryGeneration.get();
logger.info("Getting all instance registry info from the eureka server");
Applications apps = null;
EurekaHttpResponse httpResponse = this.clientConfig.getRegistryRefreshSingleVipAddress() == null ? this.eurekaTransport.queryClient.getApplications((String[])this.remoteRegionsRef.get()) : this.eurekaTransport.queryClient.getVip(this.clientConfig.getRegistryRefreshSingleVipAddress(), (String[])this.remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
apps = (Applications)httpResponse.getEntity();
}
logger.info("The response status is {}", httpResponse.getStatusCode());
if (apps == null) {
logger.error("The application is null for some reason. Not storing this information");
} else if (this.fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1L)) {
this.localRegionApps.set(this.filterAndShuffle(apps));
logger.debug("Got full registry with apps hashcode {}", apps.getAppsHashCode());
} else {
logger.warn("Not updating applications as another thread is updating it already");
}
}
- 更新注册列表
这个方法执行作为更新注册列表的数据。
private void getAndUpdateDelta(Applications applications) throws Throwable {
long currentUpdateGeneration = this.fetchRegistryGeneration.get();
Applications delta = null;
EurekaHttpResponse httpResponse = this.eurekaTransport.queryClient.getDelta((String[])this.remoteRegionsRef.get());
if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
delta = (Applications)httpResponse.getEntity();
}
if (delta == null) {
logger.warn("The server does not allow the delta revision to be applied because it is not safe. Hence got the full registry.");
this.getAndStoreFullRegistry();
} else if (this.fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1L)) {
logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
String reconcileHashCode = "";
if (this.fetchRegistryUpdateLock.tryLock()) {
try {
this.updateDelta(delta);
reconcileHashCode = this.getReconcileHashCode(applications);
} finally {
this.fetchRegistryUpdateLock.unlock();
}
} else {
logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
}
if (!reconcileHashCode.equals(delta.getAppsHashCode()) || this.clientConfig.shouldLogDeltaDiff()) {
this.reconcileAndLogDifference(delta, reconcileHashCode);
}
} else {
logger.warn("Not updating application delta as another thread is updating it already");
logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode());
}
- 获取注册中心
该方法在EndpointUtils类下,可以猜想,从application.yml的eureka.client.service-url.defaultZone配置信息读取。有以下两个方法:
String region = getRegion(clientConfig);
从一个应用的配置读取一个Region,也就是说一个Region对应一个微服务应用。
String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
大致的意思是,从一个Region中可以读取多个可用的Zone。也就是说,Region和Zone是一对多的关系。
public static Map> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
Map> orderedUrls = new LinkedHashMap();
String region = getRegion(clientConfig);
String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
if (availZones == null || availZones.length == 0) {
availZones = new String[]{"default"};
}
logger.debug("The availability zone for the given region {} are {}", region, availZones);
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
String zone = availZones[myZoneOffset];
List serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
if (serviceUrls != null) {
orderedUrls.put(zone, serviceUrls);
}
int currentOffset = myZoneOffset == availZones.length - 1 ? 0 : myZoneOffset + 1;
while(currentOffset != myZoneOffset) {
zone = availZones[currentOffset];
serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
if (serviceUrls != null) {
orderedUrls.put(zone, serviceUrls);
}
if (currentOffset == availZones.length - 1) {
currentOffset = 0;
} else {
++currentOffset;
}
}
if (orderedUrls.size() < 1) {
throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
} else {
return orderedUrls;
}
}
- 读取ServerUrls
可以看出,这里允许我们配置eureka.client.serviceUrl.defaultZone属性可以配置多个,并且需要通过逗号分隔。
public List getEurekaServerServiceUrls(String myZone) {
String serviceUrls = (String)this.serviceUrl.get(myZone);
if (serviceUrls == null || serviceUrls.isEmpty()) {
serviceUrls = (String)this.serviceUrl.get("defaultZone");
}
if (!StringUtils.isEmpty(serviceUrls)) {
String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
List eurekaServiceUrls = new ArrayList(serviceUrlsSplit.length);
String[] var5 = serviceUrlsSplit;
int var6 = serviceUrlsSplit.length;
for(int var7 = 0; var7 < var6; ++var7) {
String eurekaServiceUrl = var5[var7];
if (!this.endsWithSlash(eurekaServiceUrl)) {
eurekaServiceUrl = eurekaServiceUrl + "/";
}
eurekaServiceUrls.add(eurekaServiceUrl);
}
return eurekaServiceUrls;
} else {
return new ArrayList();
}
}