服务注册与发现
服务注册与发现是微服务架构的核心基础设施。本章详细介绍 Nacos 服务发现的工作原理和使用方法。
服务发现概述
在微服务架构中,服务实例的数量和位置是动态变化的。服务发现机制解决了以下问题:
- 服务注册:服务启动时向注册中心注册自己的地址
- 服务发现:服务消费者从注册中心获取服务提供者的地址列表
- 健康检查:检测服务实例是否健康,剔除不健康的实例
- 负载均衡:在多个服务实例之间分配请求
Nacos 服务发现模式
Nacos 支持两种服务发现模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| DNS 模式 | 通过 DNS 解析获取服务地址 | 传统应用、Kubernetes 集成 |
| RPC 模式 | 通过 HTTP/gRPC 接口获取服务地址 | Spring Cloud、Dubbo 应用 |
服务注册
临时实例 vs 持久实例
Nacos 区分两种类型的服务实例:
| 特性 | 临时实例 | 持久实例 |
|---|---|---|
| 健康检查方式 | 客户端心跳 | 服务端主动探测 |
| 实例剔除 | 心跳超时自动剔除 | 不会自动剔除 |
| 存储方式 | 内存优先 | 持久化存储 |
| 适用场景 | 微服务实例 | 数据库、中间件等基础设施 |
临时实例适合微服务场景,实例会随着服务启停动态变化。持久实例适合需要长期稳定存在的服务。
Spring Cloud 服务注册
1. 添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2. 配置文件
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: dev
group: DEFAULT_GROUP
# 注册为临时实例(默认 true)
ephemeral: true
# 服务权重(0-100)
weight: 1
# 服务元数据
metadata:
version: 1.0.0
region: cn-east
3. 启动服务
添加 @EnableDiscoveryClient 注解启用服务发现:
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
服务启动后会自动向 Nacos 注册。
手动注册服务
使用 Nacos Open API 手动注册服务:
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance' \
-d 'serviceName=user-service' \
-d 'ip=192.168.1.100' \
-d 'port=8080' \
-d 'weight=1.0' \
-d 'ephemeral=true' \
-d 'metadata={"version":"1.0.0"}'
注册参数说明
| 参数 | 说明 | 默认值 |
|---|---|---|
| serviceName | 服务名 | 必填 |
| ip | 实例 IP | 必填 |
| port | 实例端口 | 必填 |
| weight | 权重(0.01-10000) | 1.0 |
| enabled | 是否启用 | true |
| healthy | 是否健康 | true |
| ephemeral | 是否临时实例 | true |
| metadata | 元数据 | 无 |
| clusterName | 集群名 | DEFAULT |
| groupName | 分组名 | DEFAULT_GROUP |
服务发现
Spring Cloud 服务发现
1. 使用 RestTemplate
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public User getUser(Long userId) {
// 使用服务名代替具体地址
String url = "http://user-service/users/" + userId;
return restTemplate.getForObject(url, User.class);
}
}
@LoadBalanced 注解使 RestTemplate 具备负载均衡能力,会自动从 Nacos 获取服务实例列表。
2. 使用 OpenFeign
// 定义 Feign 客户端
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable("id") Long id);
}
// 使用 Feign 客户端
@Service
public class OrderService {
@Autowired
private UserClient userClient;
public User getUser(Long userId) {
return userClient.getUser(userId);
}
}
手动发现服务
使用 Nacos Open API 查询服务实例:
# 查询服务实例列表
curl 'http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=user-service'
# 查询指定实例
curl 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=user-service&ip=192.168.1.100&port=8080'
使用 NamingService API
@Service
public class NacosService {
@Autowired
private NamingService namingService;
// 注册服务
public void registerInstance() throws NacosException {
namingService.registerInstance("user-service", "192.168.1.100", 8080);
}
// 获取服务实例列表
public List<Instance> getInstances() throws NacosException {
return namingService.selectInstances("user-service", true);
}
// 获取一个健康实例
public Instance selectOneHealthyInstance() throws NacosException {
return namingService.selectOneHealthyInstance("user-service");
}
// 订阅服务变化
public void subscribe() throws NacosException {
namingService.subscribe("user-service", new EventListener() {
@Override
public void onEvent(Event event) {
if (event instanceof NamingEvent) {
NamingEvent namingEvent = (NamingEvent) event;
System.out.println("服务实例变化: " + namingEvent.getInstances());
}
}
});
}
}
健康检查
临时实例健康检查
临时实例采用客户端心跳模式:
┌──────────────┐ ┌──────────────┐
│ 服务实例 │ │ Nacos │
│ (客户端) │ │ Server │
└──────────────┘ └──────────────┘
│ │
│ 心跳请求 (每 5 秒) │
│ ─────────────────────────────────>│
│ │
│ 心跳响应 │
│<───────────────────────────────── │
│ │
│ │ 15 秒无心跳
│ │ → 标记为不健康
│ │
│ │ 30 秒无心跳
│ │ → 剔除实例
心跳时间配置:
spring:
cloud:
nacos:
discovery:
# 心跳间隔(毫秒)
heart-beat-interval: 5000
# 心跳超时时间(毫秒)
heart-beat-timeout: 15000
# IP 删除超时时间(毫秒)
ip-delete-timeout: 30000
持久实例健康检查
持久实例由 Nacos Server 主动探测,支持多种探测方式:
TCP 探测
# 注册持久实例时指定健康检查类型
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance' \
-d 'serviceName=mysql-service' \
-d 'ip=192.168.1.200' \
-d 'port=3306' \
-d 'ephemeral=false'
HTTP 探测
# 注册时指定健康检查 URL
curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance' \
-d 'serviceName=web-service' \
-d 'ip=192.168.1.100' \
-d 'port=8080' \
-d 'ephemeral=false' \
-d 'metadata={"preserved.register.source":"SPRING_CLOUD","healthCheckUrl":"/health"}'
自定义健康检查
@RestController
public class HealthController {
@GetMapping("/health")
public Map<String, Object> health() {
Map<String, Object> result = new HashMap<>();
result.put("status", "UP");
// 可以添加更多健康检查逻辑
return result;
}
}
服务订阅
服务消费者可以订阅服务变化,当服务实例发生变化时收到通知:
使用 Spring Cloud
Spring Cloud 默认会订阅服务变化,自动更新本地服务列表缓存。
使用 NamingService
@Component
public class ServiceSubscriber implements ApplicationRunner {
@Autowired
private NamingService namingService;
@Override
public void run(ApplicationArguments args) throws Exception {
// 订阅服务变化
namingService.subscribe("user-service", new EventListener() {
@Override
public void onEvent(Event event) {
NamingEvent namingEvent = (NamingEvent) event;
List<Instance> instances = namingEvent.getInstances();
// 处理服务实例变化
instances.forEach(instance -> {
System.out.println("实例: " + instance.getIp() + ":" + instance.getPort());
});
}
});
}
}
负载均衡
权重配置
通过配置实例权重实现负载均衡:
spring:
cloud:
nacos:
discovery:
weight: 0.5 # 权重值,范围 0.01-10000
或者在控制台手动配置:
- 登录 Nacos 控制台
- 进入「服务管理」->「服务列表」
- 点击服务名进入详情
- 修改实例的权重值
权重越大,被选中的概率越高。可以用于灰度发布,将新版本的权重设置较低,逐步增加。
使用 Ribbon
Spring Cloud Alibaba 默认集成 Ribbon,支持多种负载均衡策略:
user-service:
ribbon:
# 负载均衡策略
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
NacosRule 会优先选择同一集群的实例,并支持权重负载均衡。
使用 Spring Cloud LoadBalancer
Spring Cloud 2020+ 版本推荐使用 Spring Cloud LoadBalancer:
spring:
cloud:
loadbalancer:
ribbon:
enabled: false
nacos:
enabled: true
服务元数据
元数据用于存储服务的附加信息,如版本、区域、环境等:
配置元数据
spring:
cloud:
nacos:
discovery:
metadata:
version: 1.0.0
region: cn-east
env: prod
使用元数据
@Service
public class MetadataService {
@Autowired
private NamingService namingService;
public void processWithMetadata() throws NacosException {
List<Instance> instances = namingService.selectInstances("user-service", true);
for (Instance instance : instances) {
Map<String, String> metadata = instance.getMetadata();
String version = metadata.get("version");
String region = metadata.get("region");
// 根据元数据筛选实例
if ("1.0.0".equals(version) && "cn-east".equals(region)) {
// 处理请求
}
}
}
}
元数据路由
通过元数据实现服务路由,例如按版本路由:
@Configuration
public class LoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> metadataLoadBalancer(
Environment environment, LoadBalancerClientFactory factory) {
String serviceId = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new MetadataLoadBalancer(
factory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
serviceId);
}
}
服务分组与集群
服务分组
通过分组隔离不同业务的服务:
spring:
cloud:
nacos:
discovery:
group: ORDER_GROUP # 订单服务组
集群配置
通过集群实现同机房优先访问:
spring:
cloud:
nacos:
discovery:
cluster-name: SHANGHAI # 上海机房
当消费者和服务提供者在同一集群时,会优先访问同集群的实例。
保护阈值
保护阈值用于防止服务雪崩。当健康实例比例低于阈值时,Nacos 会返回所有实例(包括不健康的),让消费者自行判断。
spring:
cloud:
nacos:
discovery:
protect-threshold: 0.5 # 健康实例比例阈值
设置保护阈值后,即使大部分实例不健康,也能保证部分流量正常访问。
常见问题
1. 服务注册失败
检查以下几点:
- Nacos Server 是否正常运行
- 网络是否连通
- 命名空间和分组配置是否正确
- 查看应用日志是否有错误信息
2. 服务发现不到实例
检查以下几点:
- 服务是否已注册成功
- 命名空间和分组是否一致
- 服务实例是否健康
- 查看控制台服务列表
3. 心跳超时
如果出现心跳超时,可以调整心跳参数:
spring:
cloud:
nacos:
discovery:
heart-beat-interval: 10000 # 增加心跳间隔
heart-beat-timeout: 30000 # 增加超时时间
4. 服务实例频繁上下线
可能原因:
- 网络不稳定
- 服务频繁重启
- JVM 内存不足导致频繁 GC
- 心跳线程被阻塞