1. 概述
本文将深入解析 Eureka 的自我保护(Self Preservation)和续约(Renewal)机制。
我们会先搭建一个 Eureka 服务端,并启动多个客户端实例注册到服务端。通过模拟网络异常场景,直观展示 Eureka 是如何通过自我保护机制避免误删健康实例的。
这套机制在微服务架构中非常关键,尤其是在网络抖动频繁的生产环境中,理解它能帮你避免服务“假死”或“误摘除”的踩坑问题。
2. Eureka 自我保护机制原理
在聊自我保护前,先回顾下 Eureka 服务端是如何管理客户端注册信息的。
- ✅ 注册(Registration):客户端启动时会通过 REST 接口向 Eureka 服务端发起注册,把自己的 IP、端口等信息写入注册表。
- ✅ 注销(De-registration):正常关闭时,客户端会主动调用接口从注册表中移除自己。
- ✅ 续约(Renewal):为应对非正常关闭(如进程崩溃、网络中断),客户端会周期性发送心跳(heartbeat)给服务端,证明自己还“活着”。
⚠️ 如果服务端在一段时间内收不到某个实例的心跳,就会认为该实例已失效,并将其从注册表中驱逐(evict)。
而自我保护机制的作用就是:当服务端发现短时间内大量实例心跳丢失时,会触发保护模式,暂停驱逐操作,防止在网络分区等异常情况下误删大量其实仍健康的实例。
简单粗暴地说:
“宁可错放一千,不可错杀一个。”
一旦进入自我保护模式,Eureka 会保留这些“失联”但可能仍存活的实例,直到心跳恢复到正常水平。
3. 搭建 Eureka 服务端
首先创建一个 Eureka 服务端,使用 @EnableEurekaServer
注解启用服务注册中心功能:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
接着配置 application.yml
或 application.properties
,关键配置如下:
eureka.client.registerWithEureka=false
eureka.client.fetchRegistry=false
eureka.instance.hostname=localhost
解释一下这几个配置:
registerWithEureka=false
:服务端不把自己当作客户端注册到自己,避免自循环。fetchRegistry=false
:不从其他节点拉取注册表(单机模式下不需要)。hostname=localhost
:本地调试必须设置,否则可能因主机名解析问题导致副本状态异常,影响心跳统计。
3.1 自我保护相关配置项
Eureka 默认开启自我保护。以下是核心配置及其作用:
配置项 | 默认值 | 说明 |
---|---|---|
eureka.server.enable-self-preservation |
true |
是否启用自我保护 |
eureka.server.expected-client-renewal-interval-seconds |
30 |
期望的客户端续约间隔(秒) |
eureka.instance.lease-expiration-duration-in-seconds |
90 |
多久没收到心跳就认为实例过期 |
eureka.server.eviction-interval-timer-in-ms |
60000 |
驱逐任务执行频率(毫秒) |
eureka.server.renewal-percent-threshold |
0.85 |
心跳阈值百分比 |
eureka.server.renewal-threshold-update-interval-ms |
900000 (15分钟) |
更新心跳阈值的频率 |
✅ 重点说明:
服务端会根据当前注册的客户端数量,结合 renewal-percent-threshold
计算出每分钟应收到的心跳次数下限。如果实际收到的心跳低于这个阈值,就会触发自我保护。
例如:6 个客户端 × 每 30 秒一次心跳 = 每分钟理论 12 次心跳
按 85% 阈值计算 → 实际要求 ≥ 10.2 ≈ 11 次/分钟
一旦实际续约数低于 11,保护模式立即激活。
⚠️ 修改这些参数需格外谨慎,否则可能导致:
- 阈值计算错误
- 保护模式延迟触发或不触发
- 健康实例被误删
4. 注册多个客户端实例
接下来创建 Eureka 客户端应用:
@SpringBootApplication
@EnableEurekaClient
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
}
客户端配置如下:
spring.application.name=eureka-client
server.port=${PORT:0}
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka
eureka.instance.preferIpAddress=true
eureka.instance.lease-renewal-interval-in-seconds=30
关键点说明:
server.port=${PORT:0}
:使用随机端口或通过启动参数指定,便于启动多个实例。preferIpAddress=true
:注册时显示 IP 而非主机名,避免 DNS 解析问题。lease-renewal-interval-in-seconds=30
:每 30 秒发送一次心跳。
我们启动 6 个实例,端口分别为 8081
到 8086
,访问 http://localhost:8761 查看注册情况。
如图所示,6 个实例均已注册,Renewal threshold(续约阈值)为 11,符合我们之前的计算。
5. 测试自我保护机制
为了模拟网络故障或非正常关机,我们在客户端配置中加入:
eureka.client.should-unregister-on-shutdown=false
这个配置的作用是:关闭时不通知服务端,让服务端误以为发生了崩溃或网络中断。
现在我们手动停止其中一个实例(比如 8081),等待 90 秒(即 lease-expiration-duration-in-seconds
的设定值)。
再次访问 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.**”
- ✅ 原本停掉的实例仍然存在,状态变为
DOWN
这说明:
🔐 Eureka 已进入自我保护模式,暂停了对失效实例的驱逐。
如何退出自我保护模式?
有两种方式:
- ✅ 恢复心跳:重新启动被关闭的客户端实例,使其恢复续约。
- ❌ 关闭自我保护(不推荐):设置
eureka.server.enable-self-preservation=false
,这样服务端会严格按照超时规则驱逐实例。
如果你关闭了自我保护,再重复上述操作,你会发现那个停掉的实例会在 90 秒后自动从注册表消失 —— 这在真实网络抖动场景下是非常危险的行为。
6. 总结
Eureka 的自我保护机制是一种容错设计,旨在应对临时性网络问题,防止大规模服务误摘除。
核心要点回顾:
- ✅ 客户端通过心跳续约,服务端根据阈值判断是否进入保护模式
- ✅ 默认阈值为 85%,可配置
- ✅ 触发后不再驱逐“失联”实例,直到心跳恢复正常
- ✅ 生产环境建议保持开启,避免雪崩式服务下线
所有示例代码已上传至 GitHub:
👉 https://github.com/techgeek/spring-cloud-eureka-self-preservation