Nacos 服务注册与配置管理实战

Nacos是什么

Nacos (Dynamic Naming and Configuration Service) 是阿里开源的一站式微服务基础设施,提供两个核心能力:

  1. 服务注册与发现 — 实例上下线自动感知,消费者通过服务名调用,不再硬编码 IP:端口
  2. 配置管理(动态配置) — 配置集中存储,修改后实时推送到客户端,无需重启

Nacos = Eureka(服务发现)+ Spring Cloud Config(配置中心)+ 控制台。

Nacos 区别于 Eureka 和 ZooKeeper 的核心能力是同时支持 AP 和 CP 两种一致性模式:服务发现走 AP(可用性优先,短暂不一致可接受),配置管理走 CP(一致性优先)。在同一个集群中用不同的协议层实现——服务发现用 Distro(Gossip 类 AP 协议),配置持久化依赖 Raft + MySQL。

为什么需要

场景 没有服务发现的后果 Nacos 怎么做
服务扩缩容 IP 写死在配置里,扩缩容要改代码重启 服务名调用,自动解析到健康实例
实例故障 上游不知道下游已挂,请求持续报错 心跳检测 + 主动剔除不健康实例
配置变更 改数据库地址就要重新打包发布 动态推送,@RefreshScope 实时生效

核心架构

Nacos 的架构围绕三个角色展开:

graph LR subgraph 服务提供者 P1[payment-service<br>端口 9001/9002] end subgraph Nacos Server NS[注册表 + 配置中心<br>Distro AP / Raft CP] end subgraph 服务消费者 C1[order-service<br>端口 8081] end subgraph 配置管理 CF[统一配置<br>动态刷新推送] end P1 -->|① 注册 + 心跳| NS C1 -->|② 获取/订阅服务列表| NS C1 -->|③ 负载均衡调用| P1 CF -->|读写| NS CF -.->|推送| C1 style P1 fill:#e1f5fe style NS fill:#c8e6c9 style C1 fill:#fff9c4 style CF fill:#ffccbc

三个角色的职责:

角色 做什么 典型实例
Provider(提供者) 启动时注册自身 IP:端口,之后每 5s 发心跳证明存活 支付服务 payment-service
Nacos Server 维护服务注册表和配置数据。节点间用 Distro(服务发现 AP)或 Raft(配置管理 CP)同步 集群至少 3 节点
Consumer(消费者) 从 Nacos 拉取/订阅 Provider 列表,通过负载均衡选一个发起调用 订单服务 order-service

两条核心链路:

  1. 服务发现链路: Provider 启动时注册到 Nacos → Consumer 订阅服务列表 → Consumer 用服务名调用,Nacos 自动解析为真实的 IP:端口 + 负载均衡
  2. 配置管理链路: 运维人员在 Nacos 控制台改配置 → Nacos 实时推送 → 客户端 @RefreshScope 自动刷新 Bean,全过程不需要重启

新人上手指南

第一步:启动 Nacos Server

# 下载 2.3.2(当前稳定版)
curl -O https://github.com/alibaba/nacos/releases/download/2.3.2/nacos-server-2.3.2.zip
unzip nacos-server-2.3.2.zip && cd nacos/bin

# 单机模式启动(学习用)
sh startup.sh -m standalone
# 访问 http://localhost:8848/nacos  默认 nacos/nacos

生产集群(3 节点 + Nginx):

  • 使用 MySQL 做外部存储(替代默认 Derby,Derby 重启丢数据)
  • Nginx 配置 HTTP 反向代理 + Stream 代理 gRPC 端口(8848 → 3 个节点,gRPC 端口 = server.port + 1000)
  • 修改 cluster.conf 列出所有节点 IP:PORT
  • 关键:Nacos 2.x 的 Nginx 必须同时代理 HTTP 和 gRPC

第二步:服务注册与发现

1. 添加依赖

<!-- Nacos 服务发现 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud LoadBalancer(替代 Ribbon,必须引入) -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

说明: Spring Cloud 2020.0.0 起废弃了 Netflix Ribbon,改用 Spring Cloud LoadBalancer。spring-cloud-starter-alibaba-nacos-discovery 不会自动引入负载均衡器,必须手动加此依赖。

2. 配置

# application.yml
spring:
  application:
    name: order-service          # 服务名,其他服务通过此名调用
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: dev           # 环境隔离
        ephemeral: true          # 临时实例(默认),false 为永久实例

server:
  port: 8081

3. 服务提供者(Provider)

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentApplication {
    public static void main(String[] args) {
        SpringApplication.run(PaymentApplication.class, args);
    }
}

@RestController
@RequestMapping("/payment")
public class PaymentController {

    @Value("${server.port}")
    private String port;

    // 支付接口,返回携带端口信息方便验证负载均衡
    @PostMapping("/create")
    public Result create(@RequestBody PaymentReq req) {
        return Result.ok("支付成功,端口: " + port + ",金额: " + req.getAmount());
    }

    @GetMapping("/{id}")
    public Result getById(@PathVariable Long id) {
        return Result.ok("查询支付订单: " + id + ",端口: " + port);
    }
}

4. 服务消费者(Consumer)— 两种调用方式

方式一:RestTemplate + @LoadBalanced(适合原型快速验证)

@Configuration
public class CloudConfig {

    // 关键:加 @LoadBalanced 让 RestTemplate 具备负载均衡能力
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    // 可选:为 RestTemplate 添加统一的超时与重试
    @Bean
    public RestTemplate restTemplateWithTimeout() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(3000);     // 连接超时 3s
        factory.setReadTimeout(5000);        // 读取超时 5s
        RestTemplate tmpl = new RestTemplate(factory);

        // 添加统一拦截器:打印请求日志
        tmpl.getInterceptors().add((request, body, execution) -> {
            log.info("请求: {} {}, body: {}",
                request.getMethod(), request.getURI(), new String(body));
            return execution.execute(request, body);
        });

        return tmpl;
    }
}

@Service
@Slf4j
public class OrderService {

    @Autowired
    private RestTemplate restTemplate;

    public Result createOrder(Long userId, Long amount) {
        // 1. 先调支付服务扣款——直接写服务名,LoadBalancer 自动轮询到提供者实例
        PaymentReq req = new PaymentReq(userId, amount);
        Result payResult = restTemplate.postForObject(
            "http://payment-service/payment/create",  // 服务名代替 IP:端口
            req,
            Result.class
        );

        if (!payResult.isSuccess()) {
            return Result.error("支付失败: " + payResult.getMsg());
        }

        // 2. 落本地订单表(略)
        return Result.ok("下单成功,支付信息: " + payResult.getData());
    }
}

方式二:OpenFeign(推荐,声明式+可配置+生产级)

OpenFeign 整合了负载均衡、服务发现、熔断降级(Sentinel),是生产环境的标准选择。

// ---------- 1. 声明 Feign 客户端 ----------
// 加上 fallback 实现降级逻辑
@FeignClient(
    name = "payment-service",               // 目标服务名(与 Nacos 注册名一致)
    fallbackFactory = PaymentClientFallback.class,  // 降级工厂
    configuration = PaymentClientConfig.class       // 自定义配置
)
public interface PaymentClient {

    @PostMapping("/payment/create")
    Result pay(@RequestBody PaymentReq req);

    @GetMapping("/payment/{id}")
    Result getPayment(@PathVariable("id") Long id);
}

// ---------- 2. Feign 自定义配置 ----------
public class PaymentClientConfig {

    @Bean
    public Logger.Level feignLogger() {
        return Logger.Level.FULL;  // 打印请求/响应头、体,方便调试
    }

    // 自定义请求拦截器:每个 Feign 请求自动携带 token
    @Bean
    public RequestInterceptor tokenInterceptor() {
        return request -> {
            RequestTemplate t = request.requestTemplate();
            t.header("Authorization", "Bearer " + getCurrentToken());
            // 透传 traceId 用于链路追踪
            t.header("X-Trace-Id", UUID.randomUUID().toString().substring(0, 8));
        };
    }

    private String getCurrentToken() {
        // 从请求上下文或 ThreadLocal 获取
        return RequestContextHolder.getRequestAttributes() != null
            ? ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest().getHeader("Authorization")
            : "";
    }
}

// ---------- 3. 降级实现 ----------
@Slf4j
@Component
public class PaymentClientFallback implements FallbackFactory<PaymentClient> {

    @Override
    public PaymentClient create(Throwable cause) {
        log.error("支付服务不可用,触发降级", cause);
        return new PaymentClient() {
            @Override
            public Result pay(PaymentReq req) {
                return Result.error("支付服务暂时不可用,请稍后再试");
            }

            @Override
            public Result getPayment(Long id) {
                return Result.error("查询支付失败,服务不可用");
            }
        };
    }
}

// ---------- 4. 业务代码中使用 ----------
@Service
public class OrderService {

    @Autowired
    private PaymentClient paymentClient;

    public Result createOrder(Long userId, Long amount) {
        // 像调用本地方法一样调用远程服务,Feign 自动完成序列化、负载均衡、发送请求、反序列化
        PaymentReq req = new PaymentReq(userId, amount);
        return paymentClient.pay(req);
    }
}

5. 验证负载均衡

启动 2 个支付服务实例(端口 9001、9002),在 Nacos 控制台看到 2 个实例都健康后,多次调用订单接口。观察每次返回的 port 字段在 9001 和 9002 之间轮询切换。

第三步:配置管理(动态刷新)

1. 添加依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Boot 2.4+ 需要额外引入 bootstrap -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

2. 配置 bootstrap.yml

Nacos Config 在 Spring 容器初始化之前加载配置,所以必须放在 bootstrap.yml 中。

spring:
  application:
    name: order-service
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        namespace: dev
        group: DEFAULT_GROUP
        # 共享配置:多个微服务公用的配置(如 Redis、MQ 地址)
        shared-configs:
          - data-id: common-redis.yaml
            refresh: true
          - data-id: common-mq.yaml
            refresh: true

Nacos 配置中心的 DataId 规则:${spring.application.name}.${file-extension}order-service.yaml

3. 在 Nacos 控制台创建配置

Data ID:    order-service.yaml
配置格式:   YAML
配置内容:
  order:
    timeout: 5000
    max-count: 100
    discount-rate: 0.8

4. 代码中动态读取

@Component
@RefreshScope    // 配置变更时自动刷新 Bean 内的 @Value
@ConfigurationProperties(prefix = "order")  // 批量绑定,不用一个个 @Value
public class OrderProperties {
    private int timeout;
    private int maxCount;
    private double discountRate;
    // getter / setter ...
}

@Service
public class OrderService {

    @Autowired
    private OrderProperties orderProps;

    public void process() {
        // 每次调用都读取最新值(Nacos 改配置后实时更新)
        log.info("当前折扣: {}, 超时: {}ms", orderProps.getDiscountRate(), orderProps.getTimeout());
    }
}

验证动态刷新: Nacos 改配置 → 保存 → 应用日志打印 Refresh keys changed: [order.timeout] → 代码获取到最新值,无需重启。

完整流程

sequenceDiagram participant App as Spring Boot App participant Nacos as Nacos Server participant Config as 配置存储 App->>Nacos: 1. 注册服务(register) App->>Config: 2. 拉取远程配置(pull) Config-->>App: 3. 配置内容 App->>Nacos: 4. 每 5s 心跳(临时实例) App->>App: 5. 正常服务 Config-->>Nacos: 6. 配置变更 Nacos-->>App: 7. 长轮询返回变更通知 App->>Config: 8. 拉取新配置 App->>App: 9. @RefreshScope 刷新 Bean

常见问题

1. 服务调不通:404 / Connection Refused

graph TD A[服务调不通] --> B{服务名正确?} B -- 否 --> C[检查 @FeignClient name<br>与 application.name 一致] B -- 是 --> D{目标实例健康?} D -- 否 --> E[Nacos 控制台检查心跳状态] D -- 是 --> F{调用的端口是?} F -- Nacos 8848 --> G[❌ 调的是目标服务端口<br>不是 Nacos 端口!] F -- 目标端口 --> H[检查防火墙 / 网络策略]

2. 负载均衡不生效(总是打到同一台)

  • 未引入 spring-cloud-starter-loadbalancer 依赖(Spring Cloud 2020+ 必须手动加)
  • Feign 默认使用懒加载,首次调用时创建负载均衡上下文。开启饥饿加载:
    spring:
      cloud:
        loadbalancer:
          eager-load:
            clients: payment-service,user-service
    

3. 配置动态刷新不生效

  • 确认 Bean 加了 @RefreshScope
  • 确认 bootstrap.yml 存在(Spring Boot 2.4+ 需引入 spring-cloud-starter-bootstrap
  • 确认 Nacos 控制台和客户端的 namespace 一致(默认 public

4. Nacos 集群部署注意点(2.x)

  • Nacos 2.x 使用 gRPC 通信,服务端监听两个端口:8848(HTTP)和 9848(gRPC,= 8848+1000)
  • Nginx 反向代理必须同时代理 HTTP 和 gRPC(使用 stream 模块)
  • 三个节点端口不能连续(如 8848、8858、8868),避免 gRPC 端口冲突
  • 在线压测 / 扩缩容前建议延长 Nacos 健康检查的心跳超时时间

面试题

Q1:Nacos 如何同时支持 AP 和 CP 两种一致性模型?

参考答案:

Nacos 的设计很巧妙——它在同一个集群中根据数据类型选择不同的一致性协议:

服务发现 → AP 模式: 使用 Distro 协议(Nacos 自研,类似 Gossip)。临时实例注册后存储在内存中,节点之间异步同步。允许短暂不一致(一台机器刚宕机,另一台机器可能还有它的缓存),但保证可用性。这符合 CAP 理论——出现网络分区时,宁可读到过时的服务列表,也不能让注册中心不可用。

配置管理 → CP 模式: 使用 Raft 协议,配合 MySQL 持久化。配置信息必须强一致——读到错误的配置可能导致全链路故障(比如数据库连接地址写错)。写入配置时要求多数节点确认后才返回成功。

和同类对比: Eureka 是纯 AP(所有节点平等,集群不可用时自我保护,不剔除实例),ZooKeeper 是纯 CP(选举期间不可用),Nacos 用同一套架构在服务发现和配置管理两个模块分别选择了最适合的一致性模型。

Q2:Nacos 1.x 和 2.x 在心跳机制上有什么差异?为什么 2.x 性能更好?

参考答案:

1.x 心跳机制: 客户端每个服务实例启动一个定时任务,每 5 秒通过 HTTP PUT 请求向服务端发送心跳。服务端也每 5 秒扫描一次所有实例,超过 15 秒没收到心跳的标记为不健康,超过 30 秒的剔除。这是典型的"客户端定时上报 + 服务端定时检查"模型,HTTP 短连接频繁创建销毁,资源浪费严重。

2.x 心跳机制: 客户端通过 gRPC 与服务端建立长连接,连接本身自带 KeepAlive 心跳。此外服务端有一个 3 秒的定时器,检查哪些连接超过 20 秒没有数据交互,主动发出探测请求,不通则断开连接并剔除注册的实例。

2.x 性能更好的原因:

  1. 长连接代替短连接,省掉频繁创建 TCP 连接的开销
  2. 一个 gRPC 连接可以复用传输所有的注册、心跳、订阅请求,1.x 的每个请求都是独立的 HTTP 请求
  3. 官方数据:2.x 的注册性能是 1.x 的 2 倍以上

Q3:临时实例(ephemeral=true)和永久实例有什么区别?什么时候用哪个?

参考答案:

这是 Nacos 区别于 Eureka 的一个特性——Nacos 不仅能做微服务注册中心,还能管理基础设施,所以引入了实例类型的概念。

维度 临时实例 永久实例
存储位置 内存(服务注册表) 内存 + 磁盘持久化
健康检查 客户端心跳上报 服务端主动探测(TCP/HTTP)
实例异常 超时未心跳则剔除 标记为不健康,不会剔除
一致性协议 Distro(AP) JRaft(CP)
适用场景 业务服务(Spring Boot) 基础设施(MySQL、Redis)

选型原则: Spring Cloud 微服务默认就是临时实例(ephemeral: true),服务上下线频率高,临时实例的心跳机制效率高且不会留下"僵尸实例"。只有在注册 MySQL、Redis 这类预期永久存在的服务时才用永久实例。

另外注意一个版本差异:1.x 中同一个服务可以同时混合临时和永久实例;2.x 中一个服务所有实例必须类型一致(全部临时或全部永久)。


延伸阅读: 尼恩《Nacos 学习圣经》详细讲解了 Nacos 2.x 的 gRPC 协议、Distro 一致性协议、Raft 选主过程。推荐细读服务发现和心跳章节。

posted @ 2026-07-27 21:23  念笙  阅读(33)  评论(0)    收藏  举报