概述
上一篇把 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 后面挂着三个问题:
- 换环境就失效。开发、测试、生产三套环境,用户服务的地址各不相同,难道每次上线都改代码重发一次包。
- 多实例没法选 。为了扛并发,
user-service起了 8081、8082、8083 三个实例,写成 8081 那另外两台的存在意义是什么。 - 不知道对方死活。就算地址是对的,怎么知道 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 里。
官方文档
- Netflix Eureka(GitHub)
- Spring Cloud Netflix - Service Discovery: Eureka Clients
- Spring Cloud Netflix - Eureka Server 配置项
总结
- Eureka 结构里只有两类角色:eureka-server 记录和管理,eureka-client 注册与消费。所有微服务都是 client,原因是每个服务都可能反过来对外提供服务。
- 四条机制各管一段:注册 解决「谁在哪」,拉取 解决「怎么找到」,续约 解决「还活着吗」,剔除解决「死了怎么摘掉」。
- 最需要改掉的一个直觉是:消费者不是每次调用都问注册中心,而是本地缓存 + 定期拉取。这决定了感知延迟的存在,也是「控制台看到了但调用不通」这类问题的根源。
- 从实例挂掉到消费者彻底看不见,最坏约 90s + 60s + 30s ≈ 180 秒。要更快的故障转移,方向是调用侧的重试与熔断,不是把心跳调得更密。
- 自我保护是 AP 取舍的必然产物。它保证网络分区时注册表不被清空,代价是消费者可能拿到已宕机实例。生产环境保持默认开启。
- 负载均衡不是 Eureka 干的活。Eureka 只给地址,真正的选实例由 Ribbon 的
IRule完成,这也是@LoadBalanced注解存在的意义。