Java框架 SpringCloud 快速入门: Eureka 注册中心原理分析

概述

上一篇把 Eureka Server 跑起来了,http://127.0.0.1:10086 控制台能刷出实例列表。这一篇不搭环境,只回答一个问题:这个列表是怎么维护起来的,凭什么让 order-service 不写死 IP 也能调通、还能在 user-service 挂掉后自动绕开它。

纲要

  • 问题的起点:硬编码 IP 带来的三个死结(环境变更、多实例、实例健康)
  • 两个角色
    • eureka-server:注册中心,保存「服务名 → 实例地址列表」
    • eureka-client:所有微服务(提供者与消费者),注册、拉取、心跳都在这一端
  • 四条核心机制 (每条一张时序图)
    • 注册 register:启动即上报 name / ip / port
    • 拉取 fetch:每 30s 拉一次列表,缓存在本地
    • 续约 renew:每 30s 一次心跳,证明「我还活着」
    • 剔除 evict:超 90s 无心跳,从列表摘掉
  • 把四条机制串起来:负载均衡为什么能生效的完整时序
  • 自我保护 self-preservation:丢心跳时宁可保留也不误删,副作用与开关
  • 参数表:6 个关键配置的默认值、含义、什么时候该改
  • AP 与 CP:Eureka 是 AP,Zookeeper 是 CP,注册中心为什么选 AP
  • 实战坑:缓存当实时、延迟算错、红色警告误判、改配置不生效

起点:硬编码的 IP 撑不了多久

没上注册中心之前,order-service 里是这么调的。

java 复制代码
// cn.itcast.order.service.OrderService
public Order queryOrderById(Long orderId) {
    Order order = orderMapper.findById(orderId);
    // 地址写死在代码里,换环境就得改代码、重新编译打包
    String url = "http://localhost:8081/user/" + order.getUserId();
    User user = restTemplate.getForObject(url, User.class);
    order.setUser(user);
    return order;
}

这一行 http://localhost:8081 后面挂着三个问题:

  1. 换环境就失效。开发、测试、生产三套环境,用户服务的地址各不相同,难道每次上线都改代码重发一次包。
  2. 多实例没法选 。为了扛并发,user-service 起了 8081、8082、8083 三个实例,写成 8081 那另外两台的存在意义是什么。
  3. 不知道对方死活。就算地址是对的,怎么知道 8081 现在还活着,万一进程刚被 kill 掉。

这三个问题的答案分别是 服务注册 、负载均衡 、心跳与剔除。Eureka 把它们打包成了一整套:前两个靠注册中心,第三个靠心跳。

两个角色:谁记录,谁使用

Eureka 的结构里只有两类角色,记不住它俩的分工,后面所有机制都会讲糊。

角色 对应依赖 / 注解 职责 在案例中的代表
eureka-server(注册中心) spring-cloud-starter-netflix-eureka-server + @EnableEurekaServer 接收注册、保存「服务名 → 实例地址列表」、接收心跳、剔除过期实例、对外提供查询接口 独立模块 eureka-server,端口 10086
eureka-client(服务的提供者与消费者) spring-cloud-starter-netflix-eureka-client 启动时注册自己、定期续约、按服务名拉取列表并缓存 user-service(提供者)、order-service(消费者)

有一句话值得单独记下来:提供者和消费者不是两种依赖,而是同一个 client 的两种使用姿势。 一个微服务今天在被人调用(提供者),明天也可能去调别人(消费者),Eureka 把「注册自己」和「发现别人」统一封装在了 eureka-client 里。所以 order-service 只要将来也对外提供服务,它同样应该注册进去------注意它的 application.yml 里那句 spring.application.name: orderservice 不是可选项,那就是它的注册名。

tree 复制代码
cloud-demo
├── pom.xml                       # 父工程,统一管理 SpringCloud 版本
├── eureka-server                 # 角色一:注册中心
│   ├── pom.xml                   # spring-cloud-starter-netflix-eureka-server
│   └── src/main
│       ├── java/cn/itcast/eureka/EurekaApplication.java   # @EnableEurekaServer
│       └── resources/application.yml                      # port 10086
├── user-service                  # 角色二:提供者(可多实例)
│   ├── pom.xml                   # spring-cloud-starter-netflix-eureka-client
│   └── src/main/resources/application.yml                 # name: userservice
└── order-service                 # 角色二:消费者(同时也可被注册)
    ├── pom.xml                   # spring-cloud-starter-netflix-eureka-client
    └── src/main/resources/application.yml                 # name: orderservice

客户端的注册配置是这一段,两个服务除了 name 之外完全一样。

yaml 复制代码
# user-service/src/main/resources/application.yml
server:
  port: 8081
spring:
  application:
    name: userservice          # 注册到 eureka 的名字,消费者就是按这个名字找它的
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka   # 少写 /eureka 会 404
yaml 复制代码
# order-service/src/main/resources/application.yml
server:
  port: 8080
spring:
  application:
    name: orderservice
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

四条机制,一条一条拆

注册 register

服务启动、Spring 容器刷新完成的那一刻,eureka-client 会向 server 发一个 POST,把自己的信息报上去。报的内容是:服务名(USERSERVICE,注册时会转成大写)、实例 IP、端口、以及一个 instanceId(默认是 ip:服务名:端口,所以同一个 IP 上跑多个实例才区分得开)。
eureka-server (10086) user-service (eureka-client) eureka-server (10086) user-service (eureka-client) #mermaid-svg-olExdnBwlro08Db2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-olExdnBwlro08Db2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-olExdnBwlro08Db2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-olExdnBwlro08Db2 .error-icon{fill:#552222;}#mermaid-svg-olExdnBwlro08Db2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-olExdnBwlro08Db2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-olExdnBwlro08Db2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-olExdnBwlro08Db2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-olExdnBwlro08Db2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-olExdnBwlro08Db2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-olExdnBwlro08Db2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-olExdnBwlro08Db2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-olExdnBwlro08Db2 .marker.cross{stroke:#333333;}#mermaid-svg-olExdnBwlro08Db2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-olExdnBwlro08Db2 p{margin:0;}#mermaid-svg-olExdnBwlro08Db2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-olExdnBwlro08Db2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-olExdnBwlro08Db2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-olExdnBwlro08Db2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-olExdnBwlro08Db2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-olExdnBwlro08Db2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-olExdnBwlro08Db2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-olExdnBwlro08Db2 .sequenceNumber{fill:white;}#mermaid-svg-olExdnBwlro08Db2 #sequencenumber{fill:#333;}#mermaid-svg-olExdnBwlro08Db2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-olExdnBwlro08Db2 .messageText{fill:#333;stroke:none;}#mermaid-svg-olExdnBwlro08Db2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-olExdnBwlro08Db2 .labelText,#mermaid-svg-olExdnBwlro08Db2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-olExdnBwlro08Db2 .loopText,#mermaid-svg-olExdnBwlro08Db2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-olExdnBwlro08Db2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-olExdnBwlro08Db2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-olExdnBwlro08Db2 .noteText,#mermaid-svg-olExdnBwlro08Db2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-olExdnBwlro08Db2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-olExdnBwlro08Db2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-olExdnBwlro08Db2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-olExdnBwlro08Db2 .actorPopupMenu{position:absolute;}#mermaid-svg-olExdnBwlro08Db2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-olExdnBwlro08Db2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-olExdnBwlro08Db2 .actor-man circle,#mermaid-svg-olExdnBwlro08Db2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-olExdnBwlro08Db2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 注册成功后,才进入心跳循环 POST /eureka/apps/USERSERVICE name + ip + port + instanceId 1 写入注册表 USERSERVICE → 8081 实例, 8082 实例 2 204 No Content 3

有个细节值得知道:消费者也会把自己注册进去 。order-service 虽然只调别人,但它同样是 eureka-client,默认 register-with-eureka=true,所以它也会出现在控制台里。这不是 bug,是为将来反转角色准备的。

拉取 fetch

这是最容易被误解的一条。很多人以为「每次调 user-service 就去问一次 Eureka」,实际不是------那样注册中心会成为整个系统的单点瓶颈。

真实做法是:消费者定时把整个列表拉回来,存进自己进程内的本地注册表(EurekaClient 的 localRegionApps)。发起调用时,直接从本地这张表里查服务名对应的实例列表,一次网络请求都不发。
本地注册表 (内存) eureka-server order-service (eureka-client) 本地注册表 (内存) eureka-server order-service (eureka-client) #mermaid-svg-5zMYdnbHbw7mnbX2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5zMYdnbHbw7mnbX2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5zMYdnbHbw7mnbX2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5zMYdnbHbw7mnbX2 .error-icon{fill:#552222;}#mermaid-svg-5zMYdnbHbw7mnbX2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5zMYdnbHbw7mnbX2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5zMYdnbHbw7mnbX2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5zMYdnbHbw7mnbX2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5zMYdnbHbw7mnbX2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5zMYdnbHbw7mnbX2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5zMYdnbHbw7mnbX2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5zMYdnbHbw7mnbX2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5zMYdnbHbw7mnbX2 .marker.cross{stroke:#333333;}#mermaid-svg-5zMYdnbHbw7mnbX2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5zMYdnbHbw7mnbX2 p{margin:0;}#mermaid-svg-5zMYdnbHbw7mnbX2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5zMYdnbHbw7mnbX2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-5zMYdnbHbw7mnbX2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5zMYdnbHbw7mnbX2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-5zMYdnbHbw7mnbX2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-5zMYdnbHbw7mnbX2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-5zMYdnbHbw7mnbX2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-5zMYdnbHbw7mnbX2 .sequenceNumber{fill:white;}#mermaid-svg-5zMYdnbHbw7mnbX2 #sequencenumber{fill:#333;}#mermaid-svg-5zMYdnbHbw7mnbX2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-5zMYdnbHbw7mnbX2 .messageText{fill:#333;stroke:none;}#mermaid-svg-5zMYdnbHbw7mnbX2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5zMYdnbHbw7mnbX2 .labelText,#mermaid-svg-5zMYdnbHbw7mnbX2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-5zMYdnbHbw7mnbX2 .loopText,#mermaid-svg-5zMYdnbHbw7mnbX2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-5zMYdnbHbw7mnbX2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-5zMYdnbHbw7mnbX2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-5zMYdnbHbw7mnbX2 .noteText,#mermaid-svg-5zMYdnbHbw7mnbX2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-5zMYdnbHbw7mnbX2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5zMYdnbHbw7mnbX2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5zMYdnbHbw7mnbX2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-5zMYdnbHbw7mnbX2 .actorPopupMenu{position:absolute;}#mermaid-svg-5zMYdnbHbw7mnbX2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-5zMYdnbHbw7mnbX2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-5zMYdnbHbw7mnbX2 .actor-man circle,#mermaid-svg-5zMYdnbHbw7mnbX2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-5zMYdnbHbw7mnbX2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 每 30 秒 (registry-fetch-interval-seconds) 业务调用时只读 C, 不走网络,所以快 GET /eureka/apps 1 实例列表(JSON) 2 覆盖本地缓存 3 查 userservice → 8081, 8082, 8083 4

这就是为什么服务上线/下线后,消费者感知会有延迟。 延迟上限由拉取间隔决定,默认 30 秒。也就是说,user-service 刚启动完,order-service 最多要等 30 秒才可能拉到它。想缩短,就调小 registry-fetch-interval-seconds,代价是注册中心压力变大。

还有一个更隐蔽的坑:你拉回来的列表不等于你真正用的列表 。Ribbon 在 Eureka 列表之上还有一层自己的 ServerList 缓存,默认也有约 30 秒的刷新周期。调小 Eureka 的拉取间隔但没管 Ribbon,延迟还是下不来。

续约 renew(心跳)

注册完之后,client 进入一个后台定时任务,每 30 秒向 server 发一次 PUT,告诉它「我还在」。这个动作叫续约(renew),口语里叫心跳(heartbeat)。
eureka-server user-service (8081) eureka-server user-service (8081) #mermaid-svg-3XyXFvmj0Nxmm2PC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3XyXFvmj0Nxmm2PC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3XyXFvmj0Nxmm2PC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3XyXFvmj0Nxmm2PC .error-icon{fill:#552222;}#mermaid-svg-3XyXFvmj0Nxmm2PC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3XyXFvmj0Nxmm2PC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3XyXFvmj0Nxmm2PC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3XyXFvmj0Nxmm2PC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3XyXFvmj0Nxmm2PC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3XyXFvmj0Nxmm2PC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3XyXFvmj0Nxmm2PC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3XyXFvmj0Nxmm2PC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3XyXFvmj0Nxmm2PC .marker.cross{stroke:#333333;}#mermaid-svg-3XyXFvmj0Nxmm2PC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3XyXFvmj0Nxmm2PC p{margin:0;}#mermaid-svg-3XyXFvmj0Nxmm2PC .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3XyXFvmj0Nxmm2PC text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-3XyXFvmj0Nxmm2PC .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3XyXFvmj0Nxmm2PC .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-3XyXFvmj0Nxmm2PC .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-3XyXFvmj0Nxmm2PC .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-3XyXFvmj0Nxmm2PC #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-3XyXFvmj0Nxmm2PC .sequenceNumber{fill:white;}#mermaid-svg-3XyXFvmj0Nxmm2PC #sequencenumber{fill:#333;}#mermaid-svg-3XyXFvmj0Nxmm2PC #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-3XyXFvmj0Nxmm2PC .messageText{fill:#333;stroke:none;}#mermaid-svg-3XyXFvmj0Nxmm2PC .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3XyXFvmj0Nxmm2PC .labelText,#mermaid-svg-3XyXFvmj0Nxmm2PC .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-3XyXFvmj0Nxmm2PC .loopText,#mermaid-svg-3XyXFvmj0Nxmm2PC .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-3XyXFvmj0Nxmm2PC .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-3XyXFvmj0Nxmm2PC .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-3XyXFvmj0Nxmm2PC .noteText,#mermaid-svg-3XyXFvmj0Nxmm2PC .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-3XyXFvmj0Nxmm2PC .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3XyXFvmj0Nxmm2PC .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3XyXFvmj0Nxmm2PC .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-3XyXFvmj0Nxmm2PC .actorPopupMenu{position:absolute;}#mermaid-svg-3XyXFvmj0Nxmm2PC .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-3XyXFvmj0Nxmm2PC .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-3XyXFvmj0Nxmm2PC .actor-man circle,#mermaid-svg-3XyXFvmj0Nxmm2PC line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-3XyXFvmj0Nxmm2PC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 每 30 秒 (lease-renewal-interval-in-seconds) 只要 lastUpdateTimestamp 被刷新, 这个实例就算「健康」 PUT /eureka/apps/USERSERVICE/{instanceId} 1 刷新该实例的 lastUpdateTimestamp = now 2 200 OK 3

注意 Eureka 判断健康的方式:它不主动去 ping 实例,而是被动的------你来续约就算活着,不来就算了。这一点跟 Nacos 对非临时实例做主动探测不一样。

剔除 evict

Server 端另有一个定时任务 EvictionTask,按固定周期扫一遍注册表,把「太久没来续约」的实例删掉。
order-service 注册表 EvictionTask (每 60s 跑一次) order-service 注册表 EvictionTask (每 60s 跑一次) #mermaid-svg-JodjnFpcqEJ4EcPW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JodjnFpcqEJ4EcPW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JodjnFpcqEJ4EcPW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JodjnFpcqEJ4EcPW .error-icon{fill:#552222;}#mermaid-svg-JodjnFpcqEJ4EcPW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JodjnFpcqEJ4EcPW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JodjnFpcqEJ4EcPW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JodjnFpcqEJ4EcPW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JodjnFpcqEJ4EcPW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JodjnFpcqEJ4EcPW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JodjnFpcqEJ4EcPW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JodjnFpcqEJ4EcPW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JodjnFpcqEJ4EcPW .marker.cross{stroke:#333333;}#mermaid-svg-JodjnFpcqEJ4EcPW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JodjnFpcqEJ4EcPW p{margin:0;}#mermaid-svg-JodjnFpcqEJ4EcPW .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JodjnFpcqEJ4EcPW text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-JodjnFpcqEJ4EcPW .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JodjnFpcqEJ4EcPW .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JodjnFpcqEJ4EcPW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JodjnFpcqEJ4EcPW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JodjnFpcqEJ4EcPW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JodjnFpcqEJ4EcPW .sequenceNumber{fill:white;}#mermaid-svg-JodjnFpcqEJ4EcPW #sequencenumber{fill:#333;}#mermaid-svg-JodjnFpcqEJ4EcPW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JodjnFpcqEJ4EcPW .messageText{fill:#333;stroke:none;}#mermaid-svg-JodjnFpcqEJ4EcPW .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JodjnFpcqEJ4EcPW .labelText,#mermaid-svg-JodjnFpcqEJ4EcPW .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-JodjnFpcqEJ4EcPW .loopText,#mermaid-svg-JodjnFpcqEJ4EcPW .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-JodjnFpcqEJ4EcPW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-JodjnFpcqEJ4EcPW .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JodjnFpcqEJ4EcPW .noteText,#mermaid-svg-JodjnFpcqEJ4EcPW .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-JodjnFpcqEJ4EcPW .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JodjnFpcqEJ4EcPW .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JodjnFpcqEJ4EcPW .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-JodjnFpcqEJ4EcPW .actorPopupMenu{position:absolute;}#mermaid-svg-JodjnFpcqEJ4EcPW .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JodjnFpcqEJ4EcPW .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-JodjnFpcqEJ4EcPW .actor-man circle,#mermaid-svg-JodjnFpcqEJ4EcPW line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-JodjnFpcqEJ4EcPW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 8082 的 lastUpdate = 90 秒前 now - lastUpdate > 90s 扫描 USERSERVICE 的实例列表 1 移除 8082 2 下一次 fetch 3 8081, 8083 不含 8082 4

到这里能算出一笔很实用的账:从实例真挂掉,到消费者彻底看不见它,最坏情况要多久?

阶段 耗时 说明
心跳过期判定 90s lease-expiration-duration-in-seconds
EvictionTask 扫描周期 最多 60s eureka.server.eviction-interval-timer-in-ms 默认 60000
消费者下一次拉取 最多 30s registry-fetch-interval-seconds
合计 最多约 180s 心跳 30s 那次恰好错过扫描窗口时更接近这个值

三分钟。这个数字很关键------别拿它当「秒级故障转移」用。如果业务要求更快,得走别的路(Nacos 的消息推送,或者客户端侧的熔断降级),靠缩小心跳间隔是治标,缩短心跳会给注册中心带来成倍的 QPS 压力。

四条机制串起来:负载均衡为什么能生效

把上面四条按时间顺序摆在一起,就是 Eureka 解决开头三个问题的完整答案。
order-service:8080 eureka-server:10086 user-service:8082 user-service:8081 order-service:8080 eureka-server:10086 user-service:8082 user-service:8081 #mermaid-svg-tZ5JRLfGBqQh2iTn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tZ5JRLfGBqQh2iTn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tZ5JRLfGBqQh2iTn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tZ5JRLfGBqQh2iTn .error-icon{fill:#552222;}#mermaid-svg-tZ5JRLfGBqQh2iTn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tZ5JRLfGBqQh2iTn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tZ5JRLfGBqQh2iTn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tZ5JRLfGBqQh2iTn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tZ5JRLfGBqQh2iTn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tZ5JRLfGBqQh2iTn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tZ5JRLfGBqQh2iTn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tZ5JRLfGBqQh2iTn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tZ5JRLfGBqQh2iTn .marker.cross{stroke:#333333;}#mermaid-svg-tZ5JRLfGBqQh2iTn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tZ5JRLfGBqQh2iTn p{margin:0;}#mermaid-svg-tZ5JRLfGBqQh2iTn .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tZ5JRLfGBqQh2iTn text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-tZ5JRLfGBqQh2iTn .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tZ5JRLfGBqQh2iTn .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-tZ5JRLfGBqQh2iTn .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-tZ5JRLfGBqQh2iTn .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-tZ5JRLfGBqQh2iTn #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-tZ5JRLfGBqQh2iTn .sequenceNumber{fill:white;}#mermaid-svg-tZ5JRLfGBqQh2iTn #sequencenumber{fill:#333;}#mermaid-svg-tZ5JRLfGBqQh2iTn #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-tZ5JRLfGBqQh2iTn .messageText{fill:#333;stroke:none;}#mermaid-svg-tZ5JRLfGBqQh2iTn .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tZ5JRLfGBqQh2iTn .labelText,#mermaid-svg-tZ5JRLfGBqQh2iTn .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-tZ5JRLfGBqQh2iTn .loopText,#mermaid-svg-tZ5JRLfGBqQh2iTn .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-tZ5JRLfGBqQh2iTn .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tZ5JRLfGBqQh2iTn .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-tZ5JRLfGBqQh2iTn .noteText,#mermaid-svg-tZ5JRLfGBqQh2iTn .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-tZ5JRLfGBqQh2iTn .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tZ5JRLfGBqQh2iTn .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tZ5JRLfGBqQh2iTn .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tZ5JRLfGBqQh2iTn .actorPopupMenu{position:absolute;}#mermaid-svg-tZ5JRLfGBqQh2iTn .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-tZ5JRLfGBqQh2iTn .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tZ5JRLfGBqQh2iTn .actor-man circle,#mermaid-svg-tZ5JRLfGBqQh2iTn line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-tZ5JRLfGBqQh2iTn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 存进本地注册表 loop 每 30s ⑥ 8082 进程被 kill,心跳停了 ① 启动,注册 8081 1 ① 启动,注册 8082 2 ② 启动后拉取列表,拿到 8081, 8082 3 userservice → 8081, 8082 4 ③ 心跳续约 5 ③ 心跳续约 6 ④ 调用前,负载均衡从本地列表挑一台 → 8081 7 ⑤ GET http://userservice/user/1 8 ⑦ 90s 过期 + 扫描命中,剔除 8082 9 ⑧ 下一次拉取 10 userservice → 8081 11 ⑨ 负载均衡只剩一个候选,全部命中 8081 12

第④步就是负载均衡生效的位置,值得说透:负载均衡跟 Eureka 是两件事。 Eureka 只负责把「8081、8082 这两个地址」交给消费者,至于从两个里挑哪一个,是 LoadBalancerInterceptor 拦下 RestTemplate 的请求后,交给 Ribbon 的 IRule 策略去算的(默认 ZoneAvoidanceRule 轮询式)。

java 复制代码
// order-service 启动类:只加一个 @LoadBalanced,其余都是框架在干
package cn.itcast.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class OrderApplication {

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

    /**
     * @LoadBalanced 让 RestTemplate 具备拦截能力:
     * 请求发出前把服务名 userservice 替换成真实 ip:port
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
java 复制代码
// cn.itcast.order.service.OrderService:URL 里写服务名,不再写 ip:port
public Order queryOrderById(Long orderId) {
    Order order = orderMapper.findById(orderId);
    // userservice 是注册到 eureka 的服务名,由 @LoadBalanced 的拦截器负责还原成真实地址
    String url = "http://userservice/user/" + order.getUserId();
    User user = restTemplate.getForObject(url, User.class);
    order.setUser(user);
    return order;
}

第⑤步之后,请求落到的是 http://localhost:8081/user/1。第⑥到⑨步就是「消费者怎么感知提供者状态」的回答:不是消费者主动探活,是它下一次拉取时,列表里已经没有那台坏机器了。 这也是整条链路里唯一的延迟来源。

自我保护:宁可保留,也不误删

如果 EvictionTask 只管闷头剔除,会出一个大问题:网络抖动时,注册中心会把一大批健康的实例全删光。

设想 Eureka Server 和某几台 user-service 之间发生了网络分区------实例本身活得好好的,只是心跳传不过来。默认策略下,这些实例会一个接一个被判定过期、剔除,消费者拿到一张残缺的列表,整个集群跟着崩。这类现象在 Eureka 里有个专有名词,叫级联失败。

自我保护(self-preservation)就是为它设计的:Server 统计过去一分钟实际收到的心跳数,如果低于期望值的 85%,就认为「是我这边网络有问题,而不是服务都挂了」,于是拒绝剔除任何实例。
#mermaid-svg-9he0WGkVjOHzbkes{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9he0WGkVjOHzbkes .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9he0WGkVjOHzbkes .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9he0WGkVjOHzbkes .error-icon{fill:#552222;}#mermaid-svg-9he0WGkVjOHzbkes .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9he0WGkVjOHzbkes .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9he0WGkVjOHzbkes .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9he0WGkVjOHzbkes .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9he0WGkVjOHzbkes .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9he0WGkVjOHzbkes .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9he0WGkVjOHzbkes .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9he0WGkVjOHzbkes .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9he0WGkVjOHzbkes .marker.cross{stroke:#333333;}#mermaid-svg-9he0WGkVjOHzbkes svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9he0WGkVjOHzbkes p{margin:0;}#mermaid-svg-9he0WGkVjOHzbkes .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9he0WGkVjOHzbkes .cluster-label text{fill:#333;}#mermaid-svg-9he0WGkVjOHzbkes .cluster-label span{color:#333;}#mermaid-svg-9he0WGkVjOHzbkes .cluster-label span p{background-color:transparent;}#mermaid-svg-9he0WGkVjOHzbkes .label text,#mermaid-svg-9he0WGkVjOHzbkes span{fill:#333;color:#333;}#mermaid-svg-9he0WGkVjOHzbkes .node rect,#mermaid-svg-9he0WGkVjOHzbkes .node circle,#mermaid-svg-9he0WGkVjOHzbkes .node ellipse,#mermaid-svg-9he0WGkVjOHzbkes .node polygon,#mermaid-svg-9he0WGkVjOHzbkes .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9he0WGkVjOHzbkes .rough-node .label text,#mermaid-svg-9he0WGkVjOHzbkes .node .label text,#mermaid-svg-9he0WGkVjOHzbkes .image-shape .label,#mermaid-svg-9he0WGkVjOHzbkes .icon-shape .label{text-anchor:middle;}#mermaid-svg-9he0WGkVjOHzbkes .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9he0WGkVjOHzbkes .rough-node .label,#mermaid-svg-9he0WGkVjOHzbkes .node .label,#mermaid-svg-9he0WGkVjOHzbkes .image-shape .label,#mermaid-svg-9he0WGkVjOHzbkes .icon-shape .label{text-align:center;}#mermaid-svg-9he0WGkVjOHzbkes .node.clickable{cursor:pointer;}#mermaid-svg-9he0WGkVjOHzbkes .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9he0WGkVjOHzbkes .arrowheadPath{fill:#333333;}#mermaid-svg-9he0WGkVjOHzbkes .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9he0WGkVjOHzbkes .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9he0WGkVjOHzbkes .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9he0WGkVjOHzbkes .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9he0WGkVjOHzbkes .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9he0WGkVjOHzbkes .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9he0WGkVjOHzbkes .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9he0WGkVjOHzbkes .cluster text{fill:#333;}#mermaid-svg-9he0WGkVjOHzbkes .cluster span{color:#333;}#mermaid-svg-9he0WGkVjOHzbkes div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9he0WGkVjOHzbkes .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9he0WGkVjOHzbkes rect.text{fill:none;stroke-width:0;}#mermaid-svg-9he0WGkVjOHzbkes .icon-shape,#mermaid-svg-9he0WGkVjOHzbkes .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9he0WGkVjOHzbkes .icon-shape p,#mermaid-svg-9he0WGkVjOHzbkes .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9he0WGkVjOHzbkes .icon-shape .label rect,#mermaid-svg-9he0WGkVjOHzbkes .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9he0WGkVjOHzbkes .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9he0WGkVjOHzbkes .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9he0WGkVjOHzbkes :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
EvictionTask 触发
过去 1 分钟心跳数

低于期望值的 85% ?
触发自我保护

本轮不剔除任何实例
正常剔除过期实例
控制台出现红色警告

EMERGENCY! EUREKA MAY BE INCORRECTLY

CLAIMING INSTANCES ARE UP
消费者可能拿到已宕机实例

→ 调用报错,由重试/熔断兜底

它保护的是什么:注册表的完整性,避免网络分区导致大面积误删。

副作用是什么 :既然不剔除,那消费者就可能拿到一台真的已经宕机的实例,请求打过去报 Connection refused。Eureka 的取舍很明确------宁可返回一个不可用的地址让调用方失败重试,也不返回空列表。因为对服务发现来说,「有地址但可能不通」比「没有任何地址」后果轻得多:前者是单次请求失败,后者是整条链路找不到服务。

如果确认本地环境没有网络问题,想让它照常剔除,可以关掉。

yaml 复制代码
# eureka-server/src/main/resources/application.yml
server:
  port: 10086
spring:
  application:
    name: eureka-server
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  server:
    enable-self-preservation: false      # 关闭自我保护
    eviction-interval-timer-in-ms: 5000  # 剔除扫描周期从 60s 缩到 5s,方便本地调试

生产环境该不该关?不该。 单机开发、实例数很少的时候关掉无妨(因为实例少,一次挂两台就可能跌破 85% 的阈值,红色警告一直闪很烦)。但生产环境一旦关了自我保护,网络抖动会直接把注册表清空,那是把可用性往下限走。真要快速感知故障,正确做法是把探测做在调用侧(Ribbon 重试、Sentinel 熔断),而不是拆掉注册中心的保护。

参数速览

配置项 默认值 含义 什么时候需要改
eureka.client.register-with-eureka true 是否把自己注册到 eureka-server 只做服务端不提供服务的纯 server 节点设为 false;单机调试想让它别出现在列表里也可关
eureka.client.fetch-registry true 是否从 server 拉取服务列表并缓存 只做提供者、从不调别人的服务可设 false;集群中的 server 必须保留 true,否则无法感知其它节点
eureka.client.service-url.defaultZone 无 server 地址,多个用逗号分隔 换环境、连集群时必须改;路径结尾的 /eureka 不能省
eureka.instance.lease-renewal-interval-in-seconds 30 心跳间隔,多久续约一次 不建议改小,直接放大注册中心 QPS;服务要求快速下线时才酌情调
eureka.instance.lease-expiration-duration-in-seconds 90 租约过期时间,超过多久没心跳算挂 跟心跳间隔联动,官方建议为心跳间隔的 2~3 倍,别设得比心跳间隔还小
eureka.server.enable-self-preservation true 是否开启自我保护 仅本地开发/演示可关,生产保持默认
eureka.client.registry-fetch-interval-seconds 30 消费者拉取列表的间隔 想加快感知可调小,但要同步考虑 Ribbon 自身缓存的刷新周期
eureka.server.eviction-interval-timer-in-ms 60000 server 扫描过期实例的周期 调试时调小观察剔除动作,生产维持默认

AP 与 CP:注册中心为什么选 AP

Eureka 和 Zookeeper 常被拿来对比,本质差异不在功能,在 CAP 里的取舍。

维度 Eureka Zookeeper
CAP 定位 AP,优先保证可用性 CP,优先保证一致性
网络分区时表现 各节点继续提供注册与查询,返回的列表可能不一致 少数派节点停止服务,等待选举出新的 leader
注册表一致性 最终一致,靠定时拉取 + 增量同步对齐 强一致,写操作需要过半确认
节点宕机感知 心跳超时后剔除(默认约 90s 起判) 会话超时后移除临时节点
典型使用场景 服务注册与发现,能容忍短时间拿到旧列表 分布式协调、配置管理、分布式锁

为什么注册中心更适合 AP,推理其实很直接:

  • 服务发现这个场景,可用性比强一致更值钱。分区发生时,如果注册中心直接拒绝服务,那所有消费者瞬间拿不到任何地址,整个系统停摆;而如果只是返回一份稍旧的列表,最坏情况是个别请求打到刚下线的实例上,调用侧重试一次就能自愈。
  • 「列表短时间不一致」的代价是可承受的。前面算过,感知延迟本来就有最多约 180 秒,说明这套机制的设计前提就是容忍陈旧数据。既然旧数据是可接受的,为它牺牲可用性就没有意义。
  • 反过来看 Zookeeper:它的强一致来自 ZAB 协议的过半写机制,分区时少数派不可用。当注册中心自身都可能不可用时,把服务发现交给它,等于给整个系统加了一个更硬的依赖。

所以在 Eureka 的取舍里,自我保护机制和 AP 定位是同一件事的两面:它愿意牺牲「列表的准确性」去换「注册中心一直能应答」。

实战坑

把本地缓存当成实时列表。 最典型的现象是:user-service 明明在控制台里看到了,order-service 调过去还是 Connection refused;或者服务已经停了,还能被调上十几次。这是缓存,不是 bug。排查时第一步永远是在消费者侧确认列表是否已刷新,而不是怀疑注册中心坏了。

心跳间隔和剔除时间算错。 有人以为心跳 30 秒、剔除也是 30 秒,于是把 lease-expiration-duration-in-seconds 设成 30。结果实例偶尔一次 GC 停顿或网络抖动就超过 30 秒没发心跳,被误判为挂掉。这两个参数的关系是「过期时间必须显著大于心跳间隔」,官方建议 2~3 倍,默认的 30 / 90 就是这个比例。别把过期时间设得小于等于心跳间隔。

自我保护触发后控制台飘红就以为出故障。 那行 EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP 是提示,不是报错。想确认是不是误报,看红色警告下面那两行数字------实际收到的心跳数远高于期望值的 85%,就说明网络没问题、确实是实例挂了;实际心跳数低得离谱,那才是网络出了问题。

改了配置没生效。 配置只在启动时读取,改完 defaultZone 或服务名必须重启。另外注册表里是按 ip:服务名:端口 区分的,同一台机器改端口重跑不会被识别成同一个实例,旧的会在 90 秒后被剔除,不会立刻消失。

关掉自我保护后忘了调回。 本地调试时关掉 enable-self-preservation 并缩短了扫描周期,提交代码时顺手带进了生产配置。这类改动一旦上线,注册表会在网络抖动时被清空。建议把这类调试参数单独放在 application-dev.yml 里。

官方文档

总结

  • Eureka 结构里只有两类角色:eureka-server 记录和管理,eureka-client 注册与消费。所有微服务都是 client,原因是每个服务都可能反过来对外提供服务。
  • 四条机制各管一段:注册 解决「谁在哪」,拉取 解决「怎么找到」,续约 解决「还活着吗」,剔除解决「死了怎么摘掉」。
  • 最需要改掉的一个直觉是:消费者不是每次调用都问注册中心,而是本地缓存 + 定期拉取。这决定了感知延迟的存在,也是「控制台看到了但调用不通」这类问题的根源。
  • 从实例挂掉到消费者彻底看不见,最坏约 90s + 60s + 30s ≈ 180 秒。要更快的故障转移,方向是调用侧的重试与熔断,不是把心跳调得更密。
  • 自我保护是 AP 取舍的必然产物。它保证网络分区时注册表不被清空,代价是消费者可能拿到已宕机实例。生产环境保持默认开启。
  • 负载均衡不是 Eureka 干的活。Eureka 只给地址,真正的选实例由 Ribbon 的 IRule 完成,这也是 @LoadBalanced 注解存在的意义。
相关推荐
她说彩礼65万1 小时前
C语言 assert
java·linux·c语言
vx-程序开发1 小时前
【计算机毕设】52744基于微信小程序的民宿智能预订系统
java·spring boot·毕业设计·源码·课程设计·毕设·大作业
Wang's Blog2 小时前
Java框架 SpringCloud 快速入门: Eureka 服务注册
java·spring cloud·eureka
liangshanbo12152 小时前
请求拦截器相关面试题
java·前端·javascript
她说彩礼65万2 小时前
C语言 const的使用
java·服务器·c语言
开开心心就好2 小时前
视频压缩工具推荐,绿色版免安装双击即用
java·前端·人工智能·spring·智能手机·intellij-idea·excel
Wang's Blog2 小时前
Java框架 SpringCloud 快速入门: Eureka 注册中心的搭建
java·spring cloud·eureka
专业程序开发源2 小时前
springboot古城景区管理系统88564-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·php·课程设计
泡茶喝茶写代码2 小时前
A股量化数据工程:从 REST 接口到策略信号(第 7 篇):盈利能力因子:毛利率、净利率与 ROE
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口