Eureka 服务注册与发现
导学
微服务化改造的第一步,白歌遇到了一个看似简单却至关重要的问题:订单服务调用库存服务时,怎么知道库存服务的 IP 和端口?
黄俪的第一反应:"写在配置文件里不就行了?"
# 最初的硬编码方案
inventory:
url: http://192.168.1.100:8081
"那如果库存服务扩容到 3 台机器呢?如果某台机器挂了呢?如果 IP 变了呢?"
黄俪沉默了。
定位与问题场景
在微服务架构中,服务实例的网络地址是动态变化的:
- 弹性伸缩:实例数量随负载变化
- 故障转移:实例可能随时挂掉
- 滚动发布:新老版本共存
硬编码方案在微服务中不可行,这就是服务注册与发现要解决的核心问题。
Eureka 核心原理
架构与角色
Eureka 是 Netflix 开源的服务注册与发现组件,采用 AP 架构(优先保证可用性),包含两个核心角色:
| 角色 | 职责 |
|---|---|
| Eureka Server | 注册中心,维护服务注册表(内存 + 最近心跳时间),集群间通过 Peer to Peer 同步 |
| Eureka Client(服务提供者) | 启动时向 Server 注册,定期发送心跳(默认 30s)续约,关闭时发送下线请求 |
| Eureka Client(服务消费者) | 从 Server 获取注册表并本地缓存,通过服务名选择实例发起调用 |
注册与发现的完整时序
自我保护模式(重要面试考点)
这是 Eureka 最容易被误解的机制。当 Eureka Server 在 15 分钟内收到的实际心跳数低于期望心跳数的 85% 时,Server 会进入自我保护模式:
关键理解:自我保护模式是设计特性而非 BUG。它的设计哲学是"宁可保留一个挂掉的实例,也绝不能误杀健康的实例"。这在网络分区(Network Partition)场景下至关重要——假设因为交换机故障,Eureka Server 暂时收不到客户端的续约,如果此时强制剔除,会导致大量健康实例被误杀。
开发环境建议关闭:
eureka.server.enable-self-preservation=false,避免因为单实例调试被保护模式干扰。
完整示例:飞翔科技电商服务注册
场景描述
飞翔科技电商系统有三个微服务:
feixiang-eureka-server:注册中心feixiang-inventory-service:库存服务(提供者)feixiang-order-service:订单服务(消费者)
操作前后对比:
| 维度 | 引入 Eureka 前 | 引入 Eureka 后 |
|---|---|---|
| 服务地址 | 硬编码在 application.yml | 通过服务名动态发现 |
| 实例变更感知 | 无感知,需手动重启 | 自动感知,30 秒内生效 |
| 故障转移 | 需人工切换 URL | LoadBalancer 自动剔除故障实例 |
步骤一:搭建 Eureka Server
依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
启动类:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
配置(application.yml):
server:
port: 8761
spring:
application:
name: feixiang-eureka-server
eureka:
client:
register-with-eureka: false # Server 不需要注册自己
fetch-registry: false # Server 不需要拉取注册表
server:
enable-self-preservation: false # 开发环境关闭自我保护
步骤二:库存服务注册到 Eureka
依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
启动类:
@SpringBootApplication
@EnableDiscoveryClient
public class InventoryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryServiceApplication.class, args);
}
}
配置:
spring:
application:
name: feixiang-inventory-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
库存服务接口:
@RestController
public class InventoryController {
@GetMapping("/inventory/{productId}")
public Map<String, Object> getStock(@PathVariable Long productId) {
return Map.of(
"productId", productId,
"stock", 100,
"servicePort", 8081
);
}
}
步骤三:订单服务发现并调用库存服务
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
订单控制器——使用服务名替代硬编码 URL:
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
// 引入 Eureka 之前:硬编码 IP
// String url = "http://192.168.1.100:8081/inventory/" + productId;
// 引入 Eureka 之后:使用服务名
@GetMapping("/order/check-stock/{productId}")
public Map<String, Object> checkStock(@PathVariable Long productId) {
String url = "http://FEIXIANG-INVENTORY-SERVICE/inventory/" + productId;
return restTemplate.getForObject(url, Map.class);
}
// 查看所有可用实例(调试用)
@GetMapping("/order/instances")
public List<String> instances() {
return discoveryClient.getInstances("FEIXIANG-INVENTORY-SERVICE")
.stream()
.map(si -> si.getHost() + ":" + si.getPort())
.collect(Collectors.toList());
}
}
步骤四:启动与验证
启动顺序:Eureka Server → 库存服务 → 订单服务
访问 Eureka Dashboard:http://localhost:8761
应看到
FEIXIANG-INVENTORY-SERVICE和FEIXIANG-ORDER-SERVICE均处于 UP 状态。
调用测试:
curl http://localhost:8080/order/check-stock/1001
# 返回:{"productId":1001, "stock":100, "servicePort":8081}
易错场景
1. 服务名大小写问题
Spring Cloud 中服务名默认全大写(FEIXIANG-INVENTORY-SERVICE)。通过 spring.application.name 定义的原始名称可能包含小写和短横线,但在 Eureka 的注册表中会转为大写。调用时使用大写更可靠。
2. 自我保护模式导致"僵尸"实例
开发环境中,关闭一个服务后 Eureka Dashboard 仍显示 UP,这是因为自我保护模式阻止了过期实例的剔除。开发环境务必设置 enable-self-preservation=false。
3. defaultZone 配置错误
这是初学者最高频的错误:
# ❌ 错误:路径缺 /eureka/
eureka.client.service-url.defaultZone: http://localhost:8761
# ✅ 正确
eureka.client.service-url.defaultZone: http://localhost:8761/eureka/
4. 先启动消费端后启动服务端
如果订单服务先于库存服务启动,首次调用会失败。解决方案:在 RestTemplate 上添加 @LoadBalanced 注解,由 Ribbon/LoadBalancer 进行重试。
面试考点
Eureka 和 ZooKeeper 的核心区别?
Eureka 遵循 AP 原则(优先可用性),在发生网络分区时,宁可保留可能已经宕机的节点也不丢失注册信息;ZooKeeper 遵循 CP 原则(优先一致性),在网络分区时,如果 Leader 失联,整个集群会拒绝写操作直到重新选举。Eureka 的设计更适合互联网场景中对服务可用性的要求高于瞬时一致性要求的场合。
Eureka 的自我保护机制原理?什么场景触发?
15 分钟内实际心跳数低于期望值的 85% 时触发。触发后 Eureka Server 不再剔除任何过期实例,防止因网络抖动导致大规模误杀。这是 Eureka 实现 AP(高可用)的关键机制。生产环境不要关闭此特性。
Eureka 集群的数据同步机制?
Eureka Server 之间通过 Peer to Peer(对等复制)同步注册表。每个 Server 节点平等,无主从之分。当 Client 向任意一个 Server 注册时,该 Server 会将变更同步到其他所有节点。这种架构的优点是没有单点故障,缺点是可能存在短暂的数据不一致窗口。
小结
Eureka 解决了微服务之间"如何找到对方"的基础问题。它通过客户端心跳保持注册信息的鲜活,通过自我保护模式在网络异常时保护可用性。但 Eureka 只负责"我能找到谁",至于"选哪个实例来调用",那是下一章客户端负载均衡的话题。