Eureka笔记-----Eureka关键源码解读
1. 配置文件配置Eureka服务端和客户端
#删除原有的properties,新建application.yml
#Eureka服务治理中心
#spring.application.name配置Spring微服务名称,一个微服务可有多个实例,即具体实例拥有同一个微服务名称
#spring.profiles.active用于让yml文件起效,希望同时启动一个微服务的两个实例时,先勾选Edit Configuration的Allow parallel run,分别运行即可
spring:
application:
name: eureka_server
profiles:
active: peer1
#eureka.client.service-url.defaultZone用于配置服务治理中心的注册地址,用于注册微服务实例,逗号隔开多个地址表示多个治理中心,用于治理中心相互注册
#微服务启动时,并不会马上向Eureka治理中心发送REST请求,而是默认延迟40秒才发起请求
eureka:
client:
service-url:
#Zone和Regin:用于服务就近调用,Region表示大区域(如China),Zone表示小区域(如beijing)需要大型分布式站点时,可以这样配置:各个实例间只会调用同一个Zone的微服务实例
#eureka:
#client:
#region:China
#availability-zones:beijing
defaultZone: http://localhost:5001/eureka/,http://localhost:5002/eureka/
#治理中心只有一个时,就取消注册,即自己就是注册中心,自己不用注册
#register-with-eureka: false
#服务获取,即通过REST请求从Eureka服务中获取其他Eureka客户端实例信息
fetch-registry: false
#检索其他服务实例清单的时间间隔,默认30
registry-fetch-interval-seconds: 30
#eureka.instance.hostname表示服务治理中心服务器IP
instance:
hostname: localhost
#(续约)Eureka服务端并不能保证实例一直可用,具体的微服务需要按一定频率对Eureka服务器维持心跳,让服务器知道自己还可用
#(续约)微服务实例超时失效秒数,默认90,倘若续约超时,Eureka会将为服务实力剔除
lease-expiration-duration-in-seconds: 90
#(续约)间隔对应的秒数执行一次续约服务,默认30
lease-renewal-interval-in-seconds: 30
#服务治理中心通过注册、续约和下线3种服务,Eureka可有效管理具体的微服务实例(Eureka Server,上面都是客户端模板)
#服务治理中心之间相互注册可以说也是Eureka客户端
#相互复制:治理中心相互注册保证高可用和高性能,当微服务注册、下线、续约时,Eureka服务治理中心会将这些信息转发到其他的服务治理中心实例上完成同步,服务治理中心实例之间是对等模式,是等价的
#服务剔除:Eureka启动时,会创建一个定时任务,默认每60秒会更新一次微服务实例清单,当发现90秒没有完成续约的实例,就会剔除出清单
#自我保护:(红色英文警告)Eureka运行期间,若15分钟内低于85%情况心跳测试失败,则会警告,若希望停止
#eureka:
#server:
#enable-self-preservation:false
#微服务间相互调用(客户端)上面又讲到
#服务获取:微服务实例作为客户端,具有从服务治理中心(REST请求)获取其他为服务实例清单(只读)的功能,还会降清单缓存到本地,并且按一定时间间隔(默认30)刷新
#服务调用:具体Ribbon或OpenFeign实现(核心就是负载均衡算法)~
服务端或客户端配置都可以通过上面配置搞定
2. 服务发现(注册)
在Eureka机制中,主要是客户端主动维护和Eureka服务治理中心的关系,`旧的SpringCloud版本中,还需要添加注解@EnableDiscoveryClient进行服务注册,我们就从这个注解出发解读Eureka源码
package org.springframework.cloud.client.discovery;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
//是否启动自动注册功能,默认为true,即启用自动注册功能
boolean autoRegister() default true;
//作用是标注Eureka客户端作为服务发现的实例,对应的服务接口为DiscoveryClient
}
DiscoveryClient拥有两个重要的属性,分别是EurekaInstanceConfig和EurekaClient,第一个属性为配置类,第二个为客户端类。而SpringCloud使用EurekaDiscoveryClient对该类进行了封装,而研究的核心是EurekaClient,拥有一个实现类DiscoveryClient(这个不是之前的接口)。
那么DiscoveryClient是怎么通过配置项Eureka.client.serviceUrl.defaultZone进行配置的呢,这就是DiscoveryClient的getServiceUrlsFromConfig方法,但是这个方法不用了,取而代之的是EndpointUtils的getServiceUrlsConfig方法
/*描述一下整个过程:
获取Region,若没有配置或找不到对应的Region,则使用默认值,一个微服务只能找到一个Region
通过获取的Region获取可用的Zone数组,一Region可对应多个Zone,若获取Zone失败,则使用默认值
在可用数组中查找当前配置的Zone实例,若找到则返回第一个匹配的下标,若没有找到则返回0表示默认值
将与Zone匹配的已经配置好的可用的serviceUrls加入到orderedUrls中
*/
public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
List<String> orderedUrls = new ArrayList();
//从配置寻找Region
String region = getRegion(clientConfig);
//根据Region寻找可用的Zone
String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
if (availZones == null || availZones.length == 0) {
//若是Zone为空则使用默认值
availZones = new String[]{"default"};
}
logger.debug("The availability zone for the given region {} are {}", region, availZones);
//根据配置的Zone来匹配获取的可用Zone数组,有则返回对应下标,无则返回0
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
//根据客户端配置且已匹配了的Zone来查找Eureka服务端已经存在的defaultZone
List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
//循环变量,这样是为了循环错开循环量myZoneOffset
int currentOffset = myZoneOffset == availZones.length - 1 ? 0 : myZoneOffset + 1;
//循环,因为可能客户端配置了多个Zone,同样的步骤
while(currentOffset != myZoneOffset) {
serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
if (currentOffset == availZones.length - 1) {
currentOffset = 0;
} else {
++currentOffset;
}
}
if (orderedUrls.size() < 1) {
throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
} else {
//返回最中可用的同Zone的服务端serviceUrls,即获取对应服务端的eureka.client.serviceUrls.defaultZone
return orderedUrls;
}
}
该方法中有一参数接口EurekaClientConfig,它对Eureka客户端进行配置。配置文件中的“eureka.client”为前缀就是配置它的属性,上述用到了EurekaClientConfigBean的getEurekaSererServiceUrls方法来获取服务端serviceUrl:
public List<String> 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)) {
//多个注册的serviceURL,使用逗号分割为数组
String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
List<String> 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.trim());
}
return eurekaServiceUrls;
} else {
return new ArrayList();
}
}
回到类DiscoveryClient,其构造方法中,会调用一个私有的initScheduledTasks方法,它是一个初始化任务计划方法,分为两个服务,一个是服务获取,另一个是关于服务注册和续约的逻辑
//服务注册与续约
//是否启用注册功能。配置项:eureka.client.register-with-eureka,默认为true
if (this.clientConfig.shouldRegisterWithEureka()) {
//续约时间间隔,默认30秒。配置项:eureka.instance.lease-renewal-interval-in-seconds
renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
//倘若续约超时,Eureka会将为服务实力剔除。续约超时后,尝试最大次数,默认10
//配置项:eureka.client.heartbeat-executor-exponential-back-off-bound
expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: renew interval is: {}", renewalIntervalInSecs);
this.heartbeatTask = new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread());
//心跳服务维持续约
this.scheduler.schedule(this.heartbeatTask, (long)renewalIntervalInSecs, TimeUnit.SECONDS);
//注册线程,其中instanceInfo为注册时间间隔,默认40秒
//配置项:eureka.client.instance-info-replication-interval-seconds
this.instanceInfoReplicator = new InstanceInfoReplicator(this, this.instanceInfo, this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
//客户端本身状态监听,若发生变化,则守护线程会做相应维护。配置项:eureka.client.on-demand-update-status-change,默认true
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(//
//注册延迟时间。配置项:eureka.client.initial-instance-info-replication-interval-seconds,默认40秒
this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
可以看到注册也有时间间隔,这是因为Eureka服务器可能会因为某些原因不可用而重新启动,这时时间间隔注册功能就可以保证Eureka客户端能够自我恢复注册到重新启动到Eureka服务注册中心。
因为注册是通过线程实现的,那么核心就是run方法,该方法中最重要的是调用了DiscoveryClient类的register方法,向Eureka服务中心注册
boolean register() throws Throwable {
logger.info("DiscoveryClient_{}: registering service...", this.appPathIdentifier);
EurekaHttpResponse httpResponse;
try {
//使用REST请求进行服务注册
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());
}
//监听返回值,看返回值是否为状态204
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
再来研究register方法的具体实现(RestTemplateEurekaHttpClient的register方法):
//参数InstanceInfo为配置类
public EurekaHttpResponse<Void> register(InstanceInfo info) {
//通过serviceUrl来构建URL
String urlPath = this.serviceUrl + "apps/" + info.getAppName();
//请求头
HttpHeaders headers = new HttpHeaders();
headers.add("Accept-Encoding", "gzip");
headers.add("Content-Type", "application/json");
//使用REST风格的POST请求进行请求注册
ResponseEntity<Void> response = this.restTemplate.exchange(urlPath, HttpMethod.POST, new HttpEntity(info, headers), Void.class, new Object[0]);
//包装请求结果
return EurekaHttpResponse.anEurekaHttpResponse(response.getStatusCodeValue()).headers(headersOf(response)).build();
}
3. 服务获取(获取其它Eureka客户端清单)
回到类DiscoveryClient,其构造方法中,会调用一个私有的initScheduledTasks方法,它是一个初始化任务计划方法,分为两个服务,一个是服务获取,另一个是关于服务注册和续约的逻辑
//服务获取
int renewalIntervalInSecs;
int expBackOffBound;
//是否允许服务获取,由配置项eureka.client.fetch-registry控制,默认为true
if (this.clientConfig.shouldFetchRegistry()) {
//获取服务获取注册信息的刷新时间间隔(检索其他服务实例清单的时间间隔),默认30秒
//配置项:eureka.client.registry-fetch-interval-seconds
renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();
//获取超时最大尝试数,默认为10次
//配置项:eureka.client.cache-refresh-executor-exponential-back-off-bound
expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
this.cacheRefreshTask = new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread());
//启动线程按一定时间间隔执行服务获取
this.scheduler.schedule(this.cacheRefreshTask, (long)renewalIntervalInSecs, TimeUnit.SECONDS);
}
服务获取是Eureka客户端(服务实例)的功能,它会通过REST请求从Eureka服务治理中心获取其他Eureka客户端的信息,形成服务清单,缓存到本地。执行服务调用时,就从服务实例清单中获取可用的实例进行调用,有时服务调用就是通过获取实例清单来实现负载均衡的。
4. Eureka使用注意点
Eureka是强调AP(可用性和容任性)的组件。
可用性:Eureka的机制是通过各种REST风格请求来监控各个微服务甚至其他Eureka服务器是否可用,在一些情况下服务治理中心会剔除他们,所以即使某个微服务只存在一个实例,该微服务仍是可用的。
两延迟:启动后默认等上40秒后,才会发送REST风格请求到Eureka服务治理中心进行注册;服务发现客户端实例有自己的缓存清单,默认30秒维护刷新一次,即即使新的微服务注册到了Eureka,该缓存清单也可能不包含这个新的微服务实例,只有当缓存清单刷新后才能发现新注册的微服务。
5. Eureka配置
以前缀“eureka.client”开头,用于配置EurekaClientConfigBean配置类
5.1 服务治理中心管理配置
配置项 | 说明 |
---|---|
instance-fetch-interval-seconds | Eureka服务治理中心更新服务实例信息(30s) |
eureka-service-url-poll-interval-seconds | 轮询Eureka服务地址更新的时间间隔(300s) |
eureka-server-read-timeout-seconds | 读取Eureka服务器信息的超时时间(8s) |
eureka-server-total-connections | Eureka客户端连接Eureka服务治理中心的连接总数(200) |
eureka-server-total-connections-per-host | Eureka客户端到单个Eureka服务器的连接总数(50) |
eureka-server-idle-timeout-seconds | Eureka客户端连接Eureka服务器的超时关闭时间(30s) |
5.2 服务发现(注册)配置
配置项 | 说明 |
---|---|
enable | 是否启用Eureka客户端(true) |
initial-instance-info-replication-interval-seconds | 实例启动后到向服务治理中心发送注册请求时间间隔(40s) |
use-dns-for-fetching-service-urls | 是否使用DNS地址来获取serviceUrl(false) |
register-with-eureka | 是否将实例注册到Eureka服务器(true) |
perfer-same-zone-eureka | 是否偏爱使用相同Zonede Eureka服务器(true) |
5.3 服务获取配置
配置项 | 说明 |
---|---|
fetch-registry | 是否启用服务获取 |
registry-info-replication-interval-seconds | 从Eureka服务器获取注册服务实例清单的时间间隔(30s) |
cache-refresh-executor-thread-pool-size | 缓存刷新连接池线程数(2) |
cache-refresh-executor-exponential-back-off-bound | 缓存刷新重试延迟时间的最大乘数值(10) |
filter-only-up-instances | 获取实例信息时是否过滤服务,只保留UP状态的为服务实例(true) |
5.4 服务续约配置
配置项 | 说明 |
---|---|
heartbeat-executor-thread-pool-size | 心跳任务连接池线程数(2) |
heartbeat-executor-exponential-back-off-bound | 心跳超时重试延迟时间的最大乘数值(10) |
5.5 客户端服务实例配置
以前缀“eureka.instance”开头,用于配置EurekaInstanceConfigBean配置类。SpringCloud通过EurekaInstanceConfigBean读入的信息创建InstanceInfo实例,然后将InstanceInfo实例通过REST请求register方法发给Eureka服务器。
微服务实例命名规则:
若配置了spring.application.instance_id,则名称为{spring.cloud.hostname}:{spring.application.name}:{spring.application.instance_id}.
若没有配置,则名称为
{spring.cloud.hostname}:{spring.application.name}:{server.port}.
自定义服务实例元数据(如配置版本号):
配置项为eureka.instance.metadata-map,是一个MAP结构,允许自定义启动实例的元数据,例如标记版本号:
eureka.instance.metedata-map: version:v2
自定义的元数据会发送到Eureka服务端,其他的微服务也可以读取这个配置,这样就可以知道部署了什么版本的服务了