目录
eureka详解
前面两节,我们使用了服务注册中心、服务消费者、服务提供者,构建了eureka服务治理体系。因此对eureka也有了一定的了解,并且在使用服务消费者时接触了客户端的负载均衡ribbon的简单的接触。但是其实在实际项目中,这些东西的使用是很复杂的,所以我们还需要对eureka更透彻的理解,才能做出一些更符合项目实情的配置。下面我们将详细去理解eureka的基础架构、节点间的通信机制已经一些进阶的配置。
基础架构:
在前面的一章"服务治理"一节中,所使用的势力虽然小,但是包含了三个核心要素。
- 服务注册中心:eureka提供的服务端,提供服务注册与发现的能力。
- 服务提供者:提供接口服务的应用,他将自己提供的服务注册到服务注册中心,已供其他应用发现。
- 服务消费者:消费者从服务注册中心去获取服务列表,从而消费者知道去何处调用自己所需的服务,在上一节中我们就使用了ribbon来实现服务消费,以后还会介绍使用feign的消费方式。
服务治理机制:
在体验了eureka通过简单的注解配置就是实现了强大的服务治理功能,我们来了解下eureka基础架构中各个元素的一些通信行为,以此来理解eureka实现的服务治理体系是如何运作的。几个重要元素先理解一下:
- “服务注册中心-1” 和 “服务注册中心-2” ,两者之间互相注册,相差了高可用集群的注册中心
- “服务提供者例”启动两个实例,这两个实例分别只注册到一个服务注册中心。
- 启动两个“服务消费者”实例,也一样分别只注册到一个注册中心
根据上面的结构,我们可以来仔细了解一下各个节点之间的一些重要通信行为。
服务提供者:
-
服务注册
“服务提供者”在启动的时候会通过发送rest请求的方式将自己注册到eureka server服务注册中心,同时带上了自身的一些元数据。注册中心接收到这个rest请求之后,将元数据信息存储在一个双层结构Map中,其中第一层的key是服务名,第二层的key是具体服务的实例名。
结构:(“key(服务名)”:(“key具体服务的实例名”:“元数据”))
spring.application.name=服务名
eureka.instance.hostname=服务实例名
在注册时,需要确认一下配置 eureka.client.register-with-eureka=true参数是否为true。若设置为false则不会启动注册操作。sss
-
服务同步
如上面的架构图中所示,两个服务注册中心之间互相注册。而两个服务提供者分别只注册一个注册中心。也就是说这两个服务提供者的信息分别被服务服务注册中心所维护。因为注册中心是相互注册为服务,当服务提供者注册请求到一个服务注册中心时,它会将请求转发给集群中相连的其他注册中心,从而实现注册中心之间的服务同步。通过服务同步,服务提供者的服务信息就能被任何一台注册中心调用到。
-
服务续约-心跳机制
在注册完服务之后,服务提供者会维护一个心跳来持续告诉eureka server注册中心,表明自己还活着,以防注册中心的“剔除任务”将自己从服务列表中剔除出去。我们称该机制为服务续约。
关于服务续约有两个重要的属性:
eureka.instance.lease-renewal-interval-in-seconds=30;该参数用于定义服务续约任务的调用时间间隔,默认为30秒。
eureka.instance.lease-expiration-duration-in-seconds=90;该参数用于定义服务失效的时间,默认为90秒。
服务消费者:
-
获取服务
目前了解为止,我们已经理解了服务注册中心和服务提供者。而当我们启动服务消费者时,它会发送一个rest请求给服务注册中心,来获取注册中心上面的服务列表清单。为了性能考虑,eureka server注册中心会维护一份只读的服务清单来返回给客户端,同事缓存清单每隔30秒更新一次。
获取服务是服务消费者的基础,所以必须确保eureka.client.fetch-registry=true参数没有被修改成false,该值默认为true。若希望修改缓存清单的更新时间,可以通过eureka.client.registry-fetch-interval-seconds=30参数进行修改,该参数默认为30,单位为秒。
-
服务调用
服务调用者在获取服务清单后,通过服务名可以获取具体提供的实例名和该实例的元数据信息。因为有这些服务实例的详细信息,所以客户端可以根据自己的需求决定具体调用哪个实例,在ribbon中会默认采用轮询的方式进行调用,从而实现客户端的负载均衡。
对于访问实例的选择,eureka中有region和zone的概念,一个region中可以包含多个zone,每个服务客户端需要被注册到一个zone中,所以每个客户端对应一个region和一个zone。在进行服务调用的时候,优先访问同处一个zone中的服务提供方,若访问不到,就访问其他的zone。
-
服务下线
在系统运行过程中必然会面临关闭或重启服务的某个实例的情况,在服务关闭期间,我们自然不希望客户端会继续调用到被关闭的实例。所以在客户端程序中,当服务实例进行正常的关闭操作时,它会触发一个服务下线的rest请求给eureka server注册中心,告诉服务注册中心:“我要下线了”。服务端在接收到请求之后,将该服务状态置为下线(down),并把该下线事件传播出去。然后注册中心之间服务同步就会都知道该服务客户端已下线。
服务注册中心:
-
失效剔除机制
有些时候我们的服务实例并不一定是正常下线,有可能是内存溢出、网络故障等问题使得服务不能正常工作,而服务注册中心没有收到“服务下线”的请求。为了从服务列表中将这些无法提供服务的实例剔除掉,eureka server注册中心在启动的时候会创建一个定时任务,默认每隔一段时间(60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除出去。
-
自我保护
当我们在本地调试eureka时,基本上会碰到这样一i个情况,在服务注册中心的信息面板中出现红色警告:
实际上,该警告就是出发了自我保护机制。之前说过,服务注册到eureka server注册中心之后,会维护一个心跳机制,来告诉eureka server自己还活着。eureka server 注册中心在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况,eureka server会将当前的实例注册信息保护起来,让这些实例不会过期,经可能的保护这些信息。但是,在这段保护期间若实例出现问题,那么客户端很容易拿到实际已经不存在的服务实例,会出现调试失败的情况,所有客户端必须要有容错机制。比如可以使用请求重试、断路器等机制。
由于本地调试很容易就出发了注册中心的保护机制,这会使得注册中心维护的服务实例不那么准确。所以我们在本地开发的时候,可以使用
eureka. server.enable-self-oreservation=false;关闭保护机制,确保将不可用的实例剔除。
源码简单理解:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
boolean autoRegister() default true;
}
public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
List<String> orderedUrls = new ArrayList();
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);
List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
if (serviceUrls != null) {
orderedUrls.addAll(serviceUrls);
}
int currentOffset = myZoneOffset == availZones.length - 1 ? 0 : myZoneOffset + 1;
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 {
return orderedUrls;
}
}
public static String getRegion(EurekaClientConfig clientConfig) {
String region = clientConfig.getRegion();
if (region == null) {
region = "default";
}
region = region.trim().toLowerCase();
return region;
}
public String[] getAvailabilityZones(String region) {
String value = (String)this.availabilityZones.get(region);
if (value == null) {
value = "defaultZone";
}
return value.split(",");
}
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)) {
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);
}
return eurekaServiceUrls;
} else {
return new ArrayList();
}
}