Spring Cloud Eureka 组件

什么是服务注册与发现组件

在 Spring Cloud 中,注册中心是一个用于管理微服务实例并进行服务发现的核心组件。它可以帮助系统中的各个服务组件相互了解对方的位置以及状态,从而实现服务之间的通信。主要有以下功能:

  • 服务注册:服务注册是指系统中的各个服务实例将自己的信息(如 IP 地址、端口、元数据等)注册到注册中心,以便其他服务能够找到它。
  • 服务发现:服务发现是指服务消费者可以通过注册中心查询其他服务的位置信息(如 IP 地址、端口等)。
  • 健康检查
  • 负载均衡
  • 多环境支持与切换
  • 分布式支持

想一下这些问题

  • 服务消费者该如何获取服务提供者的地址信息
  • 如果有多个服务提供者,消费者该如何选择
  • 消费者如何得知服务提供者的健康状态

简介

Eureka 一词来源于古希腊词汇,是“发现了”的意思。在软件领域,Eureka 是 Netflix 开源的服务注册与发现组件,后被纳入 Spring Cloud 生态,分为Eureka Server与Eureka Client。它基于 REST 协议实现,主要解决微服务架构中服务实例动态变化时的服务注册、发现与健康监控问题。官方已停止更新和维护。现在主要是社区在支持维护。

其实还有一个就是 Zookeeper,也是一个分布式协调框架,最初由 Apache 开发,广泛用于分布式系统中作为配置管理、命名服务、分布式锁等功能的实现。dubbo 原理默认注册中心就是用的 zookeeper。实际上,Nacos 我用的是最多的。

实现原理与架构

了解两个概念:

  • 服务提供者:一次业务中,被其他微服务调用的服务(为其它服务提供接口)
  • 服务消费者:一次业务中,调用其他微服务的服务

服务的提供者和消费者是相对的。

Eureka主要由两个组件组成:

  • Eureka Server:作为服务注册中心,负责接收服务实例的注册信息,并提供查询服务。
  • Eureka Client:所有微服务实例(既是服务提供者也是消费者),包含两个组件:
    • Instance Registry:负责向 Eureka Server 注册 / 续约 / 注销服务。
    • Discovery Client:负责从 Eureka Server 拉取服务列表并缓存到本地。

核心流程很清晰:

  • 服务注册:
    • 服务启动时,Eureka Client 向 Eureka Server 发送注册请求(携带服务名、IP、端口等元数据)。
    • Eureka Server 验证请求后,将服务信息存入注册表(内存中的 ConcurrentHashMap)。
    • 若启用集群,Server 会将新注册的服务同步到其他 peer 节点。
  • 服务续约(心跳机制)
    • 服务注册后,Client 每隔 30 秒向 Server 发送一次续约请求(默认配置),证明自身存活。
    • Server 收到续约后,更新服务实例的最后续约时间。
    • 若超过 90 秒(默认)未收到续约,Server 会将该实例标记为「过期」。
  • 服务下线:
    • 服务正常关闭时,Client 主动向 Server 发送注销请求,Server 从注册表中移除该实例。
    • 若服务异常崩溃(未发送注销请求),Server 会通过定时任务(默认 60 秒一次)清理过期实例。
  • 服务发现
    • Client 启动时从 Server 全量拉取服务列表,缓存到本地。
    • 之后每隔 30 秒(默认)向 Server 发送增量请求,更新本地缓存。
    • 客户端调用服务时,直接从本地缓存获取实例列表,结合负载均衡(如 Ribbon)选择目标实例。

所以上面的三个核心的问题就可以知道怎么解决了

  • 服务消费者该如何获取服务提供者的地址信息
    • 注册阶段:服务提供者启动后,会主动向注册与发现组件(比如 Eureka Server )“报到”,把自己的地址信息(IP、端口、服务名称等 )登记上去,这个过程叫服务注册。可以理解成商家把自己店铺地址、经营业务(服务名称对应业务)登记到 “商业目录平台”(注册中心 )。
    • 发现阶段:服务消费者启动时,会去注册中心查询自己需要调用的服务(通过服务名称找 ),注册中心就把对应服务提供者的地址信息列表返回给消费者,这就是服务发现。好比消费者去 “商业目录平台” 查 “咖啡店”(服务名称),平台返回全城咖啡店地址,消费者就拿到了要调用的服务地址。
  • 如果有多个服务提供者,消费者该如何选择
    • 注册与发现组件一般会配合负载均衡机制来处理。当注册中心返回多个服务提供者地址后:
      • 客户端侧负载均衡(如 Ribbon ):消费者本地就有负载均衡策略(轮询、随机、权重等 ),拿到地址列表后,按策略选一个。比如轮询就是依次选,这次选第一个,下次选第二个,循环往复,能让请求均匀分布到多个提供者上,避免单个实例压力过大。
      • 服务端侧负载均衡(如 Nginx 做反向代理时的负载均衡 ):注册中心把地址给消费者,消费者先请求到 Nginx ,Nginx 再按自己配置的策略(如权重、IP 哈希 )选实际的服务提供者地址转发请求。
  • 消费者如何得知服务提供者的健康状态
    • 心跳机制(续约):服务提供者注册后,会定期(比如 Eureka 里默认 30 秒 )给注册中心发 “心跳”(续约请求 ),告诉注册中心 “我还活着,能提供服务”。要是注册中心长时间(Eureka 里默认 90 秒 )没收到某个提供者的心跳,就会把它标记为 “不健康” 或者剔除出可用地址列表。
    • 健康检查:有些组件(如 Consul )除了心跳,还会做更细致的健康检查,比如调用服务提供者暴露的健康检查接口(像 Spring Boot Actuator 的 /actuator/health ),根据返回结果判断实例是否真的能正常提供服务。
    • 状态同步:注册中心会把服务提供者的健康状态同步给消费者。消费者本地缓存的服务地址列表,会跟着注册中心的状态更新,这样消费者就知道哪些提供者是健康可调用的,哪些可能有问题要避开。
image-20250716193647040

搭建 Eureka 服务案例

搭建 Eureka 服务本身

首先引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

在启动类上添加注解标注为 Eureka 服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package edu.software.ergoutree.eruekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer // 标记为 Eureka 服务器
public class EruekaServerApplication {

public static void main(String[] args) {
SpringApplication.run(EruekaServerApplication.class, args);
}
}

进行配置文件的配置

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8761 # Eureka Server 默认端口

eureka:
client:
register-with-eureka: false # 单节点时,自身不注册到注册中心
fetch-registry: false # 单节点时,无需拉取服务列表
service-url:
defaultZone: http://localhost:8761/eureka/ # 注册中心地址
server:
enable-self-preservation: false # 开发环境可关闭自我保护(生产环境建议开启)

然后启动可以看到 Eureka 的服务现在是跑起来了

image-20250717090705408

Erueka 会把自己也注册到服务实例之中

image-20250717091036178

进行服务注册

进行服务注册,把erueka-serverdiscovery注册到Erueka中,需要引入依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

启动类添加服务发现注解,但是貌似Spring Cloud 2020+ 后就可省略

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class EruekaServerdiscoveryApplication {

public static void main(String[] args) {
SpringApplication.run(EruekaServerdiscoveryApplication.class, args);
}

}

配置配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring.application.name=erueka-serverdiscovery
server.port=8081

# Eureka客户端配置
# 注册到Eureka服务器
eureka.client.register-with-eureka=true
# 从Eureka服务器获取注册信息
eureka.client.fetch-registry=true
# Eureka服务器地址
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

# 实例ID
eureka.instance.instance-id=${spring.application.name}:${server.port}
# 优先使用IP地址注册
eureka.instance.prefer-ip-address=true
# 心跳间隔(默认30秒)
eureka.instance.lease-renewal-interval-in-seconds=30
# 服务失效时间(默认90秒)
eureka.instance.lease-expiration-duration-in-seconds=90

可以看到我们的服务被成功的注册到里面了

image-20250717092121776

写了一个控制器,来看看效果

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
@RestController
@RequestMapping("/api")
public class ServiceProviderController {

@Value("${spring.application.name}")
private String applicationName;

@Value("${server.port}")
private String serverPort;

/**
* 获取服务实例信息
*/
@GetMapping("/instance-info")
public Map<String, Object> getInstanceInfo() {
Map<String, Object> info = new HashMap<>();
info.put("applicationName", applicationName);
info.put("port", serverPort);
info.put("timestamp", System.currentTimeMillis());
return info;
}

/**
* 生成随机ID
*/
@GetMapping("/generate-id")
public Map<String, String> generateId() {
Map<String, String> result = new HashMap<>();
result.put("id", UUID.randomUUID().toString());
result.put("from", applicationName + ":" + serverPort);
return result;
}

/**
* 健康检查接口
*/
@GetMapping("/health")
public Map<String, String> health() {
Map<String, String> health = new HashMap<>();
health.put("status", "UP");
health.put("service", applicationName);
health.put("port", serverPort);
return health;
}
}
image-20250717092300989
image-20250717092441402

进行服务发现

在 erueka-servicefind 模块,实现服务拉取,基于服务名称获取服务列表,然后对服务列表做负载均衡

首先上来还是添加必要的依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改主应用类,添加Eureka客户端注解:

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class EruekaServicefindApplication {
public static void main(String[] args) {
SpringApplication.run(EruekaServicefindApplication.class, args);
}
}

配置application.properties:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring.application.name=erueka-servicefind
server.port=8082

# Eureka客户端配置
# 是否向Eureka服务器注册自己
eureka.client.register-with-eureka=true
# 从Eureka服务器获取注册信息
eureka.client.fetch-registry=true
# Eureka服务器地址
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

# 实例ID
eureka.instance.instance-id=${spring.application.name}:${server.port}
# 优先使用IP地址注册
eureka.instance.prefer-ip-address=true

创建配置类,注册RestTemplate并启用负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class RestTemplateConfig {

/**
* 创建一个支持负载均衡的RestTemplate
* 使用@LoadBalanced注解,可以直接通过服务名调用服务
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

创建服务发现控制器:

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
/**
* 服务发现控制器
* 专注于从Eureka服务器拉取服务列表
*/
@RestController
@RequestMapping("/service-find")
public class ServiceFindController {

@Autowired
private DiscoveryClient discoveryClient;

/**
* 获取所有服务列表
*/
@GetMapping("/services")
public List<String> getAllServices() {
return discoveryClient.getServices();
}

/**
* 获取指定服务的所有实例
*/
@GetMapping("/services/{serviceId}")
public List<Map<String, Object>> getServiceInstances(@PathVariable String serviceId) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);

return instances.stream()
.map(instance -> {
Map<String, Object> info = new HashMap<>();
info.put("serviceId", instance.getServiceId());
info.put("host", instance.getHost());
info.put("port", instance.getPort());
info.put("uri", instance.getUri().toString());
info.put("metadata", instance.getMetadata());
return info;
})
.collect(Collectors.toList());
}

/**
* 获取服务实例数量
*/
@GetMapping("/count/{serviceId}")
public Map<String, Object> getServiceInstanceCount(@PathVariable String serviceId) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);

Map<String, Object> result = new HashMap<>();
result.put("serviceId", serviceId);
result.put("instanceCount", instances.size());

return result;
}

/**
* 获取所有服务及其实例数量
*/
@GetMapping("/services-with-count")
public Map<String, Integer> getAllServicesWithCount() {
List<String> services = discoveryClient.getServices();

Map<String, Integer> result = new HashMap<>();
for (String service : services) {
result.put(service, discoveryClient.getInstances(service).size());
}

return result;
}
}

创建负载均衡服务调用控制器,手动实现负载均衡:

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
/**
* 负载均衡控制器
* 演示如何使用LoadBalancerClient进行负载均衡
*/
@RestController
@RequestMapping("/loadbalancer")
public class LoadBalancerController {

@Autowired
private LoadBalancerClient loadBalancerClient;

private final RestTemplate simpleRestTemplate = new RestTemplate();

/**
* 使用LoadBalancerClient选择服务实例
* 这是客户端负载均衡的核心API
*/
@GetMapping("/choose/{serviceId}")
public Map<String, Object> chooseService(@PathVariable String serviceId) {
// 从Eureka获取服务实例,内部会进行负载均衡
ServiceInstance instance = loadBalancerClient.choose(serviceId);

if (instance == null) {
Map<String, Object> error = new HashMap<>();
error.put("error", "Service " + serviceId + " not found or no instances available");
return error;
}

Map<String, Object> result = new HashMap<>();
result.put("serviceId", instance.getServiceId());
result.put("host", instance.getHost());
result.put("port", instance.getPort());
result.put("uri", instance.getUri().toString());

return result;
}

/**
* 使用LoadBalancerClient调用服务
* 手动实现负载均衡调用
*/
@GetMapping("/execute/{serviceId}/{path}")
public Object executeWithLoadBalancer(@PathVariable String serviceId, @PathVariable String path) {
// 从Eureka获取服务实例,内部会进行负载均衡
ServiceInstance instance = loadBalancerClient.choose(serviceId);

if (instance == null) {
Map<String, String> error = new HashMap<>();
error.put("error", "Service " + serviceId + " not found or no instances available");
return error;
}

// 构建完整的URL
String url = instance.getUri().toString() + "/" + path;

// 调用服务
return simpleRestTemplate.getForObject(url, Object.class);
}

/**
* 演示多次调用同一服务,观察负载均衡效果
* 默认使用轮询策略
*/
@GetMapping("/multi-choose/{serviceId}/{count}")
public Map<String, Object> multiChooseService(
@PathVariable String serviceId,
@PathVariable int count) {

Map<String, Object> result = new HashMap<>();
Map<String, Integer> instanceStats = new HashMap<>();

for (int i = 0; i < count; i++) {
ServiceInstance instance = loadBalancerClient.choose(serviceId);
if (instance == null) {
continue;
}

String key = instance.getHost() + ":" + instance.getPort();
instanceStats.put(key, instanceStats.getOrDefault(key, 0) + 1);
}

result.put("serviceId", serviceId);
result.put("totalRequests", count);
result.put("instanceDistribution", instanceStats);

return result;
}
}

创建使用@LoadBalanced RestTemplate的控制器,自动实现负载均衡

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
/**
* Ribbon负载均衡控制器
* 演示如何使用@LoadBalanced RestTemplate进行服务调用
*/
@RestController
@RequestMapping("/ribbon")
public class RibbonController {

@Autowired
private RestTemplate restTemplate;

/**
* 使用@LoadBalanced RestTemplate调用服务
* 可以直接使用服务名作为URL的一部分
*/
@GetMapping("/call/{serviceId}/{path}")
public Object callService(@PathVariable String serviceId, @PathVariable String path) {
// 直接使用服务名调用,不需要关心实例的具体地址
// Ribbon会负责负载均衡
String url = "http://" + serviceId + "/" + path;
return restTemplate.getForObject(url, Object.class);
}

/**
* 多次调用同一服务,观察负载均衡效果
*/
@GetMapping("/multi-call/{serviceId}/{path}/{count}")
public Map<String, Object> multiCallService(
@PathVariable String serviceId,
@PathVariable String path,
@PathVariable int count) {

Map<String, Object> result = new HashMap<>();
result.put("serviceId", serviceId);
result.put("path", path);
result.put("count", count);

Map<String, Object> responses = new HashMap<>();

for (int i = 0; i < count; i++) {
String url = "http://" + serviceId + "/api/instance-info";
Object response = restTemplate.getForObject(url, Object.class);
responses.put("call-" + i, response);
}

result.put("responses", responses);
return result;
}

/**
* 调用指定服务的实例信息API
*/
@GetMapping("/instance-info/{serviceId}")
public Object getInstanceInfo(@PathVariable String serviceId) {
String url = "http://" + serviceId + "/api/instance-info";
return restTemplate.getForObject(url, Object.class);
}
}

此时,我们已经完成了erueka-servicefind模块的配置,将其打造为专注于服务拉取和负载均衡的模块。启动后发现已经顺利注册到 Erueka 中

image-20250717094854601

调用一下我们之前写的 api 接口,来测试服务发现和负载均衡

服务发现测试:

  1. 获取所有服务列表,这会返回所有注册到Eureka的服务名称列表。

    http://localhost:8082/service-find/services

    image-20250717095301520
  2. 获取指定服务的所有实例:

    http://localhost:8082/service-find/services/erueka-server

    http://localhost:8082/service-find/services/erueka-serverdiscovery

    image-20250717095242857
  3. 获取服务的实例数量

    http://localhost:8082/service-find/count/erueka-serverdiscovery

  4. 获取所有服务及其实例数量

    http://localhost:8082/service-find/services-with-count

负载均衡测试

  1. 使用LoadBalancerClient选择服务实例

    http://localhost:8082/loadbalancer/choose/erueka-serverdiscovery

    这会使用负载均衡算法选择一个服务实例并返回其信息。

    image-20250717095935541
  2. 使用LoadBalancerClient调用服务

    http://localhost:8082/loadbalancer/execute/erueka-serverdiscovery/api/instance-info

    这会使用负载均衡调用指定服务的API并返回结果。

  3. 多次调用观察负载均衡效果

    http://localhost:8082/loadbalancer/multi-choose/erueka-serverdiscovery/10

    这会连续调用10次,可以观察到负载均衡的分布情况。

    image-20250717095955561
  4. 使用@LoadBalanced RestTemplate调用服务

    http://localhost:8082/ribbon/call/erueka-serverdiscovery/api/instance-info

    这会使用Ribbon负载均衡调用服务。

  5. 使用@LoadBalanced RestTemplate多次调用

    http://localhost:8082/ribbon/multi-call/erueka-serverdiscovery/api/instance-info/5

    这会使用Ribbon连续调用5次,可以观察负载均衡效果。

为了更好地观察负载均衡效果,其实可以启动多个erueka-serverdiscovery实例,通过修改端口号来实现:

启动第一个实例(端口8081),修改application.properties中的端口为8083,再启动第二个实例,然后使用上面的负载均衡测试URL,观察请求是如何分配到不同实例的

这样你就能清楚地看到负载均衡的效果,请求会被分发到不同的服务实例上。

我懒得搞了,但是我知道可以这样整,反正都是重复工作。