服务注册与发现
服务注册与发现是微服务架构的核心基础设施。本章详细介绍 Nacos 服务发现的工作原理和使用方法。
服务发现概述
在传统的单体应用架构中,服务之间的调用通常通过硬编码的地址或配置文件来实现。但在微服务架构中,服务实例的数量和位置是动态变化的,服务的部署、扩缩容、故障恢复都会导致服务地址的变化。这种场景下,传统的配置方式无法满足需求,必须引入服务发现机制。
为什么需要服务发现
服务发现机制解决了以下核心问题:
- 动态寻址:服务实例可能随时上线或下线,消费者需要能够动态感知服务地址的变化
- 负载均衡:同一服务通常部署多个实例,消费者需要在多个实例间合理分配请求
- 故障隔离:当某个服务实例出现故障时,消费者需要能够自动避开故障实例
- 弹性伸缩:服务可以根据负载动态扩缩容,服务发现机制需要能够及时感知实例变化
服务发现的核心职责
一个完整的服务发现机制需要承担以下职责:
| 职责 | 说明 |
|---|---|
| 服务注册 | 服务启动时向注册中心注册自己的网络地址(IP 和端口) |
| 服务发现 | 服务消费者从注册中心获取可用的服务提供者地址列表 |
| 健康检查 | 定期检测服务实例的健康状态,剔除不健康的实例 |
| 服务订阅 | 服务消费者订阅服务变化,当服务实例变化时收到通知 |
| 元数据管理 | 管理服务的附加信息,如版本、权重、标签等 |
Nacos 服务发现模式
Nacos 支持两种服务发现模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| DNS 模式 | 通过 DNS 解析获取服务地址,将服务名解析为 IP 地址列表 | 传统应用、Kubernetes 集成、非 Java 应用 |
| RPC 模式 | 通过 HTTP/gRPC 接口获取服务地址,支持实时订阅服务变化 | Spring Cloud、Dubbo 应用、需要实时感知的场景 |
DNS 模式的优势在于兼容性好,任何支持 DNS 解析的应用都可以使用。但 DNS 模式存在缓存延迟,无法实时感知服务变化。RPC 模式则通过长连接实现了实时推送,服务变化可以在毫秒级推送给消费者。
服务注册
服务注册是服务发现的第一步。当服务提供者启动时,需要向 Nacos 注册自己的地址信息,这样服务消费者才能发现并调用它。
服务注册流程
服务注册的完整流程如下:
- 服务启动:服务提供者应用启动
- 读取配置:读取 Nacos 服务器地址、命名空间等配置
- 建立连接:客户端与 Nacos Server 建立 gRPC 长连接(2.x 版本)
- 发送注册请求:向 Nacos 发送服务注册请求,包含服务名、IP、端口、元数据等信息
- 数据存储:Nacos 存储服务实例信息(临时实例存内存,持久实例存数据库)
- 数据同步:Nacos 集群节点间同步服务信息(Distro 协议)
- 通知订阅者:Nacos 通知订阅该服务的消费者
临时实例 vs 持久实例
Nacos 区分两种类型的服务实例,它们在健康检查、数据存储、适用场景等方面都有明显区别:
| 特性 | 临时实例(Ephemeral) | 持久实例(Persistent) |
|---|---|---|
| 健康检查方式 | 客户端主动发送心跳 | 服务端主动探测 |
| 实例剔除策略 | 心跳超时自动剔除 | 不会自动剔除 |
| 数据存储方式 | 内存存储,优先性能 | 持久化存储,优先可靠性 |
| 一致性协议 | Distro 协议(AP) | Raft 协议(CP) |
| 适用场景 | 微服务实例,频繁上下线 | 数据库、中间件等基础设施 |
| 实例数量 | 通常较多,动态变化 | 通常较少,相对固定 |
选择建议:
-
选择临时实例:如果你的服务是微服务架构中的业务服务,实例会随着部署、扩缩容频繁变化,推荐使用临时实例。临时实例的健康检查由客户端负责,Nacos 只需要接收心跳即可,性能更高。
-
选择持久实例:如果你的服务是需要长期稳定运行的基础设施服务(如数据库、Redis、消息队列等),推荐使用持久实例。持久实例即使没有心跳也不会被剔除,适合服务实例相对固定、需要保证数据一致性的场景。
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 |
服务发现
服务发现是服务消费者获取服务提供者地址的过程。消费者通过服务发现机制,可以动态获取可用的服务实例列表,并进行负载均衡调用。
服务发现原理
Nacos 的服务发现采用客户端发现模式,具体工作原理如下:
- 服务查询:消费者启动时,向 Nacos 查询目标服务的实例列表
- 本地缓存:消费者将服务实例列表缓存到本地,后续调用优先使用缓存
- 服务订阅:消费者向 Nacos 订阅目标服务的变化事件
- 变更通知:当服务实例发生变化时(上线、下线、健康状态变化),Nacos 主动推送变更通知
- 缓存更新:消费者收到通知后更新本地缓存
这种模式的优势在于:
- 减少网络开销:消费者优先使用本地缓存,减少了对 Nacos 的请求压力
- 实时性强:通过订阅机制,服务变化可以毫秒级推送给消费者
- 容错性好:即使 Nacos 短暂不可用,消费者仍可使用本地缓存继续工作
服务发现流程图
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
- 心跳线程被阻塞