目标 :掌握服务注册与发现的原理和实践
学习时长:1~2 周
目录
- 服务注册发现原理
- [Eureka 详解](#Eureka 详解)
- [Eureka Server 搭建](#Eureka Server 搭建)
- [Eureka Client 注册](#Eureka Client 注册)
- [Nacos 详解](#Nacos 详解)
- [Nacos 集成](#Nacos 集成)
- [Eureka vs Nacos 对比](#Eureka vs Nacos 对比)
- 服务实例元数据
- 健康检查机制
- 负载均衡策略
- 面试高频题
1. 服务注册发现原理
服务提供者启动
↓
向注册中心注册(服务名、IP、端口、健康状态)
↓
定期发送心跳(默认30秒)
↓
注册中心维护服务列表(健康检查,超时剔除)
服务消费者:
↓
从注册中心拉取服务列表(本地缓存)
↓
根据负载均衡策略选择实例
↓
发起 HTTP 调用
核心问题:为什么需要注册中心?
❌ 硬编码方式(无注册中心):
userService.call("http://192.168.1.100:9001/api/users")
→ 服务器 IP 变化或增减实例时,所有调用方都需要修改代码!
✅ 注册中心方式:
userFeignClient.getUserById(id) // 通过服务名调用
→ 注册中心动态维护实例列表,消费者无感知 IP 变化
2. Eureka 详解
核心架构
┌─────────────────────────────────────┐
│ Eureka Server │ ← 注册中心
│ Registry: { │
│ "user-service": [ │
│ {ip:"192.168.1.1", port:9001} │
│ {ip:"192.168.1.2", port:9001} │ ← 两个实例(水平扩展)
│ ] │
│ } │
└─────────────────────────────────────┘
↑ 注册/心跳 ↓ 拉取列表
┌─────────────┐ ┌─────────────────────┐
│ user-service │ │ order-service │
│ (提供者) │ │ (消费者,Feign调用) │
└─────────────┘ └─────────────────────┘
自我保护机制
Eureka Server 收到的心跳数低于阈值(默认85%)时,进入自我保护模式:
- 不会剔除任何实例
- 防止网络分区时误剔除健康实例
- 生产环境应开启 ,开发环境建议关闭(快速感知实例下线)
yaml
eureka:
server:
enable-self-preservation: false # 开发环境关闭
eviction-interval-timer-in-ms: 5000 # 剔除间隔缩短至5秒
AP 还是 CP?
Eureka 是 AP 系统:
- 优先保证可用性(即使注册信息不是最新的)
- 牺牲强一致性(不同 Server 节点间的数据可能短暂不一致)
- 适合:对最终一致性容忍,追求高可用
3. Eureka Server 搭建
xml
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
java
@SpringBootApplication
@EnableEurekaServer // 核心注解,开启 Eureka 服务端
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
yaml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false # 自身不注册
fetch-registry: false # 自身不拉取
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
Eureka Server 高可用(Peer to Peer)
yaml
# eureka-server-1 (端口 8761)
eureka:
client:
service-url:
defaultZone: http://localhost:8762/eureka/ # 指向 Server-2
# eureka-server-2 (端口 8762)
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/ # 指向 Server-1
# 客户端同时连接两个(任一宕机不影响)
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
4. Eureka Client 注册
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
Spring Cloud 2020 以后 :不再需要
@EnableDiscoveryClient或@EnableEurekaClient,引入依赖后自动开启
yaml
spring:
application:
name: user-service # 必须设置:注册到 Eureka 的服务名
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
registry-fetch-interval-seconds: 5 # 拉取注册表间隔(默认30秒)
instance:
prefer-ip-address: true # 注册 IP 而非主机名
lease-renewal-interval-in-seconds: 5 # 心跳间隔(默认30秒)
lease-expiration-duration-in-seconds: 15 # 超时剔除(默认90秒)
instance-id: ${spring.application.name}:${server.port} # 实例唯一ID
5. Nacos 详解
Nacos(Naming and Configuration Service)是阿里巴巴开源的注册中心 + 配置中心一体化组件。
Nacos 核心特性
| 特性 | 说明 |
|---|---|
| 服务注册/发现 | 支持 HTTP、DNS、gRPC |
| 配置管理 | 动态配置,支持热更新 |
| 健康检查 | 主动(TCP/HTTP)+ 被动(心跳) |
| 命名空间 | 隔离不同环境(dev/test/prod) |
| 集群分组 | 同一服务按地域/机房分组 |
| 权重配置 | 控制流量比例(灰度发布) |
| 保护阈值 | 类似 Eureka 自我保护 |
Nacos 健康检查模式
临时实例(ephemeral=true,默认):
- 客户端主动发心跳(15秒),5秒无心跳标记不健康,30秒剔除
- 适合:微服务实例
永久实例(ephemeral=false):
- Server 主动探测健康状态(TCP/HTTP)
- 即使实例不健康,不会自动剔除(需手动删除)
- 适合:物理服务器、数据库等基础设施
Nacos 支持 CP + AP 切换
bash
# 切换为 CP(强一致):适合需要强一致的永久实例
curl -X PUT 'http://nacos:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
# 切换为 AP(高可用):适合临时实例(默认)
curl -X PUT 'http://nacos:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=AP'
6. Nacos 集成
启动 Nacos Server
bash
# 单机模式(开发/测试)
docker run -d -p 8848:8848 -p 9848:9848 \
-e MODE=standalone \
nacos/nacos-server:v2.3.0
# 访问控制台:http://localhost:8848/nacos
# 默认账号密码:nacos/nacos
引入依赖
xml
<!-- 使用 Spring Cloud Alibaba BOM -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
服务配置
yaml
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 # Nacos 地址
namespace: dev-namespace-id # 命名空间(环境隔离)
group: DEFAULT_GROUP # 服务分组
cluster-name: SH # 集群名(同城优先调用)
# 权重(0~1,影响负载均衡流量比例)
weight: 1.0
# 元数据(可被其他服务读取)
metadata:
version: v1.0
region: shanghai
7. Eureka vs Nacos 对比
| 维度 | Eureka | Nacos |
|---|---|---|
| 语言 | Java | Java |
| CAP | AP | AP(临时)/ CP(永久) |
| 配置中心 | ❌(需单独部署 Config Server) | ✅ 内置 |
| 控制台 | 简陋 | 功能丰富 |
| 主动健康检查 | ❌ 仅被动心跳 | ✅ TCP/HTTP/MySQL |
| 命名空间 | ❌ | ✅ |
| 权重 | ❌ | ✅ |
| 社区活跃度 | 较低(Netflix 已停维护) | 高(阿里持续维护) |
| 国内使用 | 一般 | 主流 |
推荐 :新项目使用 Nacos;已有 Eureka 项目切换成本较低(配置相似)。
8. 服务实例元数据
yaml
# 在 eureka.instance.metadata-map 或 nacos.discovery.metadata 中配置
eureka:
instance:
metadata-map:
version: "v1.2"
zone: "shanghai"
weight: "100"
java
// 读取其他服务的元数据
@Autowired
private DiscoveryClient discoveryClient;
public void printMetadata() {
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
instances.forEach(inst ->
System.out.println("元数据: " + inst.getMetadata()));
}
9. 健康检查机制
Spring Boot Actuator 集成
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
yaml
management:
endpoints:
web:
exposure:
include: health, info
endpoint:
health:
show-details: always
自定义健康检查
java
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
// 检查数据库连接
if (canConnectToDatabase()) {
return Health.up()
.withDetail("database", "连接正常")
.withDetail("activeConnections", getActiveConnections())
.build();
} else {
return Health.down().withDetail("error", "数据库连接失败").build();
}
} catch (Exception e) {
return Health.down(e).build();
}
}
}
10. 负载均衡策略
Spring Cloud LoadBalancer(替代 Ribbon)默认提供两种策略:
java
// 1. 轮询(Round Robin,默认)
// 请求按顺序轮流发给每个实例
// 2. 随机(Random)
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
LoadBalancerClientFactory factory,
@Value("${spring.application.name}") String name) {
return new RandomLoadBalancer(
factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
// 3. 自定义策略(权重)
@Bean
public ReactorLoadBalancer<ServiceInstance> weightedLoadBalancer(...) {
// 根据元数据中的 weight 字段分配流量
}
11. 面试高频题
Q1:Eureka 的自我保护机制是什么?
当 Eureka Server 接收到的心跳数低于期望值的 85% 时,进入自我保护模式,停止剔除实例。这是为了防止网络故障导致大量实例被误剔除。生产应开启,开发可关闭以快速感知服务下线。
Q2:Eureka、Zookeeper、Nacos 的 CAP 模型分别是什么?
Eureka:AP(可用性优先,不保证强一致);Zookeeper:CP(一致性优先,Leader 选举期间不可用);Nacos:支持 AP + CP 切换(临时实例 AP,永久实例 CP)。
Q3:服务注册到 Eureka 后,消费者如何发现它?
消费者启动时向 Eureka Server 拉取服务列表并本地缓存(默认30秒刷新)。Feign + LoadBalancer 在调用时从本地缓存中根据负载均衡策略选择实例。
Q4:Nacos 和 Eureka 的心跳机制有什么区别?
Eureka 仅支持客户端主动心跳(被动检测);Nacos 除客户端心跳外,还支持 Server 主动探测(TCP/HTTP),且对临时实例和永久实例有不同的处理策略。
Q5:如何实现服务的优雅下线?
①调用
/actuator/shutdown端点;②发送DELETE请求到 Eureka 注销;③在 Kubernetes 中通过 PreStop Hook 延迟关闭;④Nacos 支持在控制台手动下线。
12. 专家级:Nacos 注册中心生产运维
知识点 1:服务注册失败排查清单
text
症状:服务启动后 Nacos 控制台看不到实例
排查步骤:
1. 检查 Nacos 地址是否正确(server-addr 格式:host:port,不加 http://)
2. 检查网络连通性:curl http://nacos-host:8848/nacos/actuator/health
3. 检查 Nacos 2.x 的 gRPC 端口 9848 是否开放(防火墙/安全组)
4. 检查 namespace ID 是否使用了 UUID 格式(不是 namespace 名称)
5. 检查日志:nacos.core.auth.enabled=true 时需要配置 username/password
6. 开启 Nacos 客户端日志:logging.level.com.alibaba.nacos=DEBUG
知识点 2:Nacos 服务实例保护阈值
yaml
# Nacos 控制台可设置服务保护阈值(0~1)
# 当健康实例比例低于此值时,即使实例不健康也对外提供服务
# 目的:防止过多不健康实例同时被摘除,导致健康实例压力爆炸
# 建议值:0.8(80%健康实例才正常摘除不健康的)
spring:
cloud:
nacos:
discovery:
# 在代码中注册时设置(或在控制台服务详情中设置)
protected-threshold: 0.0 # 默认0,即不保护
与 Eureka 自我保护的区别:
- Eureka 自我保护是注册中心级别(整个 Eureka Server 停止剔除)
- Nacos 保护阈值是服务级别(只针对某一个服务)
知识点 3:同城双活/异地多活部署
text
单机房架构(有单点风险):
上海 Nacos 集群 ← 所有服务
同城双机房(推荐):
上海A机房:Nacos-1, Nacos-2 ← 上海A的服务
上海B机房:Nacos-3 ← 上海B的服务
三节点组成同一 Nacos 集群,Raft 保证一致性
跨城部署注意事项:
- Raft 需要节点间低延迟通信,跨城延迟高不适合同一集群
- 推荐:每个城市独立 Nacos 集群,通过服务同步插件同步数据
- 或使用 Nacos 企业版的异地多活特性
13. 专家级:Spring Cloud LoadBalancer 源码级理解
知识点:本地缓存的刷新机制
Spring Cloud LoadBalancer 维护一份本地服务实例缓存,避免每次调用都查询注册中心。
text
缓存刷新流程:
1. 首次调用某服务时,从 Nacos/Eureka 拉取实例列表 → 写入本地缓存
2. 后续调用直接读缓存(高性能)
3. 缓存默认30秒TTL,到期后重新从注册中心拉取
配置调整:
yaml
spring:
cloud:
loadbalancer:
cache:
enabled: true
ttl: 30s # 缓存TTL(默认35秒)
capacity: 256 # 最大缓存服务数
生产注意事项:
text
问题:新实例注册后,最多30秒后才被 LoadBalancer 感知(缓存延迟)
下线实例最多30秒内仍可能收到请求(导致调用失败)
缓解方案:
1. 缩短缓存TTL(如10秒),但增加注册中心压力
2. Nacos 推送 + 缓存失效联动(Nacos 2.x 通过 gRPC 推送变更)
3. 结合 Feign Retry(调用失败时自动重试另一实例)
Feign 重试配置:
yaml
spring:
cloud:
openfeign:
client:
config:
default:
retryer: feign.Retryer.Default # 默认重试(5次,最大1秒间隔)
14. Nacos 集群生产部署
知识点 1:Nacos 集群架构原理
text
Nacos 集群(至少3节点,Raft 协议要求奇数):
┌─────────────┐
│ Nginx │ ← 负载均衡(VIP)
│ :80 / :443 │
└──────┬──────┘
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ nacos-1 │ │ nacos-2 │ │ nacos-3 │
│ :8848 │ │ :8848 │ │ :8848 │
│ :9848(gRPC) │ │ :9848(gRPC) │ │ :9848(gRPC) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
└────────────────┼────────────────┘
▼
┌──────────────┐
│ MySQL │ ← 共享数据存储
│ nacos_db │
└──────────────┘
Raft 协议保证:
- 3节点最多容忍1个节点宕机
- 5节点最多容忍2个节点宕机
- Leader 处理写请求,Follower 可以处理读请求
知识点 2:Nacos 集群配置文件
properties
# conf/cluster.conf(三台机器的 IP 和端口)
192.168.1.10:8848
192.168.1.11:8848
192.168.1.12:8848
properties
# conf/application.properties(MySQL 持久化)
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://mysql:3306/nacos_config?characterEncoding=utf8&serverTimezone=UTC
db.user.0=nacos
db.password.0=nacos123
# 开启认证(生产必须!)
nacos.core.auth.enabled=true
nacos.core.auth.plugin.nacos.token.secret.key=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkxMjM0NTY=
# 注意:secret.key 必须 Base64 编码,且原文长度 >= 32 字符
bash
# Docker Compose 集群示例
version: '3.8'
services:
nacos-1:
image: nacos/nacos-server:v2.3.0
environment:
- NACOS_SERVERS=nacos-1:8848 nacos-2:8848 nacos-3:8848
- SPRING_DATASOURCE_PLATFORM=mysql
- MYSQL_SERVICE_HOST=mysql
- MYSQL_SERVICE_DB_NAME=nacos_config
- MYSQL_SERVICE_USER=nacos
- MYSQL_SERVICE_PASSWORD=nacos123
- NACOS_AUTH_ENABLE=true
volumes:
- ./logs/nacos-1:/home/nacos/logs
nacos-2:
image: nacos/nacos-server:v2.3.0
environment:
- NACOS_SERVERS=nacos-1:8848 nacos-2:8848 nacos-3:8848
# ... 同 nacos-1
nacos-3:
image: nacos/nacos-server:v2.3.0
environment:
- NACOS_SERVERS=nacos-1:8848 nacos-2:8848 nacos-3:8848
# ... 同 nacos-1
15. 同区域优先路由(Zone Affinity)
知识点 1:为什么需要同区域路由
text
问题:多机房部署时,跨机房调用增加网络延迟
上海A机房 user-service → 深圳B机房 order-service(高延迟)
上海A机房 user-service → 上海A机房 order-service(低延迟)
解决:配置 zone,优先选择同 zone 的实例
服务注册时标注自己所在的 zone
LoadBalancer 优先选择与自己 zone 相同的实例
同 zone 无实例时,再跨 zone 调用(降级)
知识点 2:Zone Affinity 完整配置
yaml
# 每个服务配置自己的 zone(在对应机房的配置文件中)
spring:
cloud:
nacos:
discovery:
cluster-name: shanghai-a # Nacos 集群分组
metadata:
zone: shanghai-a # 自定义 zone 标签(LoadBalancer 读取)
loadbalancer:
zone: ${spring.cloud.nacos.discovery.metadata.zone} # 告诉 LB 自己的 zone
configurations: zone-preference # 启用 zone 感知路由
java
// 自定义 Zone 感知负载均衡器(Spring Cloud LB 内置支持,也可自定义)
@Configuration
@LoadBalancerClients(defaultConfiguration = ZoneAwareConfig.class)
public class GlobalLoadBalancerConfig {}
@Configuration
public class ZoneAwareConfig {
@Bean
public ServiceInstanceListSupplier zonePreferenceServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withZonePreference() // 启用 zone 优先
.withHealthChecks()
.build(context);
}
}
16. 服务优雅下线
知识点:避免下线时请求失败
text
问题:直接 kill 进程时
1. 注册中心尚未感知实例下线(有缓存延迟)
2. 正在处理的请求被强制中断
→ 客户端收到连接错误
正确的下线流程:
1. 先从注册中心注销(主动下线)
2. 等待 LoadBalancer 缓存刷新(等几秒)
3. 再等待进行中的请求处理完成
4. 最后停止进程
yaml
# 开启优雅停机
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 最多等 30 秒
bash
# Kubernetes 中配置 preStop Hook
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
# 1. 调用 actuator 主动注销
curl -X POST http://localhost:8080/actuator/service-registry?status=OUT_OF_SERVICE
# 2. 等待 LB 缓存刷新
sleep 10
# 3. Spring Boot 优雅停机会等待进行中的请求完成
# 同时配置 terminationGracePeriodSeconds > preStop 等待时间 + 应用关闭时间
spec:
terminationGracePeriodSeconds: 60
17. 自定义权重负载均衡器(完整实现)
知识点:基于 Nacos 权重的负载均衡
java
/**
* 基于 Nacos 元数据 weight 字段的加权随机负载均衡器
* 实例 weight 越高,被选中的概率越大
*/
public class NacosWeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Log log = LogFactory.getLog(NacosWeightedLoadBalancer.class);
private final String serviceId;
private final ObjectProvider<ServiceInstanceListSupplier> supplierProvider;
public NacosWeightedLoadBalancer(String serviceId,
ObjectProvider<ServiceInstanceListSupplier> supplierProvider) {
this.serviceId = serviceId;
this.supplierProvider = supplierProvider;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = supplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map(this::selectByWeight);
}
private Response<ServiceInstance> selectByWeight(List<ServiceInstance> instances) {
if (instances.isEmpty()) return new EmptyResponse();
// 计算总权重
int totalWeight = instances.stream()
.mapToInt(i -> {
String w = i.getMetadata().getOrDefault("weight", "1");
return Math.max(1, Integer.parseInt(w));
})
.sum();
// 加权随机
int random = ThreadLocalRandom.current().nextInt(totalWeight);
int cumulative = 0;
for (ServiceInstance instance : instances) {
int weight = Integer.parseInt(
instance.getMetadata().getOrDefault("weight", "1"));
cumulative += weight;
if (random < cumulative) {
log.debug("选中实例: " + instance.getHost() + ":" + instance.getPort()
+ " (weight=" + weight + ")");
return new DefaultResponse(instance);
}
}
return new DefaultResponse(instances.get(0)); // 兜底
}
}
// 注册为默认负载均衡器
@Configuration
@LoadBalancerClients(defaultConfiguration = WeightedLBConfig.class)
public class WeightedLBAutoConfig {}
@Configuration
public class WeightedLBConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> nacosWeightedLoadBalancer(
LoadBalancerClientFactory factory,
Environment env) {
String name = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new NacosWeightedLoadBalancer(name,
factory.getLazyProvider(name, ServiceInstanceListSupplier.class));
}
}
Nacos 控制台设置实例权重(不修改代码,直接在控制台调整):
- 灰度:新版本实例权重设为 5,旧版本 95(流量 5:95)
- 扩容:新加入的实例权重从 10 慢慢调到 100(预热)