Eureka详解

在前面通过一个简单的demo演示了服务注册与发现,构建了Eureka服务治理体系中的三个核心角色:服务注册中心、服务提供者、服务消费者。通过对前一篇的学习我们已经对Eureka的服务治理机制有了一些初步的认识,并学会了如何搭建单节点和高可用的服务注册中心,也知道了如何使用Eureka的注解和配置将SpringBoot应用纳入Eureka的服务治理体系中,成为服务提供者或是消费者。同时对于客户端负载均衡的服务消费也有了一些简单的接触,但是在实际工作中遇到的系统结构往往比前面列举的demo要复杂的多,此时如果仅仅依靠之前构建的服务治理内容,大多数情况是无法直接满足业务系统要求的,还需要结合实际情况来做一些配置、调整和扩展。因此本篇来学一些较深的知识,如Eureka的基础架构、节点间的通信机制、以及一些进阶的配置等。

基础架构

在前面介绍服务治理时,举了一个例子,尽管该例子非常简单,但是却包含了整个Eureka服务治理基础架构的三个核心要素:服务注册中心、服务提供者、服务消费者。

服务注册中心,它是Eureka提供的服务端,提供服务注册与发现的功能,如前面实现的eureka-server。

服务提供者,顾名思义用来提供服务,它可以是SpringBoot应用,也可以是其他技术平台且遵循Eureka通信机制的应用。它将自己提供的服务注册到Eureka中,以供其他应用发现,如前面实现的HELLO-SERVICE。

服务消费者,顾名思义用来消费服务,它从服务注册中心获取服务列表,从而使消费者可以知道去哪里调用其所需要的服务。在前面使用的是Ribbon来消费服务,后续还会介绍使用Feign来消费服务。

笔者通过实践发现在很多时候,服务提供者也是服务消费者,这一点需要引起注意。

服务治理机制

在前面我们通过使用注解和配置就实现了强大的服务治理功能,接下来进一步学习Eureka基础架构中各个元素的一些通信行为,只有理解了它们才能理解基于Eureka实现的服务治理体系是如何运作起来的。

在上图中有6个元素,其中“服务注册中心-1”和“服务注册中心-2”相互注册形成了高可用集群。“服务提供者”启动了两个实例,一个注册到“服务注册中心-1”上,另一个注册到“服务注册中心-2”上。还有两个“服务消费者”,它们也都分别指向了一个注册中心。

接下来就基于上面的结构来详细介绍,从服务注册开始到服务调用,及各个元素所涉及的一些重要的通信行为。

服务提供者

服务提供者,顾名思义用来提供服务,它可以是SpringBoot应用,也可以是其他技术平台且遵循Eureka通信机制的应用。换句话来说,服务提供者可以是用Java写的,也可以是用Python写的,还可以是用Golang写的,对于语言没有任何限制。服务提供者将自己提供的服务注册到Eureka中,以供其他应用发现然后调用。服务提供者主要有如下功能:服务注册、服务同步和服务续约等。

服务注册

服务提供者在启动的时候会通过发送REST请求将自己注册到Eureka Server上,同时还携带了自身服务的一些元数据信息。Eureka Server在接收到这个REST请求之后,将元数据信息存储在一个双层结构的Map集合中,第一层的key是服务名,第二层的key是具体服务的实例名:

在进行服务注册的时候,需要确认一下eureka.client.register-with-eureka=true这一参数是否设置正确,该值默认为true。请注意如果设置为false将不会启动注册操作。

服务同步

回到这张架构图,这里的两个服务提供者分别注册到了两个不同的服务注册中心上,也就是说它们的信息分别被两个服务注册中心所维护。此时由于服务注册中心之间进行了互相注册服务,使得当服务提供者发送注册请求到一个服务注册中心时,它会将该请求转发给集群中相连的其他注册中心,进而实现注册中心之间的服务同步。也就是说通过服务同步,两个服务提供者的服务信息可以通过这两个服务注册中心的任意一个得到。

为了加深对此服务同步的理解,接下来通过实例来进行说明。(1)在前面我们搭建了两个节点的eureka-server服务注册中心peer1和peer2,两个服务注册中心是相互注册,那么现在就通过命令来启动它们:

1
2
start /min java -jar target/eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
start /min java -jar target/eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2

需要说明的是peer1和peer2使用的端口号分别是1111和1112。(2)接着修改hello-service服务的配置文件为如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 服务同步
spring:
application:
name: hello-service
---
# p1向服务注册中心peer1注册
eureka:
client:
service-url:
defaultZone: http://peer1:1111/eureka/
spring:
profiles: p1
server:
port: 8081
---
# p2向服务注册中心peer2注册
eureka:
client:
service-url:
defaultZone: http://peer2:1112/eureka/
spring:
profiles: p2
server:
port: 8082

然后将其打包成jar包,并使用如下命令来启动这两个hello-service服务:

1
2
start /min java -jar target/eureka-client-0.0.1-SNAPSHOT.jar --spring.profiles.active=p1
start /min java -jar target/eureka-client-0.0.1-SNAPSHOT.jar --spring.profiles.active=p2

同样需要说明的是这两个实例使用的端口号分别是8081和8082,且实例p1填写的是peer1这个服务注册中心的地址,实例p2填写的是peer2这个服务注册中心的地址。

(3)当两个服务提供者都启动成功之后,接下来查看两个服务注册中心的控制面板,如下所示:

可以看到尽管实例p1填写的是peer1这个服务注册中心的地址,实例p2填写的是peer2这个服务注册中心的地址,但是由于服务注册中心互相注册的缘故,导致你在peer1注册中心可以看到p2的信息,在peer2中可以看到p1的信息。

(4)既然都到了这个份上,那么就可以通过服务消费者来验证一下。修改ribbon-consumer服务的application.yml配置文件信息如下:

1
2
3
4
5
6
7
8
9
spring:
application:
name: ribbon-consumer
server:
port: 9000
eureka:
client:
service-url:
defaultZone: http://peer1:1111/eureka/

仔细看,这里只是填写了peer1这个服务注册中心的地址,然后启动这个服务消费者,在浏览器地址栏中访问http://peer1:9000/ribbon-consumer,仔细观察p1和p2控制台的输出信息,可以看到每访问一下,控制台就会输出两次如下信息:

1
/hello,host: localhost,service_id: HELLO-SERVICE

需要注意的是由于Ribbon客户端负载均衡的存在,使得每次只会在一个hello-service服务的控制台下两次输出上述信息。通过上面这个例子就验证了服务同步的正确性。

服务续约

在注册完服务之后,服务提供者会维护一个心跳用来告诉服务注册中心Eureka Server:“我还活着”,以防止Eureka Server的“剔除任务”将该服务实例从服务列表中排除出去,我们称该操作为服务续约(Renew)。和服务续约有两个重要的属性,开发者可以关注并根据需要来进行调整:

1
2
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=90

其中第一个参数用于定义服务续约任务的调用间隔时间,默认为30秒;第二个参数用于定义服务失效的时间,默认为90秒。

说完了服务提供者的主要功能,接下来聊一聊服务消费者的功能。

服务消费者

服务消费者,顾名思义用来消费服务,它从服务注册中心获取服务列表,从而使消费者可以知道去哪里调用其所需要的服务。在前面使用的是Ribbon来消费服务,后续还会介绍使用Feign来消费服务。服务消费者主要有如下功能:获取服务、服务调用和服务下线等。

获取服务

在前面我们已经在服务注册中心注册了一个服务hello-service,并且该服务有两个实例p1和p2。当我们启动服务消费者的时候,它会发送一个REST请求给服务注册中心,用于获取该服务注册中心上面注册的服务清单。出于性能考虑,服务注册中心Eureka Server会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30秒更新一次。

获取服务是服务消费者的基础,所以必须确保服务注册中心Eureka Server上的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,关于Region和Zone的内容,笔者将会在后续源码学习中进行介绍。

服务下线

在系统运行过程中必然会面临关闭或者重启服务的某个实例的情况,在服务关闭期间,我们不希望客户端继续调用已经关闭的实例,所以在客户端程序中,当服务实例进行正常的关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务注册中心收到请求后,会将该服务状态置为DOWN,表示服务已下线,并将该事件传播出去,这样就可以避免服务消费者调用一个已经下线的服务提供者。

学习了服务提供者和服务消费者的功能后,接下来开始学习最基础也是最重要的服务注册中心。

服务注册中心

服务注册中心,它是Eureka提供的服务端,提供服务注册与发现的功能,如前面实现的eureka-server。当然除此之外也提供了失效剔除和自我保护等功能。

失效剔除

在前面介绍服务消费者的时候介绍了服务下线这个问题,正常的服务下线发生流程有一个前提,那就是服务正常关闭,但是在实际运行中服务有可能不会正常关闭,比如系统故障、网络故障等原因导致服务提供者非正常下线,而此时服务注册中心并未收到“服务下线”的请求。那么此时对于已经下线的服务,Eureka采用了定时清除机制。Eureka Server在启动的时候会创建一个定时任务,每隔一段时间(默认为60秒)就去将当前服务提供者列表中超时(默认为90秒)还没续约的服务剔除出去,通过这种方式来避免服务消费者调用了一个无效的服务。

自我保护

由于在前面笔者将服务注册中心Eureka Server的自我保护模式给关闭了,因此接下来需要打开,在application.yml配置文件中新增如下配置:

1
2
3
eureka:
server:
enable-self-preservation: true

当然了这个值默认就是true,可以不用设置,代表默认开启Eureka Server的自我保护机制。

启动一下项目,我们来看看这个自我保护会有什么提示信息,可以看到其实就是一句红色警告信息:

在前面讲过服务注册到Eureka Server之后,会维护一个心跳连接,用于告诉Eureka Server自己还活着。Eureka Server在运行期间会统计心跳失败的比例在15分钟内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,而在实际生产环境上通常是由于网络不稳定造成的),Eureka Server会将当前的实例注册信息保护起来,让这些实例不会过期,尽可能保护这些注册信息。但是在这段保护期内实例若出现问题,那么客户端就很容易拿到实际已经不存在的服务实例,会出现调用失败的情况,所以客户端必须要有容错机制,如使用请求重试、断路器等机制。

前面也说了,由于在本地单机调试很容易触发服务注册中心的保护机制,这会使得注册中心维护的服务实例不那么准确。所以通常在进行本地开发的时候,可以将服务注册中心的自我保护模式进行关闭,以确保服务注册中心可以将不可用的实例正确给剔除掉。