Eureka
Spring Cloud Eureka 是 Spring Cloud Netflix 微服务套件中的一部分, 它基于 Netflix Eureka 做了二次封装,主要负责完成微服务架构中的服务治理功能
服务治理
服务治理可以说是微服务架构中最为核心和基础的模块, 它主要用来实现各个微服务实例的自动化注册与发现。
需要的原因:静态配置难以维护
服务注册:
在服务治理框架中,通常都会构建 一个注册中心, 每个服务单元向注册中心登记自己提供的服务, 将主机与端口号、 版本号、 通信协议等 一些附加信息告知注册中心, 注册中心按服务名分类组织服务清单。
比如, 我们有两个提供服务A的进程分别运行于 192.168.0.100:8000和192.168.0.101:8000位置上,
另 外 还 有三个提供服务B的进程分别运行千192.168.0.100:9000 、192.168.0.101:9000、 192.168.0.102:9000位置上。 当这些进程均启动,并向注册中心注册自己的服务之后, 注册中心就会维护类一个如下类似的服务清单。
另外, 服务注册中心还需要以心跳的方式去监测清单中的服务是否可用, 若不可用需要从服务清单中剔除, 达到排除故障服务的效果
服务发现:
服务间的调用不再通过指定具体的实例地址来实现, 而是通过向服务名发起请求调用实现。 因此, 调用方需要向服务注册中心咨询服务, 并获取所有服务的实例清单, 以实现对具体服务实例的访问。 比如,现有服务C希望调用服务A, 服务C就需要向注册中心发起咨询服务请求, 服务注册中心就会将服务A的位置清单返回给服务C, 如按上例服务A的情况,C便获得了服务A的两个可用位置 192.168.0.100:8000和192.168.0.101:8000。当服务C要发起调用的时候, 便从该清单中以某种轮询策略取出一个位置来进行服务调用, 这就是后续我们将会介绍的客户端负载均衡。
实际的框架为了性能等因素, 不会采用每次都向服务注册中心获取服务的方式, 并且不同的应用场景在缓存和服务剔除等机制上也会有一些不同的实现策略。
Netflix Eureka:
SpringCloud Eureka, 使用Netflix Eureka来实现服务注册与发现, 它既包含了服务端组件,也包含了客户端组件,并且服务端与客户端均采用Java编写。
Eureka服务端: 我们也称为服务注册中心。 它同其他服务注册中心一样,支持高可用配置。 它依托于强一致性提供良好的服务实例可用性, 可以应对多种不同的故障场景。当集群分片出现故障时,Eureka就转入自我保护模式,允许故障期间继续提供服务,当恢复运行时,集群中的其他分片会把他们的状态状态再次同步过来
Eureka客户端:主要处理服务的注册与发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中, 在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务租约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态
搭建注册中心
也就是服务端,添加依赖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
代码:
通过注解@EnableEurekaServer开启注册中心的能力
1 | @EnableEurekaServer |
EnableEurekaServer:通过 @EnableEurekaServer 注解启动一个服务注册中心提供给其他应用进行注册。
在默认设置下, 该服务注册中心也会将自己作为客户端来尝试注册它自己, 所以我们需要禁用它的客户端注册行为, 配置:
1 | server.port=l111 |
通过http://localhost:1111/就能访问注册中心 查询注册信息
@EnableEurekaServer 的实现
可以看到EnableEurekaServer本身也有EnableDiscoveryClient注解,自注册的能力就是来自于这里
1 |
|
@EnableDiscoveryClient的注解如下:
1 | (ElementType.TYPE) |
注册服务提供者
模块名:hello-service
引入pom:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.didispace</groupId>
<artifactId>hello-service-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Controller:
DiscoveryClient注入,能够获取到服务端的相关信息
1 |
|
主类:1
2
3
4
5
6
7
8
9
10
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
EnableDiscoveryClient:激活 Eureka 中的DiscoveryClient 实现(自动化配置, 创建 DiscoveryClient 接口针对 Eureka 客户端的 EurekaDiscoveryClient 实例
配置:
1 | # 服务的名称 |
启动后就能在server端看到注册信息,以及请求信息
高可用注册中心
Eureka Server的设计一开始就考虑了高可用问题, 在Eureka的服务治理设计中, 所有节点即是服务提供方, 也是服务消费方, 服务注册中心也不例外。
Eureka高可用实际上就是将自己作为服务向其他服务注册中心注册自己, 这样就可以形成一组互相注册的服务注册中心, 以实现服务清单的互相同步, 达到高可用的效果。因此需要开启上面register-with-eureka置为false的配置
下面是一个双节点注册中心集群的案例:
创建两个配置文件,一个application-peer1.properties:
1 | spring.application.name=eureka-server |
另一个application-peer2.properties:1
2
3
4
5spring.application.name=eureka-server2
server.port=1112
eureka.instance.hostname=peer2
# 指向了1
eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/
本地测试,需要配置peer1和peer2的host,也就是修改hosts文件:1
2127.0.0.1 peerl
127.0.0.1 peer2
然后通过spring.profiles.active属性来分别启动peerl和peer2(也可以新建两个模块来执行)
1 | java -jar eureka-server-1.0.0.jar --spring.profiles.active=peerl |
启动后访问他们的注册中心,会发现registered-replicas有另个一注册中心。
服务提供方修改
在Controller服务中需要修改配置才能将服务注册到集群中:
1 | eureka.client.serviceUrl.defaultZone=http://peerl:llll/eureka/,http://peer2:lll2/eureka/ |
如我们不想使用主机名来定义注册中心的地址,也可以使用IP地址的形式, 但是需要在 配置文件中增加配置参数eureka.instance.prefer-iip-address= true, 该值默认为false。
启动服务端后,注册中心的会新增服务
在启动一个hello-service
服务发现和消费
我们已经有了服务注册中心和服务提供者,下面就来尝试构建一个服务消费者, 它主要完成两个目标:
发现服务以及消费服务
其中,服务发现的任务由eEureka的客户端完成,而服务消费的任务由Ribbonb完成 。
Ribbon是一个基于HTTP和TCP的客户端负载均衡器。它可以在通过客户端中配置的ribbonServerList服务端列表去轮询访问以达到均衡负载的作用。
当Ribbon与Eureka联合使用时,Ribbon的服务实例清单RibbonServerList会被DiscoveryEnabledNIWSServerList重写, 扩展成从Eureka注册中心中获取服务端列表(不是手动指定一个list)。
同时它也会用 NIWSDiscoveryPing来取代IPing, 它将职责委托给Eureka 来确定服务端是否已经启动
下面是一个简单的示例:
通过 java -jar命令行的方式来启动两个不同端口的hello-service, 具体如下(同样也可以新建两个不同的模块运行):
1 | java -jar hello-service-0.0.1-SNAPSHOT.jar --server.port=8081 |
新建一个consumer服务,加入ribbon服务1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-hystrix-amqp</artifactId>
</dependency>
</dependencies>
主类配置:
配置负载均衡的RestTemplate
1 |
|
可以添加@LoadBalanced:开启客户端负载均衡
消费类ConsumerController:
1 |
|
启动消费类,这时候注册中心打印:
1 | 2020-03-12 22:33:57.522 INFO 28328 --- [nio-1112-exec-1] c.n.e.registry.AbstractInstanceRegistry : Registered instance RIBBON-CONSUMER/guanhangdembp:ribbon-consumer:9000 with status UP (replication=true) |
访问:http://localhost:9000/ribbon-consumer
服务1打印:
1 | 2020-03-12 22:35:19.098 INFO 28462 --- [nio-8082-exec-1] com.didispace.web.HelloController : /hello, host:guanhangdembp, service_id:hello-service |
再次访问,服务2出现打印,说明是通过ribbon进行了负载均衡。注册中心情况:
Eureka详解
Eureka服务治理体系的三个核心角色:
服务注册中心:Eureka 提供的服务端, 提供服务注册与发现的功能, 也就是在上一节中我们实现的eureka-server
服务提供者:提供服务的应用, 可以是 Spring Boot 应用, 也可以是其他技术平台且遵循 Eureka 通信机制的应用
服务消费者:消费者应用从服务注册中心获取服务列表, 从而使消费者可以知道去何处调用其所需要的服务。可以使用Ribbon和Feign来实现服务消费
服务治理机制
服务治理体系的几个重要的元素:
服务注册中心构成了高可用集群
两个服务提供者分别注册到连个不同的服务中心上
下面是从服务注册到服务调用,各个元素所涉及的一些重要通信行为
服务提供者:
服务注册:“服务提供者 ” 在启动的时候会通过发送REST请求的方式将自己注册到EurekaServer上。同时带上了自身服务的一些元数据信息。Eureka Server接收到这个REST请求之后,将元数据信息存储在一个双层结构Map中, 其中第一层的key是服务名, 第二层的key是具体服务的实例名
服务同步:
两个服务提供者分别注册到了两个不同的服务注册中心上。由于服务注册中心之间因互相注册为服务, 当服务提供者发送注册请求到一个服务注册中心时, 它会将该请求转发。给集群中相连的其他注册中心, 从而实现注册中心之间的服务同步。两个服务提供者的服务信息就可以通过这两台服务注册中心中的任意一台获取到
服务续约:
在注册完服务之后,服务提供者会维护一个心跳用来持续告诉EurekaSe1-ver: “我还活着 ”, 以防止Eureka Server 的 “ 剔除任务 ” 将该服务实例从服务列表中排除出去, 我们称该操作为服务续约(Renew)。
服务续约有两个重要的属性:
1 | eureka.instance.lease-renewal-interval-in-seconds=30 # 用于定义服务续约任务的调用间隔时间,默认为30秒 |
服务消费者
获取服务:
当我们启动服务消费者的时候, 它会发送一个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 Server的自我保护机制。EurekaServer在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现,Eureka Server会将当前的实例注册信息保护起来, 让这些实例不会过期, 尽可能保护这些注册信息。 但是, 在这段保护期间内实例若出现问题, 那么客户端很容易拿到实际已经不存在的服务实例, 会出现调用失败的清况, 所以客户端必须要有容错机制, 比如可以使用请求重试、 断路器等机制。可以通过eureka.server.enable-self-preservation=false来关闭自我保护
源码分析
我们将服务注册到Eureka服务器,主要做了两件事情:
添加@EnableDiscoveryClient注解
配置eureka.client.serviceUrl.defaultZone指定服务注册中心的位置
EnableDiscoveryClient的源码如下:
1 | ({ElementType.TYPE}) |
从该注解的注释中我们可以知道,它主要用来开启DiscoveryClient 的实例。通过搜索DiscoveryClient , 我们可以发现有一个类和一个接口。 通过梳理可以得到如下图所示的关系
左边的DiscoveryClient是Spring Cloud接口,它定义了用来发现服务的常用抽象方法, 通过该接口可以有效地屏蔽服务治理的实现细节。所以使用Spring Cloud构建的微服务应用可以方便地切换不同服务治理框架。
这里用的版本是1.3 和书中不一致
1 | public interface DiscoveryClient { |
EurekaDiscoveryClient是对该接口的实现。该实现又依赖com.netflix.discovery.EurekaClient
主要看到上面有两个DiscoveryClient,一个是接口,一个类,分别来自于spring cloud和netflix
Spring cloud主要提供接口,服务注册和查询的实现来自于netflix包
com.netflix.discovery.EurekaClient实现类是com.netflix.discovery.DiscoveryClient,服务发现主要靠这个类,它提供的能力:
-向Eureka Server注册服务实例
-向Eureka Server服务租约
-当服务关闭期间, 向Eureka Server取消租约
-查询Eureka Server中的服务实例列表
1 |
|
defaultZone属性的获取
通过追踪defaultZone配置,发现该配置项在com.netflix.discovery.endpoint.EndpointUtils中使用
1 | eureka: |
EndpointUtils代码:
Zone名称的配置:可以通过 eureka.client.availability-zones
1 | public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { |
Region,Zone
可以看到,上面的代码依次加载了两个内容,第一个是Region,第二个是Zone
我们可以看到它从配置中读取了一个Region返回, 所以一个微服务应用只可以属于一个Region, 如果不特别配置, 默认为default。 若我们要自己设置,可以通过eureka.client.region属性来定义。
Region的默认Zone是defaultZone,也是参数eureka.client.serviceUrl.defaultZone的由来。Region和Zone是一对多的关系
要为应用指定Zone, 可以通过eureka.client.availability-zones属性来进行设置
获取Region和Zone后,接下来是获取具体地址:
1 | int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); |
getEurekaServerServiceUrls的具体实现org.springframework.cloud.netflix.eureka.EurekaClientConfigBean:
EurekaClientConfigBean是配置文件对应的配置类,它实现了EurekaClientConfig接口,关联前缀是eureka.client的配置
1 | public List<String> getEurekaServerServiceUrls(String myZone) { |
当我们在微服务应用中使用Ribbon来实现服务调用时,对于 Zone 的设置可以在负载均衡时实现区域亲和特性: Ribbon 的默认策略会优先访问同客户端处于一个Zone中的服务端实例,只有当同一个Zone中没有可用服务端实例的时候才会访问其他 Zone 中的实例。所以通过 Zone 属性的定义,配合实际部署的物理结构,我们就可以有效地设计出对区域性故障的容错集群。
服务注册、续约、心跳的实现
前面提到过EurekaDiscoveryClient集成了EurekaClient,我们看下其实现类com.netflix.discovery.DiscoveryClient的实现,在构造函数中有一个initScheduledTasks方法:
主要关注几个if,分别创建几个定时任务
1 | private void initScheduledTasks() { |
TimedSupervisorTask是服务获取定时任务,可以传Runnalble来实现具体的操作,并统计相关的信息
1 | public void run() { |
心跳的定时任务是通过HeartbeatThread来实现的:
下面代码可以看到这就是服务端和注册中心进行心跳通信维持租约,维持心跳的方法是renew
1 | private class HeartbeatThread implements Runnable { |
我们再回头看下shouldRegisterWithEureka判断里面的内容,启动上面的心跳任务后,又创建了一个InstanceInfoReplicator,它也是个定时任务。其run方法的实现:
可以看到这里进行了服务注册,注册操作也是通过REST请求的方式进行的。同时, 我们能看到发起注册请 求的时候, 传入了 一 个com.netflix.appinfo.Instanceinfo对象, 该对象就是注册时客户端给服务端的服务的元数据
1 | public void run() { |
服务获取和服务续约
服务获取的逻辑在shouldFetchRegistry判断内部,它也是一个定时任务,同样使用TimedSupervisorTask,用来定时刷新客户端的服务清单,具体的实现类是CacheRefreshThread。服务续约就是上面的HeartbeatThread,它和服务注册在一个逻辑里面。服务获取的逻辑shouldFetchRegistry的判断逻辑实际就是根据参数:
eureka.client.fetch-registry
来判断,默认为true
服务注册中心处理
上面我么可以看到不管是注册、续约都是通过REST请求来完成的
服务注册中心的请求接受端在com.netflix.eureka.resources.ApplicationResource实现:
最终的register是通过org.springframework.cloud.netflix.eureka.server.InstanceRegistry#register来实现的
该实现先发布注册时间,然后通过父类com.netflix.eureka.registry.AbstractInstanceRegistry#register的注册方法:
1 | public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) { |
该类有一个registry变量,其定义如下:
1 | private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap(); |
是一个双层的Map,第一层Map的key是应用名称,第二层Map的key是注册服务信息InstanceInfo的instanceId
配置详解
在Eureka的服务治理体系中, 主要分为服务端与客户端两个不同的角色, 服务端为服务注册中心, 而客户端为各个提供接口的微服务应用。当我们构建了高可用的注册中心之后, 该集群中所有的微服务应用和后续将要介绍的 一 些基础类应用(如配置中心、 API网关等)都可以视作该体系下的一个微服务(Eureka客户端)
服务注册中心也 一样, 只是高可用环境下的服务注册中心除了作为客户端之外, 还为集群中的其他客户端提供了服务注册的特殊功能。 所以,Eureka客户端的配置对象存在于所有Eureka服务治理体系下的应用实例中
Eureka客户端的配置主要分为以下两个方面:
• 服务注册相关的配置信息, 包括服务注册中心的地址、 服务获取的间隔时间、 可用区域等。
• 服务实例相关的配置信息, 包括服务实例的名称、IP地址、 端口号、 健康检查路径等
Eureka服务端是一个现成的产品,配置可以参考EurekaServerConfigBean类,这些参数均以eureka.server 作为前缀
注意的是这些配置Bean都是spring coud包中的,但是都实现了netflix中的配置接口
服务注册配置类
即EurekaClientConfigBean,我们看下相关的配置:
指定注册中心:
1 | public static final String DEFAULT_ZONE = "defaultZone"; |
其他配置一览
服务实例配置
元数据配置:
来源于配置类EurekaInstanceConfigBean,其中大部分是元数据的配置。它是Eureka 客户端在向服务注册 中心发送注册请求时, 用来描述自身服务信息的对象, 其中包含了 一些标准化的元数据, 比如 服务名称、 实例名称、 实例IP、 实例端口等用于服务治理的重要信息;以及一些用千负载均衡策略或是其他特殊用途的自定义元数据信息
EurekaInstanceConfigBean加载了所有配置信息,但是在注册的时候,会包装成InstanctInfo对象发送给Eureka服务端
实例名配置:
即instanceId参数,它是区分同一服务中不同实例的唯一标识,实例名的取名规则可以自定义,这样就能在本地启动多个服务(因为默认是主机名) :
server.port为0也可以随机分配端口号启动
1 | eureka.instance.instanceid={spring.application.name}:{random.int}} |
端点配置:
在 InstanceInfo 中, 我们可以看到一些 URL 的配置信息, 比如 homePageUrl、satusPageUrl、healthCheckUrl 它们分别代表了应用的主页的URL、状态页的URL、健康检查的URL。其中状态页和健康检查的URL在Eureka中默认使用了spring boot actuator模块的/info和/health端点。
为了服务的正常运作, 我们必须确保 Eureka 客户端的/health端点在发送元数据的时候, 是一个能够被注册中心访问到的地址, 否则服务注册中心不会根据应用的健康检查来更改状态(仅当开启了healthcheck 功能时, 以该端点信息作为健康检查标准)
大多数情况下,我们并不需要修改这几个 URL 的配置,但是在一些特殊情况下,比如,为应用设置了 context-path, 这时, 所有 spring-boot-actuator 模块的监控端点都会增加一个前缀:
1 | management.context-path=/hello |
另外, 有时候为了安全考虑, 也有可能会修改 /info 和/health 端点的原始路径。这个时候, 我们也需要做一些特殊的配置:
1 | endpoints.info.path=/appinfo |
需要注意的是当客户端应用以HTTPS的方式来暴露服务和监控端点时,相对路径的配置方式就无法满足需求了,需要修改成:
1 | eureka.instance.statusPageUrl=https://${eureka.instance.hostname}/info |
默认情况下,Eureka依靠客户端心跳的方式来保持服务实例的存活,客户端的健康状态从注册到注册中心开始都会处于 UP状态, 除非心跳终止 一段时间之后, 服务注册中心将其剔除。 默认的心跳实现方式可以有效检查客户端进程是否正常运作, 但却无法保证客户端应用能够正常提供服务(比如数据库连接失败)
我们可以通过简单的配置, 把Eureka客户端的健康检测交给spring-boot-actuator模块的/health端点。实现步骤:
- 引入actuator模块
- 引入eureka.client.healthcheck.enabled=true配置
- 特殊配置参考上面的介绍
其他配置
Ribbon
Spring Cloud Ribbon 是一个基于HTTP和TCP的客户端负载均衡工具,它基于 Netflix Ribbon 实现。 通过 Spring Cloud 的封装, 可以让我们轻松地将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用。它是一个工具类框架,不需要单独的部署,微服务间的调用,API网关的请求转发等内容,实际都是通过Ribbon来实现的。后面所讲的Feign,也是基于Ribbon实现的工具。
客户端负载均衡
我们通常所说的负载均衡都指的是服务端负载均衡, 其中分为硬件负载均衡和软件负载均衡:
硬件:通过在服务器节点之间安装专门用于负载均衡的设备,比如 F5 等
软件:软件负载均衡则是通过在服务器上安装 一些具有均衡负载功能或模块的软件来完成请求分发工作, 比如Nginx 等
在客户端负载均衡中, 所有客户端节点都维护着自己要访问的服务端清单, 而这些服务端的清单来自于服务注册中心,比如上一章我们介绍的Eureka服务端
同服务端负载均衡的架构类似, 在客户端负载均衡中也需要心跳去维护服务端清单的健康性, 只是这个步骤需要与服务注册中心配合完成
通过Spring CloudRibbon的封装, 我们在微服务架构中使用客户端负载均衡调用非常简单, 只需要如下两步:
服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心
服务消费者直接通过调用被 @LoadBalanced 注解修饰过的 RestTemplate 来实现面向服务的接口调用
1 |
|
RestTemplate详解
GET请求
对 GET 请求可以通过如下两个方法进行调用实现:
第 一种: getForEntity 函数
1 |
|
第二种: getForObject 函数
该方法可以理解为对 ge七ForEntity 的进一步封装,它通过 HttpMessageConverterExtractor 对 HTTP 的请求响应体 body内容进行对象转换, 实现请求直接返回包装好的对象内容:
1 | RestTemplate restTemplate = new RestTemplate(); |
POST请求
第一种: postForEntity 函数:
1 | RestTemplate restTemplate = new RestTemplate(); |
第二种: postForObject函数。
1 | RestTemplate restTemplate = new RestTempla七e(); |
第三种: postForLocation函数。 该方法实现了以POST请求提交资源, 并返回新资源的URI, 比如下面的例子
1 | User user = new User("didi", 40); |
PUT请求
1 | RestTempla七e restTemplate = new RestTemplate (); |
DELETE请求
1 | RestTemplate restTemplate = new RestTemplate(); |
源码分析
从LoadBalancerClient说起
疑问:RestTemplate和Ribbon之间什么联系
前面我们看到RestTemplate通过@LoadBalanced来实现客户端负载均衡,该注解使用到了LoadBalancerClient,它是一个接口
1 | public interface LoadBalancerClient extends ServiceInstanceChooser { |
方法解释
choose:根据传入的服务名serviceld,从负载均衡器中挑选一个对应服务的实例
execute:用从负载均衡器中挑选出的服务实例来执行请求内容
reconstructURI:为系统构建 一个合适的host:po江形式的 URI.ServicInstance对象是带有host和port的具体服务实例 , 而后者URI对象则是使用逻辑服务名定义为host的URI , 而返回的URI内容则是通过ServiceInstance的服务实例详情拼接出的具体host:post形式的请求地址
该接口子类的继承关系
LoadBalancerAutoConfiguration增加拦截器
可以看到LoadBalancerAutoConfiguration是负载均衡器的配置类:
1 |
|
LoadBalancerInterceptor干了啥
从上面我们可以得出,restTemplate实现负载均衡是通过拦截器实现的,现在我们看看拦截器LoadBalancerInterceptor干了啥:
1 | public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { |
又回到RibbonLoadBalancerClient
拦截器使用LoadBalancerClient.execute执行执行相关的请求,LoadBalancerClient只是个接口,它的实现类:RibbonLoadBalancerClient,该类的execute就是具体的拦截内容:
1 |
|
ILoadBalancer真正干活的
通过getLoadBalancer获取到类似一个管理服务实例的类,接口是ILoadBalancer,getServer 使用了 ILoadBalancer 接口中定义的 chooseServer 函数。ILoadBalancer看来是一个管理服务器实例的接口,可以看到使用该接口可以添加和选择服务实例
1 | public interface ILoadBalancer { |
ILoadBalancer的实现类:
Spring Cloud是用那个子类呢,在配置类:RibbonClientConfiguration中,通过源码可以看到使用的是ZoneAwareLoadBalancer
1 |
|
总结一下:
ZoneAwareLoadBalancer的在通过 chooseServer函数获取了负载均衡策略分配到的服务实例对象 Server 之后, 将其内容包装成RibbonServer 对象(该对象除了存储了服务实例的信息之外, 还增加了服务名 serviceId、 是否需要使用 HTTPS 等其他信息),然后使用该对象再回调 LoadBalancerinterceptor 请求拦截器中 apply(final ServiceInstance instance) 函数, 向一个实际的具体服务实例发起请求,从而实现一开始以服务名为 host 的URI 请求到 host:post 形式的实际访问地址的转换。
回到RibbonLoadBalancerClient.execute的代码,其中RibbonServer是apply方法参数ServiceInstance子类,该接口中暴露了服务治理系统中每个服务实例需要提供的一些基本信息, 比如 serviceld、 host、 port 等.
在获取到服务实例会回调apply方法:
1 | new LoadBalancerRequest<ClientHttpResponse>() { |
在apply方法中,请求包装成了ServiceRequestWrapper,ServiceRequestWrapper重写了getURI函数
1 |
|
可以看到这里使用了loadBalancer.reconstructURI获取URI,这样又回到了上面所讲RibbonLoadBalancerClient类:
1 | //拿到实例id |
这里简单介绍下springClientFactory和RibbonLoadBalancerContext:
springClientFactory类是一个用来创建客户端负载均衡器的工厂类, 该工厂类会为每一个不同名的Ribbon 客户端生成不同的 Spring 上下文
RibbonLoadBalancerContext是该类用于存储一些负载均衡器使用的上下文内容和API操作
相关的时序图
负载均衡器
虽然Spring Cloud 中定义了LoadBalancerClient作为负载均衡器的通用接口, 并且针对Ribbon实现了RibbonLoadBalancerClient, 但是它在具体实现客户端负载均衡时,是通过Ribbon的ILoadBalancer接口实现的。下面我们分析下该接口
需要注意的是RibbonLoadBalancerClient是spring cloud的类,ILoadBalancer是netflix的接口
AbstractLoadBalancer
代码如下:
1 | public abstract class AbstractLoadBalancer implements ILoadBalancer { |
BaseLoadBalancer
该类为负载均衡器的基础实现类,在该类中定义了很多关于负载均衡器相关的基础内容
- 定义并维护了两个存储服务实例 Server 对象的列表。 一个用于存储所有服务实例的清单, 一个用于存储正常服务的实例清单:
1 | "AllServerList", type = DataSourceType.INFORMATIONAL) (name = PREFIX + |
定义了之前我们提到的用来存储负载均衡器各服务 实例属性和统计信息的LoadBalancerStats 对象
定义了检查服务实例是否正常服务的IPing 对象
定义了检查服务实例操作的执行策略对象IPingStrategy,默认是SerialPingStrategy。该策略采用线性遍历 ping 服务实例的方式实现检查,也可以实现IPingStrategy接口自定义策略
定 义了负载均衡的处理规则IRule对象。理解这个对象我们先看下上面介绍过的chooseServer方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
//调用IRule对象
return rule.choose(key);
} catch (Throwable t) {
return null;
}
}
}可以看到这里实际就是调用IRule对象的choose方法,因此负责均衡选择服务实例,最终落到了IRule对象上,该对象在BaseLoadBalancer的默认值是RoundRobinRule,也就是默认轮训方式
在BaseLoadBalancer的构造函数中启动一个定时任务,检查Server是否健康,默认间隔是10s
1
2
3
4
5
6
7
8
9
10
11
12
13void setupPingTask() {
if (canSkipPing()) {
return;
}
if (lbTimer != null) {
lbTimer.cancel();
}
lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
true);
//使用了Timer类
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
forceQuickPing();
}实现了接口中的相关操作,比如addServers、chooseServer,markServerDown等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40//添加服务
public void addServer(Server newServer) {
if (newServer != null) {
try {
ArrayList<Server> newList = new ArrayList<Server>();
newList.addAll(allServerList);
newList.add(newServer);
setServersList(newList);
} catch (Exception e) {
logger.error("Exception while adding a newServer", e);
}
}
}
//标记某个服务实例暂停服务
public void markServerDown(Server server) {
if (server == null) {
return;
}
if (!server.isAlive()) {
return;
}
logger.error("LoadBalancer: markServerDown called on ["
+ server.getId() + "]");
server.setAlive(false);
// forceQuickPing();
notifyServerStatusChangeListener(singleton(server));
}
//获取可用的服务实例列表
public List<Server> getReachableServers() {
return Collections.unmodifiableList(upServerList);
}
//获取所有的服务
public List<Server> getAllServers() {
return Collections.unmodifiableList(allServerList);
}
DynamicServerListLoadBalancer
该负载均衡器中,实现了服务实例清单在运行期的动态更新能力(需要和Eureka交互);同时,它还具备了对服务实例清单的过滤功能, 也就是说, 我们可以通过过滤器来选择性地获取一批服务实例清单
添加一个实例serverListImpl:
1 | //T是Sever类型 |
ServerList接口的实现类:
那么DynamicServerListLoadBalancer(该类是neitflix的,按理说应该调用netflix的类)使用的是哪个呢:
既然该负载均衡需要实现服务实例的动态更新,那么势必需要Ribbon具备访问Eureka来获取服务实例的能力,所以查看配置类EurekaRibbonClientConfiguration,找到了其实现类DomainExtractingServerList
1 |
|
DomainExtractingServerList又定义了一个ServerList对象,主要方法的实现委托了这个对象来完成,DomainExtractingServerList对它返回的对象进行了包装。该对象是通过构造参数传过去的,也就是上面代码中的discoveryServerList,其类型是DiscoveryEnabledNIWSServerList。
注意的是EurekaRibbonClientConfiguration是spring cloud的类,DiscoveryEnabledNIWSServerList是netflix包的类
说白了spring cloud还是使用的netflix的实现,只在其基础上封装了一些东西。
DiscoveryEnabledNIWSServerList的实现:
1 |
|
内部调用了obtainServersViaDiscovery方法,该方法主要通过EurekaClient获取服务实例的InstanceInfo信息。该方法中的vipAddress,可以理解为服务名,可以逗号隔开。
再委托DiscoveryEnabledNIWSServerList返回DiscoveryEnabledServer后,DomainExtractingServerList对其进行了封装,添加了其他配置信息:
1 |
|
回过头来,通过上面的分析我们已经知道了和ribbon与Eureka 整合后 , 如何实现从 Eureka Server中获取服务实例清单。 那么它又是如何触发向 Eureka Server 去获取服务实例清单以及如何在获取到服务实例清单后更新本地的服务实例清单的呢。
我们继续查看DynamicServerListLoadBalancer类,该类定义了一个对象ServerListUpdater,通过名称可以看到这里就是用来更新服务列表,也称为服务更新器:
1 | //DynamicServerListLoadBalancer |
ServerListUpdater的其他接口:
1 | public interface ServerListUpdater { |
在DynamicServerListLoadBalancer构造函数中,启动了更新机制:
1 | public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, |
ServerListUpdater的实现类:
PollingServerListUpdater:动态服务列表更新的默认策略,DynamicServerListLoadBalancer负载均衡器的默认实现就是它,通过定时任务的方式更新服务列表的更新
EurekaNotificationServerListUpdater:利用Eureka的事件监听器来驱动服务列表的更新操作
正如上面所看到,PollingServerListUpdater的start方法在构造DynamicServerListLoadBalancer时调用:
以定时任务的方式进行服务列表的更新,更新是通过updateAction完成的,根据前面可知,会调用updateListOfServers方法
1 | //PollingServerListUpdater |
ServerListFilter
DynamicServerListLoadBalancer的updateAction调用的是updateListOfServers方法,其中引入了一个filter用来过滤服务器:
1 | if (filter != null) { |
该filter对象是ServerListFilter类型,该接口的继承关系:
除了ZonePreferenceServerListFilter实现外,其他都是netflix包中的类,下面看下这些过滤器都干了什么
AbstractServerListFilter
这是一个抽象过滤器,在这里定义了过滤时需要的一个重要依据对象 LoadBalancerStats, 我们在之前介绍过,该对象存储了关于负载均衡器的一些属性和统计信息等
1 | public abstract class AbstractServerListFilter<T extends Server> implements ServerListFilter<T> { |
ZoneAffinityServerListFilter:
该过滤器基于 “ 区域感知 (Zone Affinity)”的方式实现服务实例的过滤, 也就是说, 它会根据提供服务的实例所处 的区域(Zone) 与消费者自身的所处区域 (Zone) 进行比较, 过滤掉那些不是同处 一个区域的实例:
代码里看到,通过传入一个Predicate来作为过滤判断器,其实现是ZoneAffinityPredicate,具体的判断逻辑如下apply方法:
1 |
|
可以看到过滤服务实例后,并不是立即返回,又调用了shouldEnableZoneAffinity来判断是否满足条件:
getZoneSnapshot获取这些过滤后的同区域实例的基础指标(包含实例数量、断路器断开数、 活动请求数、
实例平均负载等)根据 一系列的算法求出下面的几个评价值并与设置的阙值进行对比(下面的为默认值), 若有一个条件符合, 就不启用 “ 区域感知 ” 过滤的服务实例清单
这一算法实现为集群出现区域故障时, 依然可以依靠其他区域的实例进行正常服务提供了完善的高可用保障
blackOutServerPercentageThreshold:故障实例百分比(断路器断开数/实例数量) >=0.8
activeReqeustsPerServerThreshold:实例平均负载 >=0.6
availableServersThreshold:可用实例数(实例数量 - 断路器断开数) <2。
1 | private boolean shouldEnableZoneAffinity(List<T> filtered) { |
DefaultNIWSServerListFilter:
完全继承ZoneAffinityServerListFilter,没有自定义实现。是默认的NIWS (Netflix Internal Web Service)过滤器
ServerListSubsetFilter:
该过滤器也继承自ZoneAffinityServerListFilter。它非常适用于拥有大规模服务器集群(上百或更多)的系统。 因为它可以产生一个 “ 区域感知 ” 结果的子集列表(从过滤的服务实例中维护一个子集,可想而知服务器很多), 同时它还能够通过比较服务实例的通信失败数量和并发连接数来判定该服务是否健康来选择性地从服务实例列表中剔除那些相对不够健康的实例。 该过滤器的实现主要分为以下三步:
获取 “ 区域感知 ” 的过滤结果, 作为候选的服务实例清单
从当前消费者维护的服务实例子集中剔除那些相对不够健康的实例(同时也将这些实例从候选清单中剔除, 防止第三步的时候又被选入),标准:1. 服务实例的并 发连接数超过客户端配置的值, 默认为0,参数可配置;2.服务实例的失败数超过客户端配置的值 , 默 认为0,参数可配置 3. 如果按符合上面任一规则的服务实例剔除后,剔除比例小于客户端默认配置的百分比, 默认为0.1(10%),可配置,那么就先对剩下的实例列表进行健康排序,再从最不健康的实例进行剔除,直到达到配置的剔除百分比
完成剔除后, 清单已经少了至少10% (默认值)的服务实例, 最后通过随机的方式从候选清单中选出 一批实例加入到清单中, 以保持服务实例子集与原来的数量 一致, 而默认的实例子集数量为20
ZonePreferenceServerListFilter:
Spring Cloud 整合时新增的过滤器。 若使用Spring Cloud整合Eureka和Ribbon时会默认使用该过滤器。它实现了通过配置或者 Eureka 实例元数据的所属区域 (Zone) 来过滤出同区域的服务实例。如下面的源码所示,它的实现非常简单,首先通过父类 ZoneAffinityServerListFilter的过滤器来获得 “ 区域感知 ” 的服务实例列表, 然后遍历这个结果, 取出根据消费者配置预设的区域 Zone 来进行过滤, 如果过滤的结果是空就直接返回父类获取的结果, 如果不为空就返回通过消费者配置的 Zone 过滤后的结果:
1 |
|
ZoneAwareLoadBalancer
继承了DynamicServerListLoadBalancer,DynamicServerListLoadBalancer没有重写chooseServer方法,即它会使用BaseLoadBalancer的实现,使用RoundRobinRule以线性轮询的方式来选择调用的服务实例。该
算法实现简单并没有区域 (Zone) 的概念, 所以它会把所有实例视为一个 Zone下的节点来看待, 这样就会周期性地产生跨区域 (Zone) 访问的情况, 由于跨区域会产生更高的延迟,这些实例主要以防止区域性故障实现高可用为目的而不能作为常规访问的实例, 所以在多区域部署的清况下会有一定的性能问题, 而该负载均衡器则 可以避免这样的问题
ZoneAwareLoadBalancer没有重写setServersList,说明实现服务实例清单的更新主逻辑没有修改,但是重写了setServerListForZones,先看下DynamicServerListLoadBalancer的该方法实现:
作用:它在父类DynamicServerListLoadBalancer中的作用是根据按区域 Zone 分组的实例列表, 为负载均衡器中的 LoadBalancerStats对象创建 Zonestats 并放入Map zonestatsMap集合中, 每一个区域 Zone 对应一个ZoneStats, 它用于存储每个 Zone 的一些状态和统计信息
1 | //该方法来自DynamicServerListLoadBalancer,在setServersList最后一步调用 |
ZoneAwareLoadBalancer的setServerListForZones方法如下:
balancers用来存储每个 Zone 区域对应的负载均衡器,负载均衡器是通过getLoadBalancer获取的,创建的同时添加了IRule,如果IRule实例为空,则创建AvailabilityFilteringRule
第二个循环则是对Zone区域中实例清单的检查, 看看是否有Zone区域下已经没有实例了, 是的话就将balancers 中对应 Zone 区域的实例列表清空, 该操作的作用是为了后续选择节点时, 防止过时的 Zone 区域统计信息干扰具体实例的选择算法
1 | //private ConcurrentHashMap<String, BaseLoadBalancer> balancers |
回过头来我们重点看下chooseServer方法的实现:
当负载均衡器中维护的实例所属的 Zone 区域的个数大于1 的时候才会执行这里的选择策略:
createSnapshot为当前负载均衡器中所有的Zone区域分别创建快照,保存在zoneSnapshot
调用getAvailableZones 来获取可用的Zone区域集合,在该函数中会通过Zone区域快照中的统计数据来实现
可用区的挑选当获得的可用Zone区域集合不为空, 并且个数小于Zone区域总数, 就随机选择 一个Zone区域
在确定了某个 Zone 区域后,则获取了对应Zone 区域的服务均衡器,并调用chooseServer来选择具体的服务实例,而在chooseServer中将使用IRule接口的 choose函数来选择具体的服务实例,这里的具体实现是ZoneAvoidanceRule
1 |
|
负载均衡策略
下面我们看下几个IRule的实现
AbstractLoadBalancerRule
负载均衡策略的 抽象类,在该抽象类中定义了负载均衡器ILoadBalancer对象 ,该对象能够在具体实现选择服务 策略时, 获取到 一些负载均衡器中维护的信息来作为分配依据
1 | public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware { |
RandomRule
采用随机的方式,从upList中筛选出实例:
1 | public Server choose(ILoadBalancer lb, Object key) { |
RoundRobinRule
内部增加了 一个 coun七计数变量, 该变量会在每次循环之后累加, 也就是说, 如果一直选择不到 server 超过 10 次, 那么就会结束尝试, 并打印 一个警告信息 。内部维护了一个计数变量,然后通过取模来获取下一个服务
RetryRule
默认使用了 RoundRobinRule 实例,若期间能够选择到具体的服务实例就返回,若选择不到就根据设置的尝试结束时间为阙值(maxRetryMillis参数定义的值+choose 方法开始执行的时间戳), 当超过该阑值后就返回 null。
WeightedResponseTimeRule
启动一个定时任务, 用来为每个服务实例计算权重, 该任务默认30秒执行一次,权重的计算:
根据LoadBalancerStats中记录的每个实例的统计信息, 累加所有实例的平均响应时间, 得到总平均响应时间totalResponseTime
为负载均衡器中维护的实例清单逐个计算权重(从第 一 个开始)
权重区间边界的开闭原则根据算法, 正常每个区间为(x, y)的形式, 但是第一个实例和最后一个实例为什么不同呢?由于随机数的最小取值可以为O, 所以第一个实例的下限是闭区间, 同时随机数的最大值取不到最大权重值, 所以最后一个实例的上限是开区间
配置详解
自动化配置
下面这些自动化配置内容仅在没有引入Spring Cloud Eureka等服务治理框架时如此,在同时引入Eureka和沁bbon依赖时,自动化配置会有一些不同
参数配置
与Eureka结合
重试机制
相关参数说明: