【架构实战】服务注册与发现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?为什么?服务下线时的优雅注销做了吗?


个人观点,仅供参考

相关推荐
薛定猫AI1 小时前
【深度解析】Gemma Chat:基于 MLX 的 Mac 离线 Coding Agent 架构与实战
macos·架构
星梦清河1 小时前
微服务-Elasticsearch01
elasticsearch·微服务·架构
MrSYJ1 小时前
到底怎么使用nginx配置一个前后端分离的项目
微服务·云原生·架构
xixixi777771 小时前
《从心理诱导突破Claude到AI仿冒直播首张拘留单:AI安全、监管与商用的三重转折点》
大数据·网络·人工智能·安全·ai·大模型·风险
源远流长jerry1 小时前
TCP 连接队列解析:从 listen () 到 connect ()
linux·服务器·网络·网络协议·tcp/ip
Xpower 171 小时前
从PHM到AI Agent-如何用OpenClaw构建设备健康诊断智能体
网络·人工智能·学习·算法
小小仙。2 小时前
IT自学第四十三天(微服务登录认证)
运维·微服务·架构
2301_780789662 小时前
2025年服务器漏洞生存指南:从应急响应到长效免疫的实战框架
网络·安全·web安全·架构·ddos
xcjbqd02 小时前
SAP硬件选择详解:服务器、存储与网络的全面解析
运维·服务器·网络