【架构实战】服务注册与发现Nacos:微服务时代的“电话总机“

一、从一次"服务雪崩"说起

那是2019年的双十一,我负责的电商系统迎来了流量洪峰。系统架构是典型的微服务:用户请求 → 网关 → 订单服务 → 库存服务 → 支付服务。

凌晨零点刚过,监控系统报警此起彼伏。我发现一个致命问题:库存服务的一台机器CPU满了,但负载均衡器不知道,还在往它分发请求。

结果那台机器扛不住,开始响应超时;上游的订单服务等啊等,终于超时了;订单服务也开始堆积请求;然后订单服务也扛不住了......

整个链路像多米诺骨牌一样,一个接一个倒下。

后来复盘发现,如果我们用了服务注册与发现机制,健康检查功能会自动把那台故障机器从服务列表中剔除,流量就不会打到它身上了。


二、为什么需要服务注册与发现

2.1 传统方式的问题

方式1:配置文件硬编码

yaml 复制代码
# 订单服务配置
inventory:
  hosts:
    - 192.168.1.10:8080
    - 192.168.1.11:8080
    - 192.168.1.12:8080

问题:服务器IP变了要改配置,扩容要改配置,缩容要改配置------改到怀疑人生

方式2:DNS服务发现

bash 复制代码
# 通过DNS解析服务地址
nslookup inventory-service

# 输出:
inventory-service    canonical = inventory-1.default.svc.cluster.local
inventory-service    canonical = inventory-2.default.svc.cluster.local

问题:DNS没有健康检查,挂了的服务IP还在DNS记录里。

2.2 服务注册与发现的原理

复制代码
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   服务启动:                                                 │
│   ┌──────────────┐                                        │
│   │  库存服务实例   │ → 注册到注册中心                      │
│   │ 192.168.1.10 │ → 心跳:I'm alive!                      │
│   └──────────────┘                                        │
│            ↓                                               │
│   ┌──────────────────────────────────────────────────┐    │
│   │                   注册中心                        │    │
│   │                                                 │    │
│   │  服务名: inventory-service                        │    │
│   │  实例1: 192.168.1.10:8080 (健康)                 │    │
│   │  实例2: 192.168.1.11:8080 (健康)                 │    │
│   │  实例3: 192.168.1.12:8080 (不健康,已移除)        │    │
│   └──────────────────────────────────────────────────┘    │
│            ↓                                               │
│   服务查询:                                                 │
│   ┌──────────────┐                                        │
│   │  订单服务      │ ← 查询注册中心:我需要调库存服务!      │
│   │              │ ← 获取实例列表:[10, 11](自动排除12)  │
│   └──────────────┘                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

2.3 核心概念

概念 说明
服务(Service) 一组提供相同功能的服务实例集合,如"用户服务"
服务实例(Instance) 服务的具体运行实例,每个实例有唯一的IP:Port
服务注册(Register) 服务启动时向注册中心注册自己的地址
服务注销(Deregister) 服务停止时从注册中心删除自己的信息
心跳(Heartbeat) 服务定期向注册中心发送心跳,证明自己还活着
服务发现(Discovery) 调用方从注册中心获取服务实例列表
健康检查(Health Check) 注册中心检测服务是否还存活

三、Nacos是什么

3.1 Nacos概览

Nacos(Naming and Configuration Service)是阿里巴巴开源的服务注册与配置管理中心,是Spring Cloud Alibaba生态的核心组件。

复制代码
Nacos核心能力:

┌────────────────────────────────────────────┐
│              Nacos                          │
│                                             │
│  ┌─────────────────┐  ┌─────────────────┐ │
│  │   服务注册与发现   │  │     配置中心      │ │
│  │                  │  │                  │ │
│  │  • 注册服务实例   │  │  • 配置管理       │ │
│  │  • 心跳检测      │  │  • 配置热更新     │ │
│  │  • 健康检查      │  │  • 配置共享       │ │
│  │  • 动态权重      │  │  • 配置加密       │ │
│  └─────────────────┘  └─────────────────┘ │
│                                             │
└────────────────────────────────────────────┘

3.2 Nacos与Spring Cloud集成

xml 复制代码
<!-- Spring Boot依赖 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>2023.0.1.2</version>
</dependency>
yaml 复制代码
# application.yml - 服务提供者
spring:
  application:
    name: inventory-service  # 服务名
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848  # Nacos地址
        namespace: prod  # 命名空间
        group: DEFAULT_GROUP  # 分组
        cluster-name: BJ-GZ01  # 集群
        
# 实例元数据
  cloud:
    nacos:
      discovery:
        instance:
          ip: ${NACOS_INSTANCE_IP:${spring.cloud.client.ip-address}}
          port: ${SERVER_PORT:8080}
          weight: 1  # 权重
          enabled: true
          healthy: true
          ephemeral: false  # false=持久化实例,true=临时实例
yaml 复制代码
# application.yml - 服务消费者
spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        
# RestTemplate + LoadBalancer配置
  cloud:
    loadbalancer:
      ribbon:
        enabled: false  # 使用Spring Cloud LoadBalancer

3.3 服务注册

java 复制代码
// 方式1:自动注册(通过依赖自动生效)
// 只需引入依赖,配置好Nacos地址,服务自动注册

// 方式2:手动注册(更精细的控制)
@RestController
public class NacosRegistrationController {
    
    @Autowired
    private NamingService namingService;
    
    @GetMapping("/register")
    public String register() throws NacosException {
        // 手动注册服务实例
        namingService.registerInstance(
            "inventory-service",  // 服务名
            "192.168.1.10",     // IP
            8080,                // 端口
            "DEFAULT"            // 分组
        );
        
        return "注册成功";
    }
    
    @GetMapping("/deregister")
    public String deregister() throws NacosException {
        // 手动注销
        namingService.deregisterInstance(
            "inventory-service",
            "192.168.1.10",
            8080
        );
        
        return "注销成功";
    }
    
    @GetMapping("/heartbeat")
    public String heartbeat() throws NacosException {
        // 发送心跳
        namingService.sendHeartbeat(
            "inventory-service",
            "192.168.1.10",
            8080
        );
        
        return "心跳发送成功";
    }
}

四、服务发现:找到你想调用的服务

4.1 RestTemplate + 服务名调用

java 复制代码
@Configuration
public class RestTemplateConfig {
    
    @Bean
    @LoadBalanced  // 开启负载均衡
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

@Service
public class OrderService {
    
    @Autowired
    private RestTemplate restTemplate;
    
    public Order createOrder(Long productId, Long userId) {
        // 直接用服务名调用,像调用本地服务一样
        String url = "http://inventory-service/api/inventory/" + productId;
        
        // Nacos + Ribbon会自动:
        // 1. 从Nacos获取inventory-service的实例列表
        // 2. 根据负载均衡策略选择一个实例
        // 3. 替换url中的服务名为实际IP
        // 4. 发起调用
        
        ResponseEntity<Inventory> resp = restTemplate.getForEntity(
            url, 
            Inventory.class
        );
        
        Inventory inventory = resp.getBody();
        
        // 创建订单...
        Order order = new Order();
        order.setProductId(productId);
        order.setUserId(userId);
        
        return order;
    }
}

4.2 Feign客户端调用

java 复制代码
// 定义Feign接口
@FeignClient(name = "inventory-service", path = "/api")
public interface InventoryClient {
    
    // 调用库存扣减
    @PostMapping("/inventory/deduct")
    Result<Void> deduct(@RequestBody DeductRequest request);
    
    // 查询库存
    @GetMapping("/inventory/{productId}")
    Result<Inventory> getInventory(@PathVariable Long productId);
}

// 使用Feign客户端
@Service
public class OrderService {
    
    @Autowired
    private InventoryClient inventoryClient;
    
    public void createOrder(Long productId, Integer count) {
        // 像调用本地方法一样调用远程服务
        inventoryClient.deduct(new DeductRequest(productId, count));
    }
}

4.3 编程式服务发现

java 复制代码
@Service
public class DynamicServiceDiscovery {
    
    @Autowired
    private NamingService namingService;
    
    // 获取所有健康实例
    public List<Instance> getHealthyInstances(String serviceName) throws NacosException {
        // selectInstances自动过滤不健康实例
        return namingService.selectInstances(serviceName, true);
    }
    
    // 获取所有实例
    public List<Instance> getAllInstances(String serviceName) throws NacosException {
        return namingService.selectInstances(serviceName, false);
    }
    
    // 获取一个健康实例(负载均衡)
    public Instance getOneHealthyInstance(String serviceName) throws NacosException {
        return namingService.selectOneHealthyInstance(serviceName);
    }
    
    // 订阅服务变化
    public void subscribeService(String serviceName) throws NacosException {
        namingService.subscribe(serviceName, event -> {
            // 服务列表变化时的回调
            Instances instances = (Instances) event.getContent();
            System.out.println("服务实例列表变化: " + instances.getHosts().size());
            
            for (Instance instance : instances.getHosts()) {
                System.out.println("  - " + instance.getIp() + ":" + instance.getPort() 
                    + " (健康:" + instance.isHealthy() + ")");
            }
        });
    }
}

五、健康检查:保障服务可用性

5.1 Nacos健康检查原理

复制代码
临时实例(ephemeral=true):
  实例注册后,Nacos不会主动检查健康状态
  依赖客户端心跳维持
  客户端心跳停止 → Nacos自动剔除实例

持久化实例(ephemeral=false):
  Nacos服务端主动检查健康状态
  通过TCP/HTTP方式探测
  探测失败 → Nacos标记为不健康

5.2 客户端心跳配置

yaml 复制代码
spring:
  cloud:
    nacos:
      discovery:
        # 心跳间隔(默认5秒)
        heart-beat-interval: 5000
        # 心跳超时时间(默认15秒)
        heart-beat-timeout: 15000
        # 不健康后多少秒剔除(默认30秒)
        ip-delete-timeout: 30000
java 复制代码
// 自定义心跳续约
@RestController
@RequestMapping("/health")
public class HealthController {
    
    @Autowired
    private HeartBeatManager heartBeatManager;
    
    @GetMapping("/status")
    public Result<String> status() {
        // 业务健康检查
        boolean serviceHealthy = checkBusinessHealth();
        
        if (!serviceHealthy) {
            // 通知Nacos当前实例不健康
            heartBeatManager.markUnhealthy();
        } else {
            heartBeatManager.markHealthy();
        }
        
        return Result.ok("OK");
    }
    
    private boolean checkBusinessHealth() {
        // 检查数据库连接
        // 检查Redis连接
        // 检查消息队列
        // ...
        return true;
    }
}

5.3 服务端主动探测

yaml 复制代码
# Nacos服务端健康检查配置(standalone-mysql.yaml)
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/nacos?characterEncoding=utf8
    username: nacos
    password: nacos

# 健康检查配置
nacos:
  health-checker:
    # HTTP健康检查
    http:
      max-timeout: 2000  # 超时时间ms
      path: /actuator/health
      
    # TCP健康检查
    tcp:
      max-timeout: 2000
      
    # MySQL健康检查
    mysql:
      max-timeout: 2000

5.4 分区容灾:同机房优先调用

yaml 复制代码
spring:
  cloud:
    nacos:
      discovery:
        cluster-name: BJ-GZ01  # 北京、广州01机房
java 复制代码
// 优先调用同机房实例
@Service
public class SameClusterLoadBalancer implements LoadBalancer {
    
    @Autowired
    private NamingService namingService;
    
    @Override
    public ServiceInstance choose(String serviceId) {
        try {
            // 1. 先尝试获取同机房实例
            List<Instance> sameClusterInstances = namingService.selectInstances(
                serviceId, 
                true,  // 只选健康的
                "BJ-GZ01"  // 当前机房
            );
            
            if (!sameClusterInstances.isEmpty()) {
                // 同机房有实例,随机选一个
                return randomPick(sameClusterInstances);
            }
            
            // 2. 同机房没有,使用任意健康实例
            List<Instance> allInstances = namingService.selectInstances(serviceId, true);
            return randomPick(allInstances);
            
        } catch (NacosException e) {
            // 异常情况下fallback
            return fallbackChoose(serviceId);
        }
    }
}

六、集群部署:保障高可用

6.1 Nacos集群架构

复制代码
                    ┌──────────────────┐
                    │   负载均衡器       │
                    │  (Nginx/云LB)    │
                    └────────┬─────────┘
                             │
        ┌────────────────────┼────────────────────┐
        │                    │                     │
        ▼                    ▼                     ▼
┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│   Nacos-1    │◄────►│   Nacos-2    │◄────►│   Nacos-3    │
│  (Leader)   │      │  (Follower)  │      │  (Follower)  │
│  192.168.1.1│      │  192.168.1.2 │      │  192.168.1.3 │
│  :8848      │      │  :8848      │      │  :8848      │
└──────────────┘      └──────────────┘      └──────────────┘
        │                    │                     │
        └────────────────────┼────────────────────┘
                             ▼
                    ┌──────────────┐
                    │    MySQL     │
                    │  (主从复制)   │
                    └──────────────┘

6.2 集群配置

yaml 复制代码
# cluster.conf - 节点配置
# 在每个Nacos节点的conf目录下配置
192.168.1.1:8848
192.168.1.2:8848
192.168.1.3:8848
yaml 复制代码
# application.properties - 应用配置
server.port=8848

# 数据源配置(使用MySQL持久化)
spring.datasource.platform=mysql
db.num=2
db.url.0=jdbc:mysql://192.168.1.10:3306/nacos?characterEncoding=utf8&connectTimeout=1000
db.url.1=jdbc:mysql://192.168.1.11:3306/nacos?characterEncoding=utf8&connectTimeout=1000
db.user=nacos
db.password=nacos

# 集群节点配置
nacos.inetutils.prefer-hostname-over-ip=false
nacos.inetutils.ip-address=192.168.1.1

# Raft协议配置
nacos.core.protocol.raft.data=file
nacos.core.protocol.raft.min-election-timeout=10000
nacos.core.protocol.raft.max-election-timeout=15000

6.3 客户端接入集群

yaml 复制代码
# 服务端接入Nacos集群
spring:
  cloud:
    nacos:
      discovery:
        # 配置所有Nacos节点地址
        server-addr: 192.168.1.1:8848,192.168.1.2:8848,192.168.1.3:8848
        
# Nacos控制台集群管理页面查看状态:
# http://192.168.1.1:8848/nacos/#/clusterManagement

6.4 健康检查机制

bash 复制代码
# 检查Nacos集群健康状态
curl http://192.168.1.1:8848/nacos/v1/console/health/readiness

# 返回:
{
  "status": "UP"
}

# 检查leader节点
curl http://192.168.1.1:8848/nacos/v1/ns/raft/leader

# 返回:
{
  "leader": "192.168.1.1:7848"
}

七、配置中心:应用的"中央控制室"

7.1 Nacos配置管理模型

复制代码
Nacos配置层次:
  
命名空间(Namespace)
    │
    └── 分组(Group)
            │
            └── Data ID
                    │
                    └── 配置内容(JSON/YAML/Properties)

命名空间场景:
  - dev(开发环境)
  - test(测试环境)
  - uat(预发环境)
  - prod(生产环境)

分组场景:
  - DEFAULT_GROUP(默认分组)
  - ORDER_GROUP(订单相关配置)
  - INVENTORY_GROUP(库存相关配置)

7.2 配置发布

java 复制代码
// 方式1:控制台发布
# 在Nacos控制台 → 配置管理 → 配置列表 → 新建配置
# Data ID: order-service.yaml
# Group: DEFAULT_GROUP
# Namespace: prod
# 配置内容:
order:
  timeout: 5000
  max-retry: 3
  payment-url: http://payment-service/api
java 复制代码
// 方式2:OpenAPI发布
@RestController
public class ConfigController {
    
    @Autowired
    private ConfigService configService;
    
    @GetMapping("/publish-config")
    public String publishConfig() throws NacosException {
        String dataId = "order-service.yaml";
        String group = "DEFAULT_GROUP";
        String content = "order:\n  timeout: 5000";
        
        configService.publishConfig(
            dataId,
            group,
            content
        );
        
        return "配置发布成功";
    }
    
    @GetMapping("/get-config")
    public String getConfig() throws NacosException {
        String config = configService.getConfig(
            "order-service.yaml",
            "DEFAULT_GROUP",
            5000
        );
        
        return config;
    }
}

7.3 动态配置监听

java 复制代码
// @RefreshScope实现配置热更新
@RestController
@RefreshScope  // 当Nacos配置变化时,Bean自动重建
public class OrderConfigController {
    
    // 配置变化时,这个值会自动更新
    @Value("${order.timeout:3000}")
    private int orderTimeout;
    
    @Value("${order.max-retry:3}")
    private int maxRetry;
    
    @GetMapping("/config")
    public Map<String, Object> getConfig() {
        Map<String, Object> config = new HashMap<>();
        config.put("timeout", orderTimeout);
        config.put("maxRetry", maxRetry);
        config.put("refreshTime", new Date());
        return config;
    }
}

7.4 多环境配置共享

yaml 复制代码
# 共享配置
spring:
  application:
    name: order-service
  cloud:
    nacos:
      config:
        # 共享配置(所有环境共享)
        shared-configs:
          - data-id: common.yaml
            group: DEFAULT_GROUP
            refresh: true
          - data-id: log.yaml
            group: DEFAULT_GROUP
            refresh: true
        # 扩展配置(当前环境私有)
        # - data-id: ${spring.application.name}-${spring.profiles.active}.yaml
        #   group: ${spring.cloud.nacos.config.group}
        #   refresh: true

八、生产环境最佳实践

8.1 保护注册中心

yaml 复制代码
# Nacos访问控制
spring:
  cloud:
    nacos:
      discovery:
        # 命名空间隔离(不同环境用不同命名空间)
        namespace: ${NACOS_NAMESPACE:prod}
        
      config:
        namespace: ${NACOS_NAMESPACE:prod}

# 开启认证
nacos.core.auth.enabled=true
nacos.core.auth.server.identity.key=${NACOS_AUTH_KEY:yourKey}
nacos.core.auth.server.identity.value=${NACOS_AUTH_VALUE:yourValue}
yaml 复制代码
# 服务端连接认证
spring:
  cloud:
    nacos:
      discovery:
        username: ${NACOS_USERNAME:nacos}
        password: ${NACOS_PASSWORD:nacos}
      config:
        username: ${NACOS_USERNAME:nacos}
        password: ${NACOS_PASSWORD:nacos}

8.2 优雅下线

java 复制代码
// 应用关闭时先从Nacos注销
@Component
public class NacosGracefulShutdown implements DisposableBean {
    
    @Autowired
    private NamingService namingService;
    
    @Value("${spring.cloud.client.ip-address}")
    private String ip;
    
    @Value("${server.port}")
    private int port;
    
    @Value("${spring.application.name}")
    private String serviceName;
    
    @PreDestroy
    public void deregister() {
        try {
            // 1. 从注册中心注销
            namingService.deregisterInstance(serviceName, ip, port);
            
            // 2. 等待一下,确保Nacos已处理
            Thread.sleep(3000);
            
            System.out.println("服务已从Nacos注销: " + serviceName);
        } catch (Exception e) {
            System.err.println("注销服务失败: " + e.getMessage());
        }
    }
}

8.3 监控告警

yaml 复制代码
# Nacos监控指标(Prometheus)
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true
yaml 复制代码
# Prometheus告警规则
groups:
  - name: nacos-alerts
    rules:
      - alert: NacosInstanceDown
        expr: up{job="nacos"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Nacos节点不可用"
          
      - alert: NacosServiceDown
        expr: nacos_service_count == 0
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "注册服务数量为0,可能有问题"

九、踩坑实录

坑1:Nacos注册的服务名带了下划线

某次部署,发现服务注册到Nacos后,消费者死活调用不到。

原因 :Spring Boot的application.name是order_service(带下划线),Nacos注册后消费者找不到。

教训:服务名统一使用横杠(order-service),不要用下划线。Ribbon和Feign默认用横杠匹配。

坑2:Nacos集群脑裂

三节点Nacos集群,网络抖动后,出现两个Leader,数据不一致。

解决:使用大于3的奇数节点(3、5、7),配置合理的Raft超时时间。

教训:Nacos使用Raft协议,需要合理的选举超时配置。

坑3:配置热更新没生效

修改了Nacos配置,控制台显示已更新,但应用没生效。

原因:Bean没有加@RefreshScope注解,或者@Value注入的字段是static的。

教训:@RefreshScope只能作用域实例化的Bean,static字段不会热更新。

坑4:临时实例被误删

某次发布,服务重启时Nacos心跳断了,旧实例被删,新实例还没注册完成,出现短暂的调用失败。

解决:提高心跳超时时间,或使用持久化实例。

教训:发布时先注册新实例,等新实例就绪后再下线旧实例(滚动发布)。


十、总结

服务注册与发现是微服务架构的基础:

  • Nacos:集服务注册、配置中心于一身,阿里开源,Spring Cloud Alibaba默认集成
  • 服务注册:心跳机制保障服务可用性
  • 服务发现:动态获取服务实例列表,无需硬编码
  • 健康检查:自动剔除故障实例,防止调用失败
  • 配置中心:配置集中管理,热更新无需重启
  • 集群部署:Nacos本身也需要高可用部署

最佳实践:

  1. 生产环境Nacos集群至少3节点
  2. 使用命名空间隔离不同环境
  3. 服务下线前先从注册中心注销
  4. 配置使用@RefreshScope实现热更新
  5. 开启Nacos认证,防止未授权访问
  6. 监控Nacos健康状态和注册实例数

血的教训:

不要以为服务注册后就万事大吉了。注册中心本身就是单点,需要集群保障高可用。同时,客户端要有容错机制(重试、熔断),不能完全依赖注册中心的健康检查。

思考题: 你们的微服务用的是Nacos、Eureka还是Consul?为什么?服务下线时的优雅注销做了吗?


个人观点,仅供参考

相关推荐
qcx2331 分钟前
【系统学AI】09 Multi-Agent架构(2026版):从学术理论到工业级实践
java·人工智能·架构·multi-agent·claude agent
wb043072011 小时前
厨房质检员——从阿明的“祖传配方“到标准化质检,看测试金字塔的落地
架构·log4j
Dongwoo Jeong1 小时前
微服务架构(MSA)是如何诞生的?
微服务·云原生·架构
Cheng小攸1 小时前
综合实验2
网络·windows
Soari2 小时前
SSH 主机密钥冲突
运维·网络·ssh
张忠琳2 小时前
【kubernetes v1.21】(kubelet 1)Kubelet 核心架构与启动流程
云原生·架构·kubernetes·kubelet
用户987409238872 小时前
超算中心 高性能计算 htc命令module use的作用
架构
AI科技星3 小时前
基于**v=c(空间光速螺旋运动)唯一第一性原理**重新完整求导证明
人工智能·线性代数·算法·机器学习·架构·概率论·学习方法
__log3 小时前
如何优雅地“借鉴”任何网站的设计系统
人工智能·架构·知识图谱
且听风吟_xincell3 小时前
用 TypeScript 从零写一个 TCP 聊天室(上)—— 网络编程入门实战
网络·tcp/ip·typescript