服务注册与 Eureka
注册中心、服务实例、心跳与健康。
服务注册与 Eureka
这一课你将能做到
- 独立搭起一个 Eureka 注册中心,并让 user-service、problem-service 注册上来,在控制台看到实例列表
- 说清注册、续约、剔除、自我保护四件事的触发条件与时间参数
- 把同一个服务起两个实例,体验「一个服务名、多个实例」的扩容模型
为什么要学
上一课留了尾巴:RestClient 里地址写死 http://localhost:8082。生产环境实例 IP 会变:扩容加机器、滚动发布换容器、宕机自动摘除。写死地址的服务一多,运维就崩溃了。
注册中心解决「服务在哪」:实例启动上报,调用方按名查询,宕机自动剔除。Eureka 是 Netflix 系经典实现,虽然新项目里 Nacos/Consul/K8s 更多,但它是理解一切注册中心的最短路径——注册、心跳、续约、剔除这套模型在 Nacos 里一模一样,面试也最爱从 Eureka 问起。
先建立图像
┌────────────────────────────────────────┐
│ Eureka Server:8761 │
│ 服务注册表(内存 Map): │
│ USER-SERVICE → [10.0.0.5:8081, 10.0.0.6:8081]
│ PROBLEM-SERVICE → [10.0.0.7:8082]
└───▲──────────────▲──────────────▲──────┘
注册+心跳30s│ │ │ 拉取注册表(每30s)
┌────┴───┐ ┌──────┴───┐ ┌──────┴─────┐
│ user:1 │ │ user:2 │ │ problem │
└────────┘ └──────────┘ └────────────┘
生命周期:启动注册 → 每 30s 心跳续约 → 90s 无心跳标记剔除 → 优雅下线主动注销
跟着做
步骤 1:搭建 Eureka Server
在父工程下新建模块 eureka-server,pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>cloud-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>eureka-server</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
启动类:
package com.example.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
client:
register-with-eureka: false # 自己是注册中心,不注册自己
fetch-registry: false # 不拉取注册表
server:
enable-self-preservation: false # 本地演示关掉自我保护,便于观察剔除
启动后访问 http://localhost:8761,能看到控制台首页(实例列表暂时为空)。
步骤 2:problem-service 注册上来
给 problem-service 的 pom 加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
server:
port: 8082
spring:
application:
name: problem-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true # 注册 IP 而非主机名,容器环境必配
lease-renewal-interval-in-seconds: 30 # 心跳间隔
lease-expiration-duration-in-seconds: 90 # 判死时间
spring.application.name 就是注册到 Eureka 的服务名(控制台转大写显示为 PROBLEM-SERVICE),别的服务以后按这个名字找它。Spring Cloud 2022+ 无需再写 @EnableDiscoveryClient,类路径上有 client 依赖即自动注册。
重启 problem-service,刷新 Eureka 控制台:PROBLEM-SERVICE 出现在 Application 表中,状态 UP (1)。
步骤 3:一服务多实例
mvn spring-boot:run -pl problem-service -Dspring-boot.run.arguments=--server.port=8083
控制台里 PROBLEM-SERVICE 变成 UP (2),点进去能看到 8082、8083 两个实例。调用方只认服务名,请求具体打给谁由客户端负载均衡决定——这就是水平扩容的全部秘密。
步骤 4:验证「自动发现」
在 user-service 中查注册表(演示用,生产走 Feign):
package com.example.user.controller;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class DiscoveryController {
private final DiscoveryClient discoveryClient;
public DiscoveryController(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
@GetMapping("/discover/problem-service")
public List<String> discover() {
return discoveryClient.getInstances("problem-service").stream()
.map(i -> i.getHost() + ":" + i.getPort())
.toList();
}
}
user-service 同样加上 eureka-client 依赖和 defaultZone 配置。访问 http://localhost:8081/discover/problem-service,返回两个实例地址——调用方不再需要知道任何 IP。
讲清楚要点
- 注册表在内存里:Eureka 用两级 ConcurrentHashMap 存注册表,读写极快;多节点间通过复制同步(Peer Awareness),优先保证可用性(AP 设计)。
- 心跳续约:客户端每 30s 发一次心跳证明「我活着」;90s 收不到就标记过期,由清理任务定期剔除。
- 自我保护模式:当「心跳失联比例超过阈值(默认 15%)」时,Eureka 宁可保留可能已死的数据也不剔除实例,防止网络抖动把健康服务团灭。生产建议开启。
- 缓存有延迟:Eureka 服务端有 readOnly/readWrite 两级响应缓存(30s),客户端本地也缓存注册表(30s),所以注册后最长约 30-60s 才能被其他服务感知,不是 bug。
- 优雅下线:
kill -15 触发 shutdown 钩子主动注销,比等心跳超时快得多;K8s 里要配 preStop 才有等价效果。
- Eureka 2.x 已停更、1.x 维护模式:新项目国内多用 Nacos(注册+配置一体、带控制台),Cloud 原生栈可换 Consul 或 K8s Service DNS;核心模型一致。
你可能会懵的地方
-
注册中心自己挂了怎么办?
客户端本地有全量注册表缓存,注册中心短暂宕机后服务间调用照常(按缓存地址直连),只是新实例无法注册。这就是 Eureka AP 设计的意义——注册中心不是单点故障。
-
Eureka 和 Nacos 怎么选?
一致性语义:Eureka 只有 AP;Nacos 支持临时实例(AP,心跳制)+ 持久实例(CP,Raft)两种模式,且自带配置中心和中文控制台。国内新项目偏 Nacos;学原理用 Eureka 足够,模型一致。
-
为什么查到列表还要「负载均衡」?
注册中心只给地址列表(DiscoveryClient),选哪个实例是调用方(Spring Cloud LoadBalancer)的职责——两层分离,才能让 Feign、Gateway 各自定制策略。
-
prefer-ip-address 什么时候必须开?
容器/K8s 环境。默认注册主机名,跨机器解析不了;注册 IP+端口才能直连。本地多网卡环境若注册了虚拟网卡 IP,用 eureka.instance.ip-address 手工指定。
-
服务下线了为什么还能被调到?
三层缓存叠加:Eureka 响应缓存 30s + 客户端缓存 30s + 负载均衡自身缓存。最多两分钟左右流量才彻底切走,设计重试与灰度发布时要预留这个窗口。
错法对照
| 错法 | 问题 | 正解 |
|---|
| Server 忘配 register-with-eureka: false | 自己注册自己,控制台出现 EUREKA-SERVER | 单机关闭;集群模式下才开启互相注册 |
服务名带下划线 problem_service | 部分组件按主机名规则校验失败 | 用中划线:problem-service |
| 改完实例数立即验证调用方感知 | 缓存未过期,请求仍打旧实例 | 预留 30-60s 窗口,或调低缓存时间 |
| 生产环境关闭自我保护 | 网络抖动时健康实例被团灭剔除 | 保留自我保护,叠加主动健康检查 |
| defaultZone 写死内网 IP 编进镜像 | 环境迁移即失效 | 环境变量 ${EUREKA_URL:...} 注入 |
卡点急救
- 控制台出现红色警告 EMERGENCY:自我保护触发(本地频繁重启导致心跳丢失比例超标)。演示环境加
enable-self-preservation: false,生产保留开启。
- 客户端启动报
Connection refused: localhost:8761:Server 没起,或 defaultZone 路径写错——正确路径必须以 /eureka/ 结尾。
- 注册成功但状态是 DOWN:健康检查未通过。加
spring-boot-starter-actuator 并配置 eureka.client.healthcheck.enabled: true,让 Eureka 用 /health 的结果而非心跳判断状态。
面试常问
-
讲讲 Eureka 的自我保护机制。
Eureka 统计续约占比:若最近收到的续约数低于期望值的 85%,判定出现网络分区或本机网络异常,进入自我保护——不再剔除任何过期实例,注册表只增不减。目的是防止网络抖动团灭健康实例(AP 取舍);代价是可能保留死实例,需要客户端重试/熔断兜底。
-
Eureka、Nacos、Consul 对比?
从三个维度答:①CAP——Eureka 纯 AP;Nacos 临时实例 AP/持久实例 CP(Raft);Consul CP。②功能——Nacos 注册+配置一体带控制台;Consul 有强健康检查和 KV;Eureka 纯注册。③场景——K8s 体系常直接用 Service DNS 免注册中心。
-
服务注册与发现的完整流程?(背到能复述)
①服务启动 → ②注册(上报实例信息)→ ③每 30s 续约心跳 → ④调用方每 30s 拉取注册表到本地缓存 → ⑤90s 无心跳剔除(触发自我保护判断)→ ⑥下线时主动注销。补一句「两层缓存导致 30-60s 感知延迟」是加分点。
自测三问
- 实例从注册到被剔除,完整生命周期经过哪几步?各自时间参数是多少?
- 自我保护模式为什么存在?开着有什么代价?
- 为什么服务注册成功后,调用方不会立刻发现它?
小结一句话
Eureka 是一张自动维护的电话簿:启动登记、心跳保活、超时销号,配合客户端缓存实现「按名字找服务」;理解它就理解了所有注册中心。
延伸