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 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: 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.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
eureka.instance.instance-id=${spring.application.name}:${server.port}
eureka.instance.prefer-ip-address=true
eureka.instance.lease-renewal-interval-in-seconds=30
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; }
@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.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
eureka.instance.instance-id=${spring.application.name}:${server.port}
eureka.instance.prefer-ip-address=true
|
创建配置类,注册RestTemplate并启用负载均衡
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Configuration public class RestTemplateConfig {
@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
|
@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
|
@RestController @RequestMapping("/loadbalancer") public class LoadBalancerController {
@Autowired private LoadBalancerClient loadBalancerClient;
private final RestTemplate simpleRestTemplate = new RestTemplate();
@GetMapping("/choose/{serviceId}") public Map<String, Object> chooseService(@PathVariable String serviceId) { 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; }
@GetMapping("/execute/{serviceId}/{path}") public Object executeWithLoadBalancer(@PathVariable String serviceId, @PathVariable String path) { 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; } 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
|
@RestController @RequestMapping("/ribbon") public class RibbonController {
@Autowired private RestTemplate restTemplate;
@GetMapping("/call/{serviceId}/{path}") public Object callService(@PathVariable String serviceId, @PathVariable String path) { 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; }
@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 接口,来测试服务发现和负载均衡
服务发现测试:
获取所有服务列表,这会返回所有注册到Eureka的服务名称列表。
http://localhost:8082/service-find/services
image-20250717095301520
获取指定服务的所有实例:
http://localhost:8082/service-find/services/erueka-server
http://localhost:8082/service-find/services/erueka-serverdiscovery
image-20250717095242857
获取服务的实例数量
http://localhost:8082/service-find/count/erueka-serverdiscovery
获取所有服务及其实例数量
http://localhost:8082/service-find/services-with-count
负载均衡测试
使用LoadBalancerClient选择服务实例
http://localhost:8082/loadbalancer/choose/erueka-serverdiscovery
这会使用负载均衡算法选择一个服务实例并返回其信息。
image-20250717095935541
使用LoadBalancerClient调用服务
http://localhost:8082/loadbalancer/execute/erueka-serverdiscovery/api/instance-info
这会使用负载均衡调用指定服务的API并返回结果。
多次调用观察负载均衡效果
http://localhost:8082/loadbalancer/multi-choose/erueka-serverdiscovery/10
这会连续调用10次,可以观察到负载均衡的分布情况。
image-20250717095955561
使用@LoadBalanced RestTemplate调用服务
http://localhost:8082/ribbon/call/erueka-serverdiscovery/api/instance-info
这会使用Ribbon负载均衡调用服务。
使用@LoadBalanced RestTemplate多次调用
http://localhost:8082/ribbon/multi-call/erueka-serverdiscovery/api/instance-info/5
这会使用Ribbon连续调用5次,可以观察负载均衡效果。
为了更好地观察负载均衡效果,其实可以启动多个erueka-serverdiscovery实例,通过修改端口号来实现:
启动第一个实例(端口8081),修改application.properties中的端口为8083,再启动第二个实例,然后使用上面的负载均衡测试URL,观察请求是如何分配到不同实例的
这样你就能清楚地看到负载均衡的效果,请求会被分发到不同的服务实例上。
我懒得搞了,但是我知道可以这样整,反正都是重复工作。