最近开始对SpringCloud这块内容进行一个整体的回顾,同时也将相关知识进行整理方便后续复习,最近主要是在看《SpringCloud 微服务实战》这本书,SpringCloud详解相关博客都统一使用SpringBoot 2.1.6.RELEASE版本,SpringCloud Greenwich.SR1版本。博客主要涉及的SpringCloud组件有Eureka、Ribbon、Hystrix、Fegin、Zuul等,后续还打算研究一下一些相关开源组件
服务治理主要用来实现各个微服务实例的自动化注册与发现
服务注册:服务治理框架中,构建一个注册中心,每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。服务注册中心还需要以心跳的方式去监测清单中的服务是否可用,若不可用需要从服务清单中剔除,达到排除故障服务的效果
服务发现:在服务治理框架下运作,服务间的调用不再通过指定具体的实例地址来实现,而是通过向服务名发起请求调用实现。服务调用方在调用服务提供方接口的时候,并不知道具体的服务实例位置。因此,调用方需要向服务注册中心咨询服务,并获取所有服务的实例清单,以实现对具体服务实例的访问
Eureka服务端,也称为服务注册中心。它支持高可用配置,提供良好的服务实例可用性。如果Eureka以集群模式部署,当集群中有分片出现故障时,那么Eureka就转入自我保护模式。它允许在分片故障期间继续提供服务的发现和注册,当故障分片恢复运行时,集群中的其他分片会把它们的状态再次同步回来
Eureka客户端,主要处理服务的注册与发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性地发送心跳来更新它的服务租约。同时,它也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性地刷新服务状态
项目名称:eureka-server
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.6.RELEASEversion>
<relativePath/>
parent>
<groupId>com.handgroupId>
<artifactId>eureka-serverartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>eureka-servername>
<description>eureka-serverdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Greenwich.SR1spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
使用@EnableEurekaServer注册启动一个服务注册中心
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
application.properties
server.port=7001
eureka.instance.hostname=localhost
#该应用为注册中心,所以设置为false,代表不向注册中心注册自己
eureka.client.register-with-eureka=false
#注册中心的职责是维护服务实例,并不需要去检索服务,所以设置为false
eureka.client.fetch-registry=false
#服务注册中心地址
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
启动并应用访问http://localhost:7001/,可以看到如下所示的Eureka信息面板,其中Instances currently registered with Eureka栏是空的,说明该注册中心还没有注册任何服务
项目名称:service-provider
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.6.RELEASEversion>
<relativePath/>
parent>
<groupId>com.handgroupId>
<artifactId>service-providerartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>service-providername>
<description>service-providerdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Greenwich.SR1spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
提供一个RESTful API接口
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(HttpServletRequest request) {
return "hello:" + request.getServerPort();
}
}
在主类上添加@EnableDiscoveryClient注解
@EnableDiscoveryClient
@SpringBootApplication
public class ServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceProviderApplication.class, args);
}
}
application.properties
server.port=8080
#服务名
spring.application.name=hello-service
#指定服务注册中心地址
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/
先启动服务注册中心再启动service-provider。访问Eureka的信息面板,在Instances currently registered with Eureka一栏中看到服务的注册信息
Eureka Server的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心,以实现服务清单的互相同步,达到高可用的效果
构建一个双节点的服务注册中心集群:
application-peer1.properties
server.port=7001
spring.application.name=eureka-server
eureka.instance.hostname=eureka7001.com
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.service-url.defaultZone=http://eureka7002.com:7002/eureka/
application-peer2.properties
server.port=7002
spring.application.name=eureka-server
eureka.instance.hostname=eureka7002.com
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.service-url.defaultZone=http://eureka7001.com:7001/eureka/
在/etc/hosts文件中添加对peer1和peer2的转换,让上面配置的host形式的service-url能在本地访问到
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
通过spring.profiles.active
属性来分别启动peer1和peer2
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2
此时访问peer1的注册中心http://eureka7001.com:7001/或者peer2的注册中心http://eureka7002.com:7002/效果如下:
在设置了多个节点的服务注册中心之后,服务提供方需要修改服务注册中心地址
server.port=8080
spring.application.name=hello-service
eureka.client.service-url.defaultZone=http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
如果不想使用主机名来定义注册中心的地址,也可以使用IP地址的形式,但是需要在配置文件中增加配置参数eureka.instance.prefer-ip-address=true
,该值默认为false
服务消费者主要完成两个目标,发现服务以及消费服务。服务发现的任务由Eureka客户端完成,而服务消费的任务由Ribbon完成。Ribbon是一个基于HTTP和TCP的客户端负载均衡器,它可以在通过客户端中配置的ribbonServerList服务端列表去轮询访问以达到负载均衡的作用。当Ribbon和Eureka联合使用时,Ribbon的服务实例清单RibbonServerList会被DiscoveryEnabledNIWSServerList重写,扩展成从Eureka注册中心中获取服务端列表。同时它也会用NIWSDiscoveryPing来取代IPing,它将职责委托给Eureka来确定服务端是否已经启动
为了实验Ribbon的客户端负载均衡功能,通过java -jar命令行的方式来启动两个不同端口的hello-service服务
java -jar service-provider-0.0.1-SNAPSHOT.jar --server.port=8080
java -jar service-provider-0.0.1-SNAPSHOT.jar --server.port=8081
在成功启动两个hello-service服务之后,从Eureka信息面板中可以看到名为HELLO-SERVICE的服务中出现了两个实例单元
新建一个ribbon-consumer项目,pom.xml文件内容如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.6.RELEASEversion>
<relativePath/>
parent>
<groupId>com.handgroupId>
<artifactId>ribbon-consumerartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>ribbon-consumername>
<description>ribbon-consumerdescription>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Greenwich.SR1spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
在主类上添加@EnableDiscoveryClient注解让该应用注册为Eureka客户端应用,以获得服务发现的能力。同时,在该主类中创建RestTemplate的Spring Bean实例,并通过@LoadBalanced注解开启客户端负载均衡
@EnableDiscoveryClient
@SpringBootApplication
public class RibbonConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(RibbonConsumerApplication.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
使用RestTemplate来实现对HELLO-SERVICE服务提供的/hello接口进行调用,访问的地址是服务名HELLO-SERVICE而不是一个具体的地址
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/ribbon-consumer")
public String helloConsumer() {
return restTemplate.getForEntity("http://HELLO-SERVICE/hello", String.class).getBody();
}
}
application.properties
spring.application.name=ribbon-consumer
server.port=9000
eureka.client.register-with-eureka=false
eureka.client.service-url.defaultZone=http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
通过向http://localhost:9000/ribbon-consumer发起GET请求,多发送几次请求,访问结果一次显示hello:8080,下一次则显示hello:8081
A.服务注册
服务提供者在启动的时候会通过发送REST请求的方式将自己注册到Eureka Server上,同时带上了自身服务的一些元数据信息。Eureka Server接收到这个REST请求之后,将元数据信息存储在一个双层结构Map中,其中第一层的key是服务名称,第二层的key是具体的服务实例名
B.服务同步
两个服务提供者分别注册到了两个不同的服务注册中心上,也就是说,它们的信息分别被两个服务注册中心所维护。此时,由于服务注册中心之间因互相注册为服务,当服务提供者发送注册请求到一个服务注册中心时,它会将该请求转发给集群中相连的其他注册中心,从而实现注册中心之间的服务同步。通过服务同步,两个服务提供者的服务信息就可以通过这两台服务注册中心中的任意一台获取到
C.服务续约
在注册完服务之后,服务提供者会维护一个心跳用来持续告诉Eureka Server:“我还活着”,以防止Eureka Server的“剔除任务”将该服务实例从服务列表中排除出去
#定义服务续约任务的调用间隔时间,默认30秒
eureka.instance.lease-renewal-interval-in-seconds=30
#定义服务失效的时间,默认90秒
eureka.instance.lease-expiration-duration-in-seconds=90
2)服务消费者
A.获取服务
当我们启动服务消费者的时候,它会发送一个REST请求给服务注册中心,来获取上面注册的服务清单。为了性能考虑,Eureka Server会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30秒更新一次
#想要获取服务必须确保该参数没有被修改成false,默认为true
eureka.client.fetch-registry=true
#修改缓存清单的更新时间,默认30秒
eureka.client.registry-fetch-interval-seconds=30
B.服务调用
服务消费者在获取服务清单后,通过服务名可以获得具体提供服务的实例名和该实例的元数据信息。因为有这些服务实例的详细信息,所以客户端可以根据自己的需要决定具体调用哪个实例,在Ribbon中会默认采用轮询的方式进行调用,从而实现客户端的负载均衡
对于访问实例的选择,Eureka中有Region和Zone的概念,一个Region中可以包含多个Zone,每个服务客户端需要被注册到一个Zone中,所以每个客户端对应一个Region和一个Zone。在进行服务调用的时候,优先访问同处于一个Zone中的服务提供方,若访问不到,就访问其他的Zone
C.服务下线
当服务实例进行正常的关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务端在接收到请求之后,将该服务状态置为下线(DOWN),并把该下线事件传播出去
3)服务注册中心
A.失效剔除
有些时候,服务实例并不一定会正常下线,可能由于内存溢出、网络故障等原因使得服务不能正常工作,而服务注册中心并未收到“服务下线”的请求。为了从服务列表中将这些无法提供服务的实例剔除,Eureka Server在启动的时候会创建一个定时任务,默认每隔90秒将当前清单中超时(默认为90秒)没有续约的服务剔除出去
B.自我保护
在服务注册中心的信息面板上出现如下的红色警告信息:
该警告就是触发了Eureka Server的自我保护机制。服务注册到Eureka Server之后,会维护一个心跳连接,告诉Eureka Server自己还活着。Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况,Eureka Server会将当前的实例注册信息保护起来,让这些实例不会过期,尽可能保护这些注册信息。但是,在这段保护期间内实例若出现问题,那么客户端很容易拿到实际已经不存在的服务实例,会出现调用失败的情况,所以客户端必须要有容错机制,比如可以使用请求重试、断路器等机制
#关闭保护机制,以确保注册中心可以将不用的实例正确剔除(本地调试可以使用,线上不推荐)
eureka.server.enable-self-preservation=false