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.ymlapplication.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 个实例,端口分别为 80818086,访问 http://localhost:8761 查看注册情况。

Eureka Registered Instances 1

如图所示,6 个实例均已注册,Renewal threshold(续约阈值)为 11,符合我们之前的计算。


5. 测试自我保护机制

为了模拟网络故障或非正常关机,我们在客户端配置中加入:

eureka.client.should-unregister-on-shutdown=false

这个配置的作用是:关闭时不通知服务端,让服务端误以为发生了崩溃或网络中断。

现在我们手动停止其中一个实例(比如 8081),等待 90 秒(即 lease-expiration-duration-in-seconds 的设定值)。

再次访问 Eureka 控制台:

Eureka Self Preserve Mode 2

可以看到:

  • ❌ 页面顶部红色警告:“**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 已进入自我保护模式,暂停了对失效实例的驱逐。

如何退出自我保护模式?

有两种方式:

  1. 恢复心跳:重新启动被关闭的客户端实例,使其恢复续约。
  2. 关闭自我保护(不推荐):设置 eureka.server.enable-self-preservation=false,这样服务端会严格按照超时规则驱逐实例。

如果你关闭了自我保护,再重复上述操作,你会发现那个停掉的实例会在 90 秒后自动从注册表消失 —— 这在真实网络抖动场景下是非常危险的行为。


6. 总结

Eureka 的自我保护机制是一种容错设计,旨在应对临时性网络问题,防止大规模服务误摘除。

核心要点回顾:

  • ✅ 客户端通过心跳续约,服务端根据阈值判断是否进入保护模式
  • ✅ 默认阈值为 85%,可配置
  • ✅ 触发后不再驱逐“失联”实例,直到心跳恢复正常
  • ✅ 生产环境建议保持开启,避免雪崩式服务下线

所有示例代码已上传至 GitHub:

👉 https://github.com/techgeek/spring-cloud-eureka-self-preservation


原始标题:Guide to Eureka Self Preservation and Renewal