Netflix eureka 简单使用笔记

Eureka 是 Netflix 开发的服务发现框架.
它本身是一个基于 REST 的服务, 主要用于定位中间层服务, 同时也有负载均衡和分区容灾的目的
Spring cloud 将它集成在其子项目 spring-cloud-netflix 中以实现 Spring cloud 的服务发现功能

注册中心简介

微服务架构中的通讯录, 记录服务和服务地址的映射关系

常见的注册中心

  • Netflix Eureka
  • Alibaba Nacos
  • HashiCorp Consul
  • Apache Zookeeper
  • CoreOS etcd
  • CNCF CoreDNS

CAP

  1. Consistency(一致性)
  2. Availability(可用性)
  3. Partition tolerance(分区容错性)

    CAP 中三者最多同时实现两个

注册中心中的角色

  • Server(注册中心)
  • Consumer(消费者)
  • Provider(生产者)

配置

注册中心实例需要在 Spring boot 启动类添加 @EnableEurekaServer 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 9998 # 实例端口

spring:
application:
name: eureka-server ## 实例名称, 相同实例名称相同
eureka:
server:
enable-self-preservation: false ## Eureka自我保护模式是否可以开启
eviction-interval-timer-in-ms: 60000 ## 清理间隔时间 (ms), 默认60000
instance:
hostname: eureka ## 主机名, 不配置会自动获取
prefer-ip-address: true ## 是否使用IP地址注册, 不使用IP注册其他服务将不能直接获取该实例IP
instance-id: ${spring.cloud.client.ip-address}:${server.port} ## 实例ID, 与eureka.instance.prefer-ip-address搭配可使注册中心网页实例显示IP:port
client:
fetch-registry: true ## 是否拉去注册信息, 单节点注册中心需要关闭
register-with-eureka: true ## 是否将自己注册到注册中心, 单节注册中心点需要关闭
registry-fetch-interval-seconds: 10 ## 拉取实例注册信息间隔(s), 默认30
service-url: ## 注册中心地址, 多节点注册中心需要互相注册
defaultZone: http://127.0.0.1:8761/eureka/, http://127.0.0.1:8762/eureka/ ## 添加多个注册中心地址时可以用逗号分隔, 或写成数组形式, Eureka节点间会互相同步
## 添加 Spring security 后需要改为 http://${spring.security.user.name}:${spring.security.user.password}@${Host}:${Port}/eureka/ 进行 BasicHttp 验证

eureka.client.service.defaultZone 中如需添加多个注册中心地址, 尽量用逗号分隔, 使用数组形式时服务发现可能会不进行 basichttp 验证

eureka.client.service.defaultZone 中地址尽量以 / 结尾, 否则某些版本会报错

远程调用

实现方式

  • DiscoverClient
    通过元数据获取服务信息
  • LoadBalancerClient
    通过负载均衡器获取服务信息
  • @LoadBalanced
    RestTemplate远程调用时自动拉取服务实例信息, 添加@LoadBalanced注解后前两种方法将的 RestTemplate 不能使用 IOC 中的 bean

代码

Config.java
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class Config {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
ControllerTest.java
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.Mapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Controller
@RestController
@RequestMapping("/consumer")
public class ControllerTest {
private final RestTemplate restTemplate;
private final DiscoveryClient discoveryClient; // 元数据对象
private final LoadBalancerClient loadBalancerClient; // 负载均衡器

public ControllerTest(RestTemplate restTemplate, DiscoveryClient discoveryClient, LoadBalancerClient loadBalancerClient) {
this.restTemplate = restTemplate;
this.discoveryClient = discoveryClient;
this.loadBalancerClient = loadBalancerClient;
}

@RequestMapping("/discoveryClient")
public String discoveryClient() {
StringBuilder stringBuilder = new StringBuilder("<h1>discoveryClient:</h1><br>");

//获取全部服务列表
List<String> serviceIds = discoveryClient.getServices();
if (serviceIds.isEmpty()) {
stringBuilder.append("serviceIds: empty<br>");
return stringBuilder.toString();
} else {
stringBuilder.append("serviceIds: ").append(serviceIds).append("<br>");
}

// 根据服务名称获取服务
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("eureka-provider");
if (serviceInstances.isEmpty()) {
stringBuilder.append("eureka-provider: null<br>");
return stringBuilder.toString();
} else {
stringBuilder.append("eureka-provider:").append(serviceInstances).append("<br>");
}

ServiceInstance serviceInstance = serviceInstances.get(0);

// 远程调用
stringBuilder.append("remote call result: ");

String remoteUrl = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/provider/test";
String remoteCallResult = restTemplate.getForEntity(remoteUrl, String.class).getBody();

stringBuilder.append(remoteCallResult);
return stringBuilder.toString();
}

@RequestMapping("/LoadBalancerClient")
public String LoadBalancerClient() {
StringBuilder stringBuilder = new StringBuilder("<h1>LoadBalancerClient:</h1><br>");

// 根据服务名称获取服务
ServiceInstance serviceInstance = loadBalancerClient.choose("eureka-provider");
if (null == serviceInstance) {
stringBuilder.append("serviceInstance: null <br>");
return stringBuilder.toString();
} else {
stringBuilder.append("serviceInstance: ").append(serviceInstance).append("<br>");
}

// 远程调用
stringBuilder.append("remote call result: ");
String remoteUrl = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/provider/test";
String remoteCallResult = restTemplate.getForEntity(remoteUrl, String.class).getBody();

stringBuilder.append(remoteCallResult);
return stringBuilder.toString();
}

@RequestMapping("/LoadBalanced")
public String LoadBalanced() {
StringBuilder stringBuilder = new StringBuilder("<h1>LoadBalanced:</h1><br>");
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://eureka-provider/provider/test", String.class);

return stringBuilder.append("remote call result: ").append(responseEntity.getBody()).toString();
}
}

自我保护模式

Eureka 发现实例的心跳比例在 15 min 内低于 85% 时触发. Eureka会将实例保护起来不会过期, 并发出警告. 当网络故障恢复后Eureka将解除自我保护模式
Eureka客户端具有缓存功能, 所有注册中心实例都下线时, 其他实例也可根据缓存通信
负载均衡策略会自动剔除下线实例

Eureka 常用的 API

请求名称 请求方式 HTTP地址 请求描述
注册新服务 POST /eureka/apps/{appID} 传递JSON或者XML格式参数内容,HTTP code为204时表示成功
取消注册服务 DELETE /eureka/apps/{appID}/{instanceID} HTTP code为200时表示成功
发送服务心跳 PUT /eureka/apps/{appID}/{instanceID} HTTP code为200时表示成功
查询所有服务 GET /eureka/apps HTTP code为200时表示成功,返回XML/JSON数据内容
查询指定appID的服务列表 GET /eureka/apps/{appID} HTTP code为200时表示成功,返回XML/JSON数据内容
查询指定appID&instanceID GET /eureka/apps/{appID}/{instanceID} 获取指定appID以及InstanceId的服务信息,HTTP code为200时表示成功,返回XML/JSON数据内容
查询指定instanceID服务列表 GET /eureka/apps/instances/{instanceID} 获取指定instanceID的服务列表,HTTP code为200时表示成功,返回XML/JSON数据内容
变更服务状态 PUT /eureka/apps/{appID}/{instanceID}/status?value=DOWN 服务上线、服务下线等状态变动,HTTP code为200时表示成功
变更元数据 PUT /eureka/apps/{appID}/{instanceID}/metadata?key=value HTTP code为200时表示成功
查询指定IP下的服务列表 GET /eureka/vips/{vipAddress} HTTP code为200时表示成功
查询指定安全IP下的服务列表 GET /eureka/svips/{svipAddress} HTTP code为200时表示成功

心跳

实例默认每 30s 会向 Eureka 发送一次心跳
Eureka会剔除 90s 未发送心跳的实例

健康检测

步骤

  1. 导入 actuator 的 jar 包
    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  2. 配置application.yml 文件
    1
    2
    3
    4
    5
    6
    7
    8
    management: ## 度量指标监控与健康检查
    endpoints:
    web:
    exposure:
    include: shutdown ## 添加 shutdown 端点, 即 /actuator/shutdown URI
    endpoint:
    shutdown:
    enabled: true ## 开启 shutdown 端点

    如需开启 shutdown 端点必须同时将 management.endpoint.shutdown.enabled 设置为 true

  3. 访问 /actuator URI即可查看该实例状况

远程停服

开启 shutdown 端点后 POST 访问 /actuator/shutdown URI 即可将服务远程关闭,返回信息:

1
2
3
{
"message": "Shutting down, bye..."
}

整合Spring Security

问题

Spring boot 3.0.0 + Spring cloud 2022.0.0-RC2 版本组合会周期性触发空指针异常, 日志信息为
1
2
3
4
5
6
7
2022-12-10T16:01:33.097+08:00  WARN 5620 --- [get_127.0.0.1-0] c.n.eureka.util.batcher.TaskExecutors: Discovery WorkerThread error

java.lang.NullPointerException: Cannot invoke "String.toLowerCase()" because the return value of "java.lang.Throwable.getMessage()" is null
at com.netflix.eureka.cluster.ReplicationTaskProcessor.maybeReadTimeOut(ReplicationTaskProcessor.java:196) ~[eureka-core-2.0.0-rc.4.jar:2.0.0-rc.4]
at com.netflix.eureka.cluster.ReplicationTaskProcessor.process(ReplicationTaskProcessor.java:95) ~[eureka-core-2.0.0-rc.4.jar:2.0.0-rc.4]
at com.netflix.eureka.util.batcher.TaskExecutors$BatchWorkerRunnable.run(TaskExecutors.java:190) ~[eureka-core-2.0.0-rc.4.jar:2.0.0-rc.4]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

版本降为 Spring boot 2.7.6 + Spring cloud 2021.0.5 后问题没有复现

配置

eureka.client.service-url.defaultZone
需改为 http://${spring.security.user.name}:${spring.security.user.password}@${Host}:${Port}/eureka 的形式

CSRF 处理

通常有两种简单的处理方式

  • 使 CSRF 忽略 /eureka/** 的所有请求
  • 关闭 CSRF

使 CSRF 忽略 /eureka/** 的所有请求:

Config.java
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
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class Config {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests()
.anyRequest().authenticated()

.and()
.formLogin()

.and()
.httpBasic()

.and()
.csrf().ignoringRequestMatchers("/eureka/**")

.and().build();
}
}

关闭 CSRF

Config.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class Config {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.formLogin()
.and().httpBasic() // 配置 HTTP 基本身份验证, 为了兼容 http://${spring.security.user.name}:${spring.security.user.password}@${Host}:${Port}/eureka 登录方式

.and()
.authorizeHttpRequests()
.anyRequest().authenticated()

.and()
.csrf().disable().build();
}
}

负载均衡

主流的负载均衡方案分为服务器负载均衡(集中式负载均衡)和客户端负载均衡(进程内负载均衡)
高版本 Spring cloud 默认使用 LoadBalance 代替 Ribbon 执行负载均衡

Ribbon

Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具

问题

Spring boot 2.7.6 + Spring cloud 2021.0.5 整合 Ribbon 会出现负载均衡无法获取注册实例情况,经过调试发现, Ribbon 可用从 Eureka 获取所有实例详细, 但最终没有保存下来.

版本降为 Spring boot 2.2.4.RELEASE + Spring cloud Hoxton.SR1 后问题没有复现

依赖

高版本的 Spring cloud 默认负载均衡变为 LoadBalance ,使用 Ribbon 时需要手动导入 jar 包

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>${spring-cloud-starter-netflix-ribbon.version}</version>
</dependency>

策略

策略名称 对应类名 原理
轮询 RoundRobinRule 按默认顺序每次调用按序取 provider
权重随机 WeightedResponseTimeRule 根据每个 provider 响应时间分配权重.
响应时间越长, 权重越小刚开始时为轮询策略, 同时开启计时器,
每 30 秒计算一次各个 provider 的平均响应时间, 之后按权重随机选择 provider
随机 RandomRule 随机选择 provider
最少并发 BestAvailableRule 选择请求并发数量最小的可用的 provider
重试 RetryRule 轮询策略的服务不可用时不做处理,
重试策略的服务不可用时会重新尝试连接其它节点
可用性敏感 AvailabilityFilteringRule 过滤性能差的 provider
  • 过去一段时间内始终连接失败的 provider
  • 处于高并发状态的 provider
区域敏感性 ZoneAvoidanceRule 以区域为单位, 过滤不可用的区域
当一个区域内有服务不可用或者响应变慢时, 降低该区域中服务的权重

Ribbon 的默认负载均衡策略是 ZoneAvoidanceRule

高版本的 Spring boot 需要配置
spring.cloud.loadbalancer.cache.enabled = false

1
2
3
4
5
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>

配置

全局配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class Config {

@Bean
IRule randomRule() {
return new RandomRule();
}

}
局部配置

配置文件 application.yml 中配置

1
2
3
eureka-provider-ribbon:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule

eureka-provider-ribbon 为具体的实例名称, 表示对该实例的调用采取的负载均衡策略
com.netflix.loadbalancer.RoundRobinRule 是策略全类名

全局配置会覆盖局部配置

Ribbon 点对点直连

使用 Ribbon 点对点直连时, 需要屏蔽 Eureka (去除 Eureka 依赖)

配置

配置文件 application.yml 中配置

1
2
3
4
5
6
7
8
eureka-provider-ribbon:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
listOfServers: http://127.0.0.1:9996, http://127.0.0.1:9997 # eureka-provider-ribbon 服务地址

ribbon:
eureka:
enable: false # 关闭 Eureka

eureka-provider-ribbon 为具体的实例名称