Nacos 注册中心
定位
Nacos(Dynamic Naming and Configuration Service,动态命名与配置服务)在微服务架构中扮演服务注册中心(Service Registry)的角色。它的核心使命是:
- 解决服务发现(Service Discovery)问题:在微服务架构下,服务实例的 IP 和端口是动态变化的(弹性伸缩、滚动发布、故障转移)。Nacos 让服务消费者无需硬编码目标地址,只需通过服务名即可调用。
- 淘汰手动维护 IP:Port 的运维方式:新增实例自动注册,下线实例自动剔除,调用方始终拿到健康实例列表。
对比 Eureka 与 ZooKeeper
| 维度 | Nacos | Eureka | ZooKeeper |
|---|---|---|---|
| 一致性协议 | AP(Distro)/ CP(Raft)双模式 | AP | CP(ZAB) |
| 配置中心 | 内置 | 无 | 需自行实现 |
| 健康检查 | 客户端心跳 + 服务端探测 | 客户端心跳 | 临时节点 + Session |
| 多数据中心 | 原生支持 | 原生支持 | 第三方扩展 |
| 2.x 通信协议 | gRPC 双向流 | HTTP | TCP Session |
Nacos 的 AP/CP 双模式是核心优势:默认 AP 模式(Distro 协议)保证高可用,服务发现宁可短暂不一致也绝不能不可用;当需要强一致性场景(如配置变更)时,可切换为 CP 模式(Raft 协议)。
一句话讲清楚:Nacos = Eureka 注册中心 + Config 配置中心。本章只讲注册中心,配置中心见下一章。
核心概念
命名空间(Namespace)
命名空间是 Nacos 中最顶层的隔离单位,用于实现多环境完全隔离(开发 / 测试 / 生产)。每个 Namespace 由唯一 UUID 标识。不同 Namespace 下的服务不可互相发现。
飞翔科技的实际使用:
devNamespace(UUID:a1b2c3d4-xxx)用于日常开发联调;prodNamespace(UUID:e5f6a7b8-xxx)用于线上生产环境。
分组(Group)
同一 Namespace 内服务的逻辑分组,默认值为 DEFAULT_GROUP。白歌习惯将支付相关服务划入 PAYMENT_GROUP,订单相关服务划入 ORDER_GROUP,避免跨团队互相干扰。
服务(Service)
一组提供相同功能的实例集合。例如飞翔科技的用户服务部署了 3 个实例,它们在 Nacos 中同属一个 Service,服务名为 user-service。
实例(Instance)
服务的具体节点,包含 IP、端口、权重(Weight)、健康状态、元数据(Metadata)等信息。每个实例启动时向 Nacos 注册自己的完整信息。
临时实例 vs 永久实例
| 类型 | 注册方式 | 健康检查 | 剔除机制 | 适用场景 |
|---|---|---|---|---|
| 临时实例(Ephemeral Instance) | 客户端主动注册 + 心跳维持 | 客户端每 5s 上报心跳 | 15s 无心跳→不健康,30s→自动剔除 | 微服务(默认) |
| 永久实例(Permanent Instance) | 手动注册 / API 注册 | 服务端主动探测(TCP / HTTP / MySQL) | 不自动剔除,需手动下线 | 数据库、Redis、传统 Dubbo 服务 |
保护阈值(Protection Threshold)
保护阈值是一个 0~1 之间的浮点数,表示健康实例占比的最低容忍线。当健康实例占比低于该阈值时,Nacos 触发保护模式:不再剔除任何实例,宁可让消费者拿到可能不健康的实例,也绝不丢弃所有实例导致服务完全不可用。这是 CAP 定理中 AP 倾向的典型体现。
核心原理
服务注册与发现完整流程
以下时序图展示了从 Provider 启动到 Consumer 调用的全过程:
临时实例健康检查状态流转
Distro 协议简介(AP 模式)
Distro 是 Nacos 自研的最终一致性协议,用于处理临时实例的服务注册。核心设计:
- 一致性哈希分发:请求按
serviceName哈希到特定节点(命中节点),该节点负责写入。 - 本地写即返回:命中节点写入后立即返回成功,不等待跨节点同步,写入延迟 < 1ms。
- 异步复制:后台向其他节点异步推送数据,网络正常时延迟 < 500ms。
- 健康检查去中心化:每个节点独立对本地实例执行心跳检查,不做跨节点同步。
选择 Distro(AP)而非 Raft(CP)是刻意的工程权衡:服务发现场景中,宁可短暂不一致也绝不能完全不返回实例。
Raft 协议简介(CP 模式)
CP 模式下 Nacos 使用简化 Raft 协议保证强一致性,用于永久实例和配置管理:
| 阶段 | 说明 |
|---|---|
| Leader 选举 | 节点随机超时(150ms~300ms),超时后发起投票,获得 N/2+1 票成为 Leader |
| 日志复制 | Leader 将变更追加到日志,复制到所有 Follower,等待过半确认后才提交 |
| 快照 | 定期压缩 Raft 日志,避免日志无限增长 |
配置管理对一致性要求高于可用性——用户的数据库密码如果读到旧值可能引发生产故障,因此配置变更走 CP 模式。
环境准备
Nacos Server 下载与启动
Windows:
# 下载(PowerShell)
Invoke-WebRequest -Uri https://github.com/alibaba/nacos/releases/download/v2.4.0/nacos-server-2.4.0.zip -OutFile nacos-server-2.4.0.zip
Expand-Archive nacos-server-2.4.0.zip -DestinationPath .
cd nacos\bin
# 单机模式启动
.\startup.cmd -m standalone
Linux / macOS:
wget https://github.com/alibaba/nacos/releases/download/v2.4.0/nacos-server-2.4.0.zip
unzip nacos-server-2.4.0.zip
cd nacos/bin
# 单机模式启动
sh startup.sh -m standalone
控制台访问
| 项目 | 值 |
|---|---|
| 访问地址 | http://127.0.0.1:8848/nacos |
| 默认账号 | nacos |
| 默认密码 | nacos |
登录后可在「服务管理 → 服务列表」查看已注册的所有服务。
完整示例1:服务注册与发现(RestTemplate + LoadBalancer)
场景
飞翔科技需要构建用户服务(User Service)和订单服务(Order Service)。架构师白歌负责用户服务端(Provider),后端开发小崔负责订单服务端(Consumer)。订单服务需要通过 Nacos 发现用户服务,实现服务名调用,彻底告别硬编码 IP:Port。
操作前:单体应用直连
订单服务 → http://192.168.1.10:8080/user/1
问题:用户服务扩容到 3 台后,订单服务需要手动维护 IP 列表,实例变更感知不到。
操作后:通过 Nacos 服务发现
订单服务 → http://user-service/user/1
↑ Nacos 自动负载均衡到底层 3 台实例之一
依赖配置
两个服务均需引入如下依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
application.yml 完整配置
用户服务(Provider,白歌开发):
server:
port: 8080
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: a1b2c3d4-1234-5678-9abc-def012345678 # dev 命名空间的 UUID
group: DEFAULT_GROUP
ephemeral: true # 临时实例
weight: 1.0 # 权重(1~100)
metadata:
version: v1 # 版本元数据
developer: baige # 开发者标识
命名空间在实际配置中填写的是 UUID 而非名称(如"dev")。名称仅用于 Nacos 控制台展示,注册时必须用 UUID。这是初学者最高频踩坑点。
订单服务(Consumer,小崔开发):
server:
port: 8081
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: a1b2c3d4-1234-5678-9abc-def012345678
group: DEFAULT_GROUP
ephemeral: true
Provider 代码
// 启动类
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
@RestController
@RequestMapping("/user")
public class UserController {
@Value("${server.port}")
private String port;
@GetMapping("/{id}")
public String getUser(@PathVariable Long id) {
return "User-" + id + " from port: " + port;
}
}
Consumer 代码
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // 核心:开启负载均衡 + 服务名解析
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/{id}")
public String getOrder(@PathVariable Long id) {
// 使用服务名替代 IP:Port,Nacos + LoadBalancer 自动解析
String url = "http://user-service/user/" + id;
return restTemplate.getForObject(url, String.class);
}
}
验证
- 启动 Nacos Server
- 启动用户服务(可启动多实例,使用
--server.port=8082参数) - 启动订单服务
- 访问
http://127.0.0.1:8848/nacos→ 服务列表,确认user-service和order-service均已注册 - 多次访问
http://localhost:8081/order/1,返回端口交替变化,证明负载均衡生效
完整示例2:OpenFeign 声明式调用
场景
小崔发现使用 RestTemplate 拼接 URL 字符串不够优雅,代码可读性差。白歌建议使用 OpenFeign(Open Feign,声明式 HTTP 客户端),将远程调用抽象为本地接口方法调用。
@FeignClient 注解用法
// 订单服务中定义的用户服务调用接口
@FeignClient(name = "user-service", path = "/user")
public interface UserFeignClient {
@GetMapping("/{id}")
String getUser(@PathVariable("id") Long id);
}
name:目标服务名,即spring.application.name的值path:目标服务的统一路径前缀
@EnableFeignClients 激活
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 激活 Feign 客户端扫描
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
接口定义与使用
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private UserFeignClient userFeignClient;
@GetMapping("/{id}")
public String getOrder(@PathVariable Long id) {
// 像调用本地方法一样调用远程服务,底层自动通过 Nacos 发现实例
String userInfo = userFeignClient.getUser(id);
return "Order-" + id + " | " + userInfo;
}
}
运行效果
启动两个服务后,进入 Nacos 控制台(http://127.0.0.1:8848/nacos):
- 「服务管理 → 服务列表」可看到
user-service和order-service两个服务 - 点击
user-service→ 「订阅者列表」可看到order-service,说明订单服务已订阅用户服务 - 「订阅者列表」清晰展示了服务间的调用依赖关系
完整示例3:元数据路由(灰度发布)
场景
飞翔科技的用户服务需要发布 v2 版本。白歌希望内部测试流量路由到 v2 实例,正式用户流量继续走 v1 实例,实现灰度发布(Grayscale Deployment)。
Provider:v2 版本标记
白歌启动用户服务 v2 实例时,通过 metadata.version 标记版本:
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
metadata:
version: v2 # 灰度版本标记
server:
port: 8088
此时 Nacos 中 user-service 下有 2 个实例:v1(元数据 version=v1)和 v2(元数据 version=v2)。
Consumer:自定义负载均衡策略
@Configuration
public class GrayLoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> grayLoadBalancer(
Environment env,
LoadBalancerClientFactory factory) {
String serviceName = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new GrayLoadBalancer(
factory.getLazyProvider(serviceName, ServiceInstanceListSupplier.class),
serviceName
);
}
}
// 灰度负载均衡器核心逻辑
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceName;
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier =
serviceInstanceListSupplierProvider.getIfAvailable();
return supplier.get()
.next()
.map(instances -> {
// 从请求上下文中获取灰度标记
RequestDataContext context = (RequestDataContext) request.getContext();
String version = context.getClientRequest()
.getHeaders()
.getFirst("version");
// 按版本过滤实例
List<ServiceInstance> matched = instances.stream()
.filter(i -> version == null ||
version.equals(i.getMetadata().get("version")))
.collect(Collectors.toList());
if (matched.isEmpty()) {
// 回退到默认实例
matched = instances;
}
ServiceInstance instance = matched.get(
ThreadLocalRandom.current().nextInt(matched.size()));
return new DefaultResponse(instance);
});
}
}
操作前后对比
| 阶段 | 路由策略 | 效果 |
|---|---|---|
| 操作前 | 全量路由 | 所有请求随机分发到 v1 和 v2,无法区分验证 |
| 操作后 | 灰度路由 | 请求 Header 带 version: v2 → 路由到 v2;普通请求 → 路由到 v1 |
灰度路由的核心原理:Nacos 存储实例元数据 → LoadBalancer 读取元数据 → 按 Header 匹配路由。
易错场景
命名空间填成了名称而非 UUID
错误写法:
spring:
cloud:
nacos:
discovery:
namespace: dev # 错误!Nacos 实际使用 UUID
正确写法:
spring:
cloud:
nacos:
discovery:
namespace: a1b2c3d4-1234-5678-9abc-def012345678 # 控制台 → 命名空间 → 复制 ID
现象:服务一直注册不上,日志报
404 Not Found。在 Nacos 控制台「命名空间」页面点击对应空间,右侧「命名空间ID」列的值才是配置中应填的值。
Group 不一致导致消费者发现不到服务
白歌将用户服务注册到 PAYMENT_GROUP,小崔的订单服务默认使用 DEFAULT_GROUP 去订阅 → 发现列表为空。同一 Group 内服务才能互相发现。两个服务在开发联调前,务必确认 Group 配置一致。
临时实例心跳断开后服务列表仍有残留
Nacos 对临时实例的剔除不是实时的:15s 无心跳标记不健康 → 30s 无心跳才剔除。因此手动 Kill 进程后,Nacos 控制台可能仍有该实例展示。这是正常行为,等待约 30s 即可自动消失。
Nacos 2.x gRPC 端口 9848 未放行导致连接失败
Nacos 2.x 引入了 gRPC 通信(默认端口:主端口 + 1000 = 9848)。如果服务器或防火墙只放行了 8848,客户端将无法建立 gRPC 长连接,表现为主端口正常但注册失败。解决方案:在安全组/防火墙规则中同时放行 8848 和 9848。
面试考点
Nacos AP 与 CP 模式如何选择?各自适用什么场景?
- AP 模式(默认):适用于服务注册与发现。服务发现场景容量大、频率高,宁可短暂不一致也不能完全不返回实例列表。底层使用 Distro 协议。
- CP 模式:适用于配置管理。配置错误可能引发生产事故(如数据库密码错误),对一致性要求高于可用性。底层使用 Raft 协议。
Nacos 1.x 与 2.x 的心跳机制有何不同?
| 版本 | 协议 | 机制 | 特点 |
|---|---|---|---|
| 1.x | HTTP + UDP Push | 客户端每 5s 发送 HTTP 心跳,变更通过 UDP 推送 | 单向通信,UDP 推送不可靠 |
| 2.x | gRPC 双向流(Bidirectional Streaming) | 建立长连接,客户端主动上报 + 服务端实时推送 | 双向通信,单连接复用(配置变更 + 服务发现共用同一连接),性能与可靠性大幅提升 |
保护阈值触发后会发生什么?
当健康实例占比低于保护阈值时,Nacos 进入保护模式:
- 停止剔除不健康实例
- 即使某些实例心跳断开,依然保留在实例列表中
- 消费者可能拿到不健康的实例并调用失败
设计意图:宁可让消费者拿到不可用的实例(部分失败),也绝不能服务列表为空(全局不可用)。
临时实例和永久实例的区别及各自适用场景
| 维度 | 临时实例 | 永久实例 |
|---|---|---|
| 心跳机制 | 客户端主动上报(5s 间隔) | 服务端主动探测(TCP / HTTP / MySQL) |
| 剔除策略 | 自动剔除(30s 无心跳) | 不自动剔除,需手动注销 |
| 一致性协议 | Distro(AP) | Raft(CP) |
| 适用场景 | 微服务实例(Kubernetes Pod) | 基础设施(数据库 / Redis / DNS) |
| 配置项 | ephemeral: true | ephemeral: false |