Nacos 服务发现与配置中心原理:AP 架构与 Distro 协议

摘要

Nacos 作为阿里巴巴开源的云原生服务发现与配置中心,同时支持 AP 和 CP 两种一致性架构。本文深入剖析 Nacos 2.2.0 的 AP 架构设计及 Distro 协议原理,通过源码分析和实战示例,揭示其如何通过"最终一致性"和"自举机制"实现高可用的服务注册中心,在性能与一致性之间找到最佳平衡点。


一、服务注册中心的挑战与 Nacos 架构

1.1 CAP 理论与注册中心选型

在分布式系统中,Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性)三者只能同时满足两个。

渲染错误: Mermaid 渲染失败: Parse error on line 11: ...] A3[Consul (默认)] ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'

1.2 Nacos 整体架构

客户端

Nacos 存储层

Nacos 集群

注册服务

订阅服务

发送心跳

Distro 同步

Distro 同步

Distro 同步

Nacos Server 1
AP + CP

Nacos Server 2
AP + CP

Nacos Server 3
AP + CP

AP 内存存储
Distro 协议

CP 持久化存储
Raft 协议

服务提供者 Provider

服务消费者 Consumer

1.3 AP vs CP 架构对比

特性 AP 架构 (Distro) CP 架构 (Raft)
一致性 最终一致 强一致
可用性 高(部分节点故障不影响) 中(Leader 故障需选举)
性能 高(内存存储,异步同步) 中(磁盘写入,同步复制)
适用场景 服务注册与发现(非核心配置) 配置管理、元数据
数据存储 内存 磁盘 + 内存

二、Distro 协议核心原理

2.1 Distro 设计理念

Distro 是 Nacos 自研的最终一致性协议,专为 AP 场景设计:

  1. 自举机制:每个节点平等,无 Leader 角色
  2. 增量同步:仅同步变更数据,减少网络开销
  3. 故障自愈:节点故障后自动从其他节点恢复数据
  4. 心跳续约:客户端定期续约,服务端定期清理过期实例
Nacos Node 3 Nacos Node 2 Nacos Node 1 Provider Nacos Node 3 Nacos Node 2 Nacos Node 1 Provider 客户端心跳续约 定时健康检查 故障恢复场景 注册服务 (instance) 写入本地内存 同步注册信息 (异步) 同步注册信息 (异步) 心跳 (beat) 更新实例最后心跳时间 扫描过期实例(20秒) 同步删除操作 同步删除操作 请求数据全量(N3 重启) 返回所有实例数据

2.2 Distro 数据同步流程

注册

注销

心跳

timestamp 更新

timestamp 旧

客户端请求

写入类型?

写入本地内存

从本地删除

更新心跳时间

构建 Distro 协议包
timestamp + data

异步广播到其他节点

接收节点校验

应用更新

丢弃旧数据


三、源码分析:AP 架构实现

3.1 服务注册入口

源码位置nacos-src/naming/src/main/java/com/alibaba/nacos/naming/

// naming/src/main/java/com/alibaba/nacos/naming/core/InstanceController.java

@RestController
@RequestMapping("/v1/ns")
public class InstanceController {
    
    @CanDistro
    @PostMapping("/instance")
    public String register(HttpServletRequest request) throws Exception {
        // 1. 解析请求参数
        String serviceName = WebUtils.required(request, "serviceName");
        String ip = WebUtils.required(request, "ip");
        int port = Integer.parseInt(WebUtils.required(request, "port"));
        
        // 2. 构建 Instance 对象
        Instance instance = new Instance();
        instance.setIp(ip);
        instance.setPort(port);
        instance.setWeight(Double.parseDouble(WebUtils.optional(request, "weight", "1.0")));
        instance.setHealthy(true);
        instance.setEphemeral(true); // 临时实例
        
        // 3. 注册服务
        getInstanceOperator().registerInstance(serviceName, "DEFAULT_GROUP", instance);
        
        return "ok";
    }
}

// naming/src/main/java/com/alibaba/nacos/naming/core/InstanceOperatorServiceImpl.java

public class InstanceOperatorServiceImpl implements InstanceOperator {
    
    @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) {
        // 1. 构建完整服务名
        String fullServiceName = groupName + Constants.SERVICE_INFO_SPLITER + serviceName;
        
        // 2. 写入 AP 存储(Distro 协议)
        DistroConsistencyServiceImpl distroConsistencyService = 
            (DistroConsistencyServiceImpl) consistencyService;
        
        // 3. 注册实例(写入内存)
        distroConsistencyService.put(fullServiceName, instance);
        
        // 4. 广播到其他节点
        distroProtocol.sync(new DistroKey(fullServiceName, instance.getIp() + ":" + instance.getPort()), 
                           DataOperation.CHANGE);
    }
}

3.2 Distro 数据存储

源码位置naming/src/main/java/com/alibaba/nacos/naming/consistency/distro/

// naming/src/main/java/com/alibaba/nacos/naming/consistency/distro/DistroConsistencyServiceImpl.java

public class DistroConsistencyServiceImpl implements ConsistencyService {
    
    // 本地数据存储(内存)
    private final ConcurrentHashMap<String, Datum> dataStore = new ConcurrentHashMap<>(1024);
    
    // 监听器集合
    private final ConcurrentHashMap<String, RecordListener> listeners = new ConcurrentHashMap<>(1024);
    
    @Override
    public void put(String key, Record value) throws NacosException {
        // 1. 构建 Datum(带时间戳的数据)
        Datum datum = new Datum();
        datum.key = key;
        datum.value = value;
        datum.timestamp = System.currentTimeMillis(); // 关键:时间戳用于版本控制
        
        // 2. 写入本地内存
        dataStore.put(key, datum);
        
        // 3. 通知监听器(触发推送到客户端)
        notifyListener(key, DataOperation.CHANGE);
        
        // 4. 广播到其他节点
        distroProtocol.sync(new DistroKey(key, value), DataOperation.CHANGE);
    }
    
    @Override
    public Datum get(String key) {
        // 从本地内存读取
        return dataStore.get(key);
    }
    
    @Override
    public void remove(String key) {
        // 1. 从本地内存删除
        dataStore.remove(key);
        
        // 2. 广播删除操作到其他节点
        distroProtocol.sync(new DistroKey(key, null), DataOperation.DELETE);
        
        // 3. 通知监听器
        notifyListener(key, DataOperation.DELETE);
    }
}

3.3 Distro 协议同步

源码位置naming/src/main/java/com/alibaba/nacos/naming/core/DistroProtocol.java

// naming/src/main/java/com/alibaba/nacos/naming/core/DistroProtocol.java

public class DistroProtocol {
    
    // 同步数据到其他节点
    public void sync(DistroKey key, DataOperation action) {
        // 1. 遍历所有集群节点(除自己)
        for (Member member : memberManager.allMembersWithoutSelf()) {
            // 2. 异步同步(不阻塞主流程)
            distroTaskEngineHolder.getExecuteWorkers().get(member.getAddress())
                .addTask(new DistroSyncTask(key, action, member.getAddress()));
        }
    }
}

// naming/src/main/java/com/alibaba/nacos/naming/core/DistroSyncTask.java

public class DistroSyncTask implements Runnable {
    
    @Override
    public void run() {
        try {
            // 1. 构建 HTTP 请求
            String url = "http://" + targetServer + "/v1/ns/distro/datum";
            
            // 2. 序列化数据
            Map<String, Object> data = new HashMap<>();
            data.put("key", distroKey.getKey());
            data.put("timestamp", System.currentTimeMillis());
            
            // 3. 发送 HTTP 请求
            RestResult<String> result = restTemplate.post(url, data);
            
            if (result.ok()) {
                log.info("同步成功: {}", distroKey);
            }
        } catch (Exception e) {
            log.error("同步失败: {}", distroKey, e);
        }
    }
}

3.4 版本冲突处理

// naming/src/main/java/com/alibaba/nacos/naming/consistency/distro/DistroConsistencyServiceImpl.java

public void onDatum(DistroDatum datum) {
    // 1. 获取本地数据
    Datum localDatum = dataStore.get(datum.key);
    
    // 2. 版本校验
    if (localDatum != null && localDatum.timestamp > datum.timestamp) {
        // 本地数据更新,丢弃远程数据
        log.warn("丢弃旧数据: local={}, remote={}", localDatum.timestamp, datum.timestamp);
        return;
    }
    
    // 3. 应用远程数据
    dataStore.put(datum.key, datum);
    
    // 4. 通知监听器
    notifyListener(datum.key, DataOperation.CHANGE);
}

四、健康检查与故障自愈

4.1 客户端心跳机制

// 客户端心跳代码(Spring Cloud Alibaba Nacos Discovery)

@Configuration
public class NacosDiscoveryClientConfig {
    
    @Bean
    public NacosNamingService nacosNamingService() {
        Properties properties = new Properties();
        properties.put("serverAddr", "127.0.0.1:8848");
        properties.put("namespace", "public");
        
        return new NacosNamingService(properties);
    }
    
    @Scheduled(fixedRate = 5000) // 每 5 秒发送心跳
    public void sendBeat() {
        try {
            Instance instance = new Instance();
            instance.setIp(InetAddress.getLocalHost().getHostAddress());
            instance.setPort(8080);
            instance.setServiceName("user-service");
            
            // 发送心跳
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName("user-service");
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setPeriod(5000); // 心跳间隔
            
            nacosNamingService.sendBeat(beatInfo);
        } catch (Exception e) {
            log.error("心跳发送失败", e);
        }
    }
}

4.2 服务端健康检查

// naming/src/main/java/com/alibaba/nacos/naming/healthcheck/ClientBeatCheckTask.java

public class ClientBeatCheckTask implements Runnable {
    
    @Override
    public void run() {
        try {
            // 1. 获取所有服务实例
            Collection<Service> services = serviceManager.getAllServices();
            
            for (Service service : services) {
                // 2. 获取服务的所有实例
                List<Instance> instances = service.allIPs();
                
                for (Instance instance : instances) {
                    // 3. 检查实例是否健康
                    if (instance.isEphemeral()) { // 临时实例需要健康检查
                        if (System.currentTimeMillis() - instance.getLastBeat() > HEARTBEAT_TIMEOUT) {
                            // 超过 20 秒未收到心跳,标记为不健康
                            instance.setHealthy(false);
                            
                            // 通知客户端
                            distroConsistencyService.put(service.getName(), instance);
                        }
                        
                        if (System.currentTimeMillis() - instance.getLastBeat() > IP_DELETE_TIMEOUT) {
                            // 超过 30 秒未收到心跳,删除实例
                            distroConsistencyService.remove(service.getName(), instance);
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.error("健康检查失败", e);
        }
    }
}

4.3 故障自愈机制

需要更新

已是最新

Nacos 节点故障/重启

节点恢复

启动 Distro 协议

向其他节点请求数据

数据版本校验

拉取最新数据

跳过

写入本地内存

开始正常提供服务

// naming/src/main/java/com/alibaba/nacos/naming/core/DistroProtocol.java

public void loadDatumFromRemote() {
    // 1. 遍历所有其他节点
    for (Member member : memberManager.allMembersWithoutSelf()) {
        try {
            // 2. 请求全量数据
            String url = "http://" + member.getAddress() + "/v1/ns/distro/datums";
            RestResult<Map<String, Datum>> result = restTemplate.get(url);
            
            if (result.ok()) {
                Map<String, Datum> datums = result.getData();
                
                // 3. 写入本地内存
                for (Datum datum : datums.values()) {
                    distroConsistencyService.onDatum(datum);
                }
                
                log.info("从节点 {} 恢复数据成功", member.getAddress());
                break;
            }
        } catch (Exception e) {
            log.error("从节点 {} 恢复数据失败", member.getAddress(), e);
        }
    }
}

五、配置中心原理

5.1 配置推送流程

持久化存储 Nacos Server 客户端 持久化存储 Nacos Server 客户端 发布配置 (dataId, group, content) 写入配置(持久化) 写入成功 生成配置 MD5 查询订阅该配置的客户端 推送配置变更(长轮询) 更新本地缓存 @RefreshScope 触发 Bean 刷新

5.2 长轮询机制

// 客户端长轮询代码(Spring Cloud Alibaba Nacos Config)

public class ConfigRpcTransportClient implements TransportClient {
    
    public String getConfig(String dataId, String group, long timeoutMs) throws NacosException {
        // 1. 检查本地缓存
        String cacheKey = GroupKey.getKey(dataId, group);
        CacheItem cacheItem = cacheMap.get(cacheKey);
        
        if (cacheItem != null) {
            // 2. 检查本地 MD5
            String localMd5 = MD5Utils.md5Hex(cacheItem.content);
            
            // 3. 向服务端发起长轮询
            ConfigQueryResponse response = server.queryConfig(dataId, group, localMd5, timeoutMs);
            
            if (response.isChanged()) {
                // 配置变更,更新本地缓存
                cacheItem.content = response.getContent();
                cacheItem.md5 = response.getMd5();
                
                // 通知监听器
                notifyListener(dataId, group, response.getContent());
                
                return response.getContent();
            }
        }
        
        return cacheItem.content;
    }
}

5.3 配置存储结构

-- Nacos 配置表
CREATE TABLE `config_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `data_id` varchar(255) NOT NULL COMMENT '配置ID',
  `group_id` varchar(128) DEFAULT NULL COMMENT '分组ID',
  `content` longtext NOT NULL COMMENT '配置内容',
  `md5` varchar(32) DEFAULT NULL COMMENT 'MD5值',
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `src_user` varchar(128) DEFAULT NULL,
  `src_ip` varchar(50) DEFAULT NULL,
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段(对应命名空间ID)',
  `c_desc` varchar(256) DEFAULT NULL,
  `c_use` varchar(64) DEFAULT NULL,
  `effect` varchar(64) DEFAULT NULL,
  `type` varchar(64) DEFAULT NULL,
  `c_schema` text,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 配置历史表
CREATE TABLE `his_config_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `nid` bigint(20) NOT NULL AUTO_INCREMENT,
  `data_id` varchar(255) NOT NULL,
  `group_id` varchar(128) NOT NULL,
  `app_name` varchar(128) DEFAULT NULL,
  `content` longtext NOT NULL,
  `md5` varchar(32) DEFAULT NULL,
  `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `src_user` varchar(128) DEFAULT NULL,
  `src_ip` varchar(50) DEFAULT NULL,
  `op_type` char(10) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

六、实战:Spring Cloud Alibaba Nacos

6.1 服务提供者配置

# application.yml
spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: public
        group: DEFAULT_GROUP
        register-enabled: true
        ephemeral: true  # 临时实例(AP 模式)
        heart-beat-interval: 5000  # 心跳间隔
        heart-beat-timeout: 15000  # 心跳超时
        ip-delete-timeout: 30000   # 删除超时
        metadata:
          version: 1.0.0
          region: beijing
// 启动类
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

6.2 服务消费者配置

# application.yml
spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: public
        group: DEFAULT_GROUP
// 服务调用
@RestController
public class OrderController {
    
    @Autowired
    private LoadBalancerClient loadBalancerClient;
    
    @GetMapping("/order/create")
    public String createOrder() {
        // 1. 从 Nacos 获取服务实例
        ServiceInstance instance = loadBalancerClient.choose("user-service");
        
        // 2. 发起 HTTP 调用
        String url = instance.getUri() + "/user/get";
        RestTemplate restTemplate = new RestTemplate();
        String result = restTemplate.getForObject(url, String.class);
        
        return "订单创建成功,用户信息:" + result;
    }
}

6.3 配置中心集成

# bootstrap.yml
spring:
  application:
    name: user-service
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        namespace: public
        group: DEFAULT_GROUP
        file-extension: yaml  # 配置文件格式
        refresh-enabled: true # 启用自动刷新
        shared-configs:
          - dataId: common.yaml  # 共享配置
            group: DEFAULT_GROUP
            refresh: true
// 使用配置
@RestController
@RefreshScope  // 支持配置动态刷新
public class ConfigController {
    
    @Value("${user.title}")
    private String title;
    
    @GetMapping("/config")
    public String getConfig() {
        return "配置信息:" + title;
    }
}

七、性能优化与最佳实践

7.1 集群部署规划

节点数 推荐配置 说明
3 节点 2C4G 最小集群(允许 1 节点故障)
5 节点 4C8G 生产推荐(允许 2 节点故障)
7 节点+ 8C16G 大规模集群(允许 3 节点故障)

7.2 性能调优参数

# application.properties
# 服务注册性能优化
nacos.naming.distro.taskDispatchPeriod=200  # 同步任务派发周期
nacos.naming.distro.taskRetryDelay=5000     # 任务重试延迟
nacos.naming.distro.batchSyncKeyCount=1000  # 批量同步数量

# 健康检查优化
nacos.naming.expireInstance.period=5000     # 过期实例扫描周期

# 配置中心优化
nacos.config.notify.connectTimeout=5000     # 配置推送超时

7.3 避免雪崩效应

// ❌ 错误:同步调用导致雪崩
@FeignClient(name = "user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    User getUser(@PathVariable Long id);  // 同步调用
}

// ✅ 正确:使用异步 + 熔断
@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
    @GetMapping("/user/{id}")
    CompletableFuture<User> getUserAsync(@PathVariable Long id);  // 异步调用
}

// 降级处理
public class UserClientFallback implements UserClient {
    @Override
    public CompletableFuture<User> getUserAsync(Long id) {
        return CompletableFuture.completedFuture(new User(id, "默认用户"));
    }
}

八、版本演进:Nacos 2.2.0 新特性

8.1 gRPC 协议支持

# Nacos 2.x 默认使用 gRPC(性能优于 HTTP 1.x)
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        grpc-port: 9848  # gRPC 端口

8.2 访问控制优化

// Nacos 2.2.0 支持基于角色的访问控制(RBAC)
@Configuration
public class NacosSecurityConfig {
    
    @Bean
    public Properties nacosProperties() {
        Properties properties = new Properties();
        properties.put("username", "nacos");
        properties.put("password", "nacos");
        properties.put("accessKey", "your-access-key");
        properties.put("secretKey", "your-secret-key");
        return properties;
    }
}

九、总结

Nacos AP 架构通过以下机制实现高可用服务发现:

  1. Distro 协议:自举机制,无 Leader 单点故障
  2. 内存存储:高性能读写,异步持久化
  3. 增量同步:减少网络开销,提升同步效率
  4. 故障自愈:节点恢复后自动从其他节点拉取数据
  5. 健康检查:自动剔除不健康实例

AP 模式适用场景

  • 服务注册与发现(非核心元数据)
  • 对可用性要求高于一致性
  • 允许短暂的不一致窗口

CP 模式适用场景

  • 配置中心(配置变更需要强一致)
  • 核心元数据存储(如 Leader 选举)

参考资料

  • Nacos 官方文档:https://nacos.io/zh-cn/
  • Nacos GitHub 源码(2.2.0):https://github.com/alibaba/nacos/tree/2.2.0
  • 《Spring Cloud Alibaba 微服务原理与实战》第4章:服务注册与发现

标签:Nacos,服务发现,配置中心,AP架构,Distro协议

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐