跳到主要内容

服务注册与发现

服务注册与发现是微服务架构的核心基础设施。本章详细介绍 Nacos 服务发现的工作原理和使用方法。

服务发现概述

在传统的单体应用架构中,服务之间的调用通常通过硬编码的地址或配置文件来实现。但在微服务架构中,服务实例的数量和位置是动态变化的,服务的部署、扩缩容、故障恢复都会导致服务地址的变化。这种场景下,传统的配置方式无法满足需求,必须引入服务发现机制。

为什么需要服务发现

服务发现机制解决了以下核心问题:

  • 动态寻址:服务实例可能随时上线或下线,消费者需要能够动态感知服务地址的变化
  • 负载均衡:同一服务通常部署多个实例,消费者需要在多个实例间合理分配请求
  • 故障隔离:当某个服务实例出现故障时,消费者需要能够自动避开故障实例
  • 弹性伸缩:服务可以根据负载动态扩缩容,服务发现机制需要能够及时感知实例变化

服务发现的核心职责

一个完整的服务发现机制需要承担以下职责:

职责说明
服务注册服务启动时向注册中心注册自己的网络地址(IP 和端口)
服务发现服务消费者从注册中心获取可用的服务提供者地址列表
健康检查定期检测服务实例的健康状态,剔除不健康的实例
服务订阅服务消费者订阅服务变化,当服务实例变化时收到通知
元数据管理管理服务的附加信息,如版本、权重、标签等

Nacos 服务发现模式

Nacos 支持两种服务发现模式:

模式说明适用场景
DNS 模式通过 DNS 解析获取服务地址,将服务名解析为 IP 地址列表传统应用、Kubernetes 集成、非 Java 应用
RPC 模式通过 HTTP/gRPC 接口获取服务地址,支持实时订阅服务变化Spring Cloud、Dubbo 应用、需要实时感知的场景

DNS 模式的优势在于兼容性好,任何支持 DNS 解析的应用都可以使用。但 DNS 模式存在缓存延迟,无法实时感知服务变化。RPC 模式则通过长连接实现了实时推送,服务变化可以在毫秒级推送给消费者。

服务注册

服务注册是服务发现的第一步。当服务提供者启动时,需要向 Nacos 注册自己的地址信息,这样服务消费者才能发现并调用它。

服务注册流程

服务注册的完整流程如下:

  1. 服务启动:服务提供者应用启动
  2. 读取配置:读取 Nacos 服务器地址、命名空间等配置
  3. 建立连接:客户端与 Nacos Server 建立 gRPC 长连接(2.x 版本)
  4. 发送注册请求:向 Nacos 发送服务注册请求,包含服务名、IP、端口、元数据等信息
  5. 数据存储:Nacos 存储服务实例信息(临时实例存内存,持久实例存数据库)
  6. 数据同步:Nacos 集群节点间同步服务信息(Distro 协议)
  7. 通知订阅者: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 的服务发现采用客户端发现模式,具体工作原理如下:

  1. 服务查询:消费者启动时,向 Nacos 查询目标服务的实例列表
  2. 本地缓存:消费者将服务实例列表缓存到本地,后续调用优先使用缓存
  3. 服务订阅:消费者向 Nacos 订阅目标服务的变化事件
  4. 变更通知:当服务实例发生变化时(上线、下线、健康状态变化),Nacos 主动推送变更通知
  5. 缓存更新:消费者收到通知后更新本地缓存

这种模式的优势在于:

  • 减少网络开销:消费者优先使用本地缓存,减少了对 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

或者在控制台手动配置:

  1. 登录 Nacos 控制台
  2. 进入「服务管理」->「服务列表」
  3. 点击服务名进入详情
  4. 修改实例的权重值

权重越大,被选中的概率越高。可以用于灰度发布,将新版本的权重设置较低,逐步增加。

使用 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
  • 心跳线程被阻塞

下一步