2.SpringCloud 服务注册与发现-Eureka
Eureka 工作原理
1. Eureka 两大组件
Eureka 采用 CS(Client/Server,客户端/服务器) 架构,它包括以下两大组件:
- Eureka Server:Eureka 服务注册中心,主要用于提供服务注册功能。用来接收其他服务的注册。
- 服务注册,所有的服务都注册到 Eureka Server 上面来
- 提供注册表,Eureka Client 在调用服务时,需要获取这个注册表,一般来说,这个注册表会缓存下来,如果缓存失效,则直接获取最新的注册表
- 同步状态,Eureka Client 通过注册、心跳等机制,和 Eureka Server 同步当前客户端的状态
- Eureka Client:Eureka 客户端,通常指的是微服务系统中各个微服务,主要用于和 Eureka Server 进行交互,并可以实现负载均衡等功能。
-
服务注册,服务提供者将自己注册到服务注册中心
-
服务续约,注册成功后,默认情况下,Eureka CLient 每隔 30 秒就要向 Eureka Server 发送一条心跳消息。如果 Eureka Server 连续 90 秒都有没有收到 Eureka Client 的续约消息,它会认为 Eureka Client 已经掉线了,会从当前的服务注册列表中剔除。
# 服务续约时间 30s eureka.instance.lease-renewal-interval-in-seconds=30 # 服务失效时间 90s eureka.instance.lease-expiration-duration-in-seconds=90
-
服务下线,当 Eureka Client 下线时,它会主动发送一条消息,告诉 Eureka Server
-
获取注册表信息,Eureka Client 从 Eureka Server 上获取服务的注册信息,并将其缓存在本地
# 是否允许获取注册表信息 eureka.client.fetch-registry=true # 定期更新注册表的时间间隔,默认 30 秒 eureka.client.registry-fetch-interval-seconds=30
-
2. 工作原理
- Eureka Server:注册中心,用于提供服务注册和发现功能。
- Eureka Provider:服务提供者,它将自己提供的服务注册到服务注册中心,以供服务消费者发现。
- Eureka Consumer:服务消费者,它可以从服务注册中心获取服务列表,调用所需的服务。
Eureka 搭建
1.创建一个普通的 Spring Boot 项目,创建时,勾选 Eureka Server依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
2.在项目启动类上添加 @EnableEurekaServer 注解,标记该项目是一个 Eureka Server
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
3.在 application.properties 中添加基本配置信息
# 设置服务名称
spring.application.name=eureka
# 设置端口号
server.port=1111
# 默认情况下,Eureka Server 也是一个普通的微服务,自己也会注册进来
# register-with-eureka 设置为 false,表示当前项目不要注册到注册中心上
eureka.client.register-with-eureka=false
# 表示是否从 Eureka Server 上获取注册信息
eureka.client.fetch-registry=false
项目启动成功后,浏览器输入 http://localhost:1111 就可以查看 Eureka 后台管理页面了:
Eureka 集群搭建
使用了注册中心之后,所有的服务都要通过服务注册中心来进行信息交换。要保证注册中心的稳定性,Eureka 一般都是以集群的形式出现的。Eureka 集群,实际上就是启动多个 Eureka 实例,多个 Eureka 实例之间,互相注册,互相同步数据,共同组成一个 Eureka 集群。
1.修改电脑的 hosts 文件
127.0.0.1 eurekaA eurekaB
2.新增两个eureka实例,为了方便,这里新增两个配置文件,再以jar包形式分别启动
application-a.properites 内如如下:
# 设置服务名称
spring.application.name=eureka
# 设置端口号
server.port=1111
eureka.instance.hostname=eurekaA
# 默认情况下,Eureka Server 也是一个普通的微服务,自己也会注册进来
# register-with-eureka 设置为 false,表示当前项目不要注册到注册中心上
eureka.client.register-with-eureka=true
# 表示是否从 Eureka Server 上获取注册信息
eureka.client.fetch-registry=true
# A 服务要注册到 B 上面
eureka.client.service-url.defaultZone=http://eurekaB:1112/eureka
application-b.properites 内如如下:
# 设置服务名称
spring.application.name=eureka
# 设置端口号
server.port=1112
eureka.instance.hostname=eurekaB
# 默认情况下,Eureka Server 也是一个普通的微服务,自己也会注册进来
# register-with-eureka 设置为 false,表示当前项目不要注册到注册中心上
eureka.client.register-with-eureka=true
# 表示是否从 Eureka Server 上获取注册信息
eureka.client.fetch-registry=true
# B 服务要注册到 A 上面
eureka.client.service-url.defaultZone=http://eurekaA:1111/eureka
3.对当前项目打包,打成 jar 包,分别启动
java -jar eureka-0.0.1-SNAPSHOT.jar --spring.profiles.active=a
java -jar eureka-0.0.1-SNAPSHOT.jar --spring.profiles.active=b
启动成功后,浏览器访问 http://127.0.0.1:1111/,就可以看到,两个服务之间互相注册,共同给组成一个集群。
Eureka 集群原理
在这个集群架构中,Eureka Server 之间通过 Replicate 进行数据同步,不同的 Eureka Server 之间不区分主从节点,所有节点都是平等的。节点之间,通过置顶 serviceUrl 来互相注册,形成一个集群,进而提高节点的可用性。
在 Eureka Server 集群中,如果有某一个节点宕机,Eureka Client 会自动切换到新的 Eureka Server 上。每一个 Eureka Server 节点,都会互相同步数据。
服务注册与消费
服务注册
服务注册就是把一个微服务注册到 Eureka Server 上,以传统springboot项目为例,添加Web 和 Eureka Discovery Client依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在 application.properties 中配置一下项目的注册地址
# provider 配置
spring.application.name=provider
server.port=1113
eureka.client.service-url.defaultZone=http://localhost:1111/eureka
启动 Eureka Server,待服务注册中心启动成功后,再启动 provider。
两者都启动成功后,浏览器输入 http://localhost:1111,就可以查看到 provider 的注册信息
服务消费
在 provider 中,提供一个 hello 接口
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello javaboy";
}
}
创建一个 consumer 项目,添加Web 和 Eureka Discovery Client依赖
# 消费者 consumer 配置
spring.application.name=consumer
server.port=1115
eureka.client.service-url.defaultZone=http://localhost:1111/eureka
consumer 中消费 provider 提供的接口:
- DiscoveryClient:服务查询 ,可以根据服务名从 Eureka Server 上查询服务的详细信息
- RestTemplate:HTTP调用
- @LoadBalanced:负载均衡,给 RestTemplate 添加一个 @LoadBalanced 注解,开启负载均衡
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
@Autowired
RestTemplate restTemplate;
@GetMapping("/hello2")
public String hello2() {
return restTemplate.getForObject("http://provider/hello", String.class);
}
注意,上述代码中,restTemplate.getForObject(“http://provider/hello”, String.class);第一个参数URL,是服务名称而不是具体的服务地址。使用RestTemplate,其实是封装了服务查询和URL拼接的过程。
// 根据服务名查询服务的详细信息
List<ServiceInstance> list = discoveryClient.getInstances(“provider”);
// 手动实现负载均衡 (count++) % list.size()
ServiceInstance instance = list.get((count++) % list.size());
RestTemplate
RestTemplate 是从 Spring3.0 开始支持的一个 Http 请求工具,提供了常见的 REST 请求方法模板,例如 GET、POST、PUT、DELETE 请求以及一些通用的请求执行方法 exchange 和 execute 方法。
GET 请求
- getForObject() 返回的是一个对象,这个对象就是服务端返回的具体值。
- getForEntity() 返回的是一个ResponseEntity,还包含了 Http 响应头的数据。
POST 请求
- postForObject()
- postForEntity()
- postForLocation() 执行完post 请求之后,进行重定向,比如 注册后重定向到登录页面,注意,重定向的地址,
一定要写成绝对路径
,不要写相对路径,否则在 consumer 中调用时会出问题
PUT 请求
- put()
可接受两种类型的参数,key/value 形式以及 JSON 形式。
DELETE 请求
- delete()
有两种方式来传递参数,key/value 形式或者 PathVariable(参数放在路径中)
restTemplate.delete("http://provider/user1?id={1}", 99);
restTemplate.delete("http://provider/user2/{1}", 99);
负载均衡
- 服务端负载均衡,就是传统的 Nginx 的方式。调用的客户端并不知道具体是哪一个 Server 提供的服务,它也不关心。
- 客户端负载均衡,调用的客户端本身是知道所有 Server 的详细信息的,当需要调用 Server 上的接
口的时候,客户端从自身所维护的 Server 列表中,根据提前配置好的负载均衡策略,自己挑选一个
Server 来调用。
负载均衡原理
在 RestTemplate 中,要想使用负载均衡功能,只需要给 RestTemplate 实例上添加 @LoadBalanced注解即可,此时,RestTemplate 就会自动具备负载均衡功能,这个负载均衡就是客户端负载均衡。
整体上来说,这个功能的实现就是三个核心点:
- 查询注册的服务列表;
- 选择一个可以调用的服务;
- 根据 2 中所选择的服务,重构请求 URL 地址。
RibbonLoadBalancerClient部分源码如下:
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
//1.加载服务列表
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
//2.选择一个服务
Server server = this.getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
}
}
//3.重构URI
public URI reconstructURI(ServiceInstance instance, URI original) {
Assert.notNull(instance, "instance can not be null");
String serviceId = instance.getServiceId();
RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
URI uri;
Server server;
if (instance instanceof RibbonLoadBalancerClient.RibbonServer) {
RibbonLoadBalancerClient.RibbonServer ribbonServer = (RibbonLoadBalancerClient.RibbonServer)instance;
server = ribbonServer.getServer();
uri = RibbonUtils.updateToSecureConnectionIfNeeded(original, ribbonServer);
} else {
server = new Server(instance.getScheme(), instance.getHost(), instance.getPort());
IClientConfig clientConfig = this.clientFactory.getClientConfig(serviceId);
ServerIntrospector serverIntrospector = this.serverIntrospector(serviceId);
uri = RibbonUtils.updateToSecureConnectionIfNeeded(original, clientConfig, serverIntrospector, server);
}
return context.reconstructURIWithServer(server, uri);
}
参考文章:http://itboyhub.com/2021/01/31/spring-cloud-provider-consumer/