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 3Nacos Node 2Nacos Node 1ProviderNacos Node 3Nacos Node 2Nacos Node 1Provider客户端心跳续约定时健康检查故障恢复场景注册服务 (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编程工具,助力开发者即刻编程。

更多推荐