1.1 简介
1.1.1 概述
Netflix Eureka 是由 Netflix 开源的一款基于 REST 的服务发现组件,包括 Eureka Server 及 Eureka Client。2012 年 9 月在 GitHub 上发布 1.1.2 版本,目前 Netflix 以宣布闭源,所以市面上还是以 1.x 版本为主。Eureka 提供基于 REST 的服务,在集群中主要用于服务管理。Eureka 提供了基于 Java 语言的客户端组件,客户端组件实现了负载均衡的功能,为业务组件的集群部署创造了条件。使用该框架,可以将业务组件注册到 Eureka 容器中,这些组件可进行集群部署,Eureka 主要维护这些服务的列表并自动检查它们的状态。Spring Cloud Netflix Eureka 是 Pivotal 公司为了将 Netflix Eureka 整合于 Spring Cloud 生态系统提供的版本。
Eureka 包含两个组件:Eureka Server 和 Eureka Client, Eureka Server 提供服务注册服务。各个微服务节点通过配置启动后,会在 EurekaServer 中进行注册,这样 EurekaServer 中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。EurekaClient 通过注册中心进行访问。它是一个 Java 客户端,用于简化 Eureka Server 的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向 Eureka Server 发送心跳(默认周期为30秒)。如果 Eureka Server 在多个心跳周期内没有接收到某个节点的心跳,EurekaServer 将会从服务注册表中把这个服务节点移除(默认90秒)
1.1.2 原理图
一个简单的 Eureka 集群,需要一个 Eureka 服务器、若干个服务提供者。我们可以将业务组件注册到 Eureka 服务器中,其他客户端组件可以向服务器获取服务并且进行远程调用。Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址;提供者:启动后向 Eureka 注册自己信息(地址,提供什么服务);消费者:向 Eureka 订阅服务,Eureka 会将对应服务的所有提供者地址列表发送给消费者,并且定期更新;心跳(续约):提供者定期通过 http 方式向 Eureka 刷新自己的状态。
1.1.3 相关依赖
<!-- 需要确定 Spring Cloud 版本 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
1.2 搭建 EurekaServer
1.2.1 相关依赖
现在都是子父工程,我们将子模块中都需要用的依赖放到父工程的 pom 文件中
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<modules>
<module>eureka</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.software</groupId>
<artifactId>spring-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 演示就不使用数据库了 -->
<!--<dependency>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-starter-data-jpa</artifactId>-->
<!--</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- H 版 Spring Cloud 将 server 与 client 分开了,需要导入两个坐标 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.2.2 声明为 Eureka Server
在 eureka 服务的启动类上使用 @EnableEurekaServer,声明当前应用为 Eureka 服务。
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/10/29
* @description Eureka 启动类
*/
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
1.2.3 配置文件
server:
port: 8081
spring:
application:
name: eurekaServer # 应用名称,在 Eureka 中作为 id 标识
eureka:
client:
register-with-eureka: false # 不注册自己
fetch-registry: false # 不拉取自己
service-url:
defaultZone: http://127.0.0.1:8081/eureka/ # EurekaServer 的地址,如果是集群,需要加上其它 Server 的地址
1.2.4 启动服务
启动服务访问对应的端口就可以看到以下界面,现在是一个服务都没有注册上来。可以把
register-with-eureka
,
fetch-registry
两个配置取消就可以看到 eureka 自己了。
1.3 提供者
1.3.1 声明为 Eureka Client
在服务提供者启动类中使用
@EnableDiscoveryClient
,让 Eureka 能够发现,扫描到该服务。
@EnableEurekaClient
注解也能实现但是该注解只支持 Eureka 作为注册中心,
@EnableDiscoveryClient
可以是其他注册中心,建议使用
@EnableDiscoveryClient
。需要注意的是 Spring Cloud 从 Finchley 版本开始不加
@EnableEurekaClient
注解也可以启用注册功能。它在 Spring Boot 的自动配置类里启用了注册功能,所以只要引了 eureka-client 的依赖就会进行注册。但是要使用 DiscoveryClient 还是需要加
@EnableDiscoveryClient
注解
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/10/29
* @description 服务提供者启动类
*/
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
1.3.2 配置文件
server:
port: 8082
spring:
application:
name: ProviderServer # 应用名称,在 Eureka 中作为 id 标识
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8081/eurake/
1.3.3 提供服务
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/10/29
* @description
*/
@RestController
@RequestMapping("/provider")
public class ProviderController {
@GetMapping("/get")
public Object get() {
return "你已经消费了";
}
}
1.3.4 启动服务
启动服务之后,会自动将自己注册到 Eureka 中
1.4 消费者
1.3.1 声明为 Eureka Client
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/10/29
* @description 消费者启动类
*/
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
// 将 RestTemplate 交由容器管理
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
1.3.2 配置文件
server:
port: 8083
spring:
application:
name: ConsumerServer # 应用名称,在 Eureka 中作为 id 标识
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8081/eureka
1.3.3 消费服务
我们之前使用 RestTemplate 需要自己写 URI,这样很不利于维护,而且容易出错,现在只需要确定应用名称,利用应用名称从 Eureka 中就可以获取到详细信息。
/**
* Created with IntelliJ IDEA.
*
* @author gaohu9712@163.com
* @date 2020/10/29
* @description
*/
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/go")
public void go() {
List<ServiceInstance> providerServer = discoveryClient.getInstances("ProviderServer");
if (0 == providerServer.size()) {
return;
}
ServiceInstance serviceInstance = providerServer.get(0);
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
URI uri = serviceInstance.getUri();
System.out.println("主机:" + host);
System.out.println("端口:" + port);
System.out.println("uri:" + uri);
RestTemplate restTemplate = new RestTemplate();
String str = restTemplate.getForObject(uri + "/provider/get", String.class);
System.out.println(str);
}
}
1.3.4 启动服务
1.3.5 请求服务
1.3.6 执行流程
♞ 先启动 eureka 注册中心
♞ 启动服务提供者 provider
♞ 服务提供者启动后会把自身信息(比如服务地址以别名方式注册进 eureka)
♞ 消费者 consumer 服务在需要调用接口时,使用服务别名去注册中心获取实际的 RPC 远程调用地址
♞ 消费者获得调用地址后,底层实际是利用 HttpClient 技术实现远程调用
♞ 消费者获得服务地址后会缓存在本地 jvm 内存中,默认每间隔 30 秒更新一次服务调用地址
1.5 补充配置
1.5.1 actuator 信息完善
我们现在的服务注册到 Eureka 上面是没有 ip 地址的,以后等服务搭建集群是很不方便的,所以我们需要让他显示自己的 ip 地址;第二个就是服务名称为主机 + 服务名 + 端口,这样就暴露了主机名,我们可以指定显示的名称。
在配置文件中添加如下配置,通过健康检查(http://ip:port/actuator/health)查看是否修改成功。
eureka:
instance:
# 实例名称
instance-id: consumer-01
# 地址中显示 ip
prefer-ip-address: true
1.5.2 自我保护机制
☞ 概述
我们可以看到 Eureka 上有一行红色的英文
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
它代表了 Eureka 保护模式的开启。一旦进入保护模式,Eureka Server 将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
默认情况下,如果 Eureka Server 在一定时间内没有接收到某个微服务实例的心跳,Eureka Server 将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与 Eureka Server 之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。为了防止 Eureka Client 可以正常运行,但是与 Eureka Server 网络不通情况下,Eureka Server 不会立刻将 Eureka Client 服务剔除。
☞ 关闭自我保护机制
我们之前的 Eureka 截图中可以看到
DESKTOP-GL7GS52:ConsumerServer:8083
,
consumer-01
两个同时存在,这明明是一个服务,修改完配置之后前面的没有剔除,这就是因为自我保护机制打开了。
# Eureka Server 配置
eureka:
server:
# 关闭自我保护模式, 默认为打开
enable-self-preservation: false
# 续期时间,即扫描失效服务的间隔时间
eviction-interval-timer-in-ms: 5000
# Eureka Client 配置
eureka:
instance:
# Eureka Client 给 Eureka Server 发送心跳的时间间隔,默认 30 单位是 s
lease-renewal-interval-in-seconds: 1
# Eureka Server 最后一次收到心跳的等待上限,超时剔除服务,默认 90 单位是 s
lease-expiration-duration-in-seconds: 2
1.5.3 开启密码
服务连接 eureka 时可能会报
[eureka]MismatchedInputException: Root name 'timestamp' does not match expected
,这是由于 Spring Cloud 从 Finchey 版开始使用 security 时默认开启了 csrf,而客户端无法通过校验,所以我们需要关闭 csrf。注意
security.basic.enabled=true
这种方式在低版本才能生效,高版本已经过时。
# 需要引入 security 依赖才可使用
security:
user:
name: root
password: root
# 地址需要改为
defaultZone: http://name:password@host:port/eureka/
1.6 Eureka 高可用
1.6.1 Eureka 集群搭建
在之前的单体中我们的端口是随意的,但是搭建集群我们需要对端口进行规划,例如将 808X 端口作为 Eureka 集群的端口。先来看下配置有什么区别,起初我们是将自己注册到自己上,现在我们需要将自己注册到其他 Eureka 上,有多个则用
,
隔开。
server:
port: 8081
spring:
application:
name: eurekaServer
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://127.0.0.1:8082/eureka
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 5000
server:
port: 8082
spring:
application:
name: eurekaServer_back
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://127.0.0.1:8081/eureka
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 5000
1.6.2 Privoder 集群
服务提供者的集群配置了多个 Eureka 地址,会将自己同时注册到多个 Eureka 上,除了配置文件以外其他的服务代码完全一致,也可以加以区分是哪个提供的服务。需要注意的是 Eureka 集群的应用名称可以不一致甚至不写,但是服务提供者的应用名称必须保持一致,否则会被认为不是一个服务。
server:
port: 8091
spring:
application:
name: ProviderServer # 应用名称,在 Eureka 中作为 id 标识
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8081/eureka, http://127.0.0.1:8082/eureka
instance:
instance-id: provider-prim
prefer-ip-address: true
lease-renewal-interval-in-seconds: 1
lease-expiration-duration-in-seconds: 2
server:
port: 8092
spring:
application:
name: ProviderServer
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8081/eureka, http://127.0.0.1:8082/eureka
instance:
instance-id: provider-back
prefer-ip-address: true
lease-renewal-interval-in-seconds: 1
lease-expiration-duration-in-seconds: 2
1.6.3 远程调用
/**
* Created with IntelliJ IDEA.
*
* @author Demo_Null
* @date 2020/10/29
* @description
*/
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/go")
public void go() {
List<ServiceInstance> providerServer = discoveryClient.getInstances("ProviderServer");
if (0 == providerServer.size()) {
return;
}
RestTemplate restTemplate = new RestTemplate();
for (ServiceInstance instance : providerServer) {
System.out.print(instance.getUri() + "---");
String url = instance.getUri() + "/provider/get";
System.out.println(restTemplate.getForObject(url, String.class));
}
}
}
咱们可以使用服务发现 DiscoveryClient 来获取服务信息,但是无法自动选择使用那个服务,这里就涉及到 Ribbon 负载均衡了。我们可以将 RestTemplate 交由 Ioc 管理,在注入时使用
@LoadBalanced
注解进行负载均衡。