核心概念

自我保护机制是服务注册中心的一种容错机制,当网络出现异常或服务大量下线时,防止注册中心误删健康的服务实例。Nacos的自我保护机制主要针对临时实例(AP模式),在心跳失败率超过阈值时触发。

核心思想:宁可保留可能不健康的实例,也不删除可能健康的实例,保证服务的高可用性。

自我保护触发条件

1. 心跳失败率阈值

// Nacos Server配置
nacos:
  naming:
    # 自我保护阈值心跳失败率超过此值触发保护
    protect-threshold: 0.85  # 默认0.85即85%
    
    # 健康检查任务执行间隔
    health-check-interval: 5000  # 5

触发条件

  • 计算所有临时实例的心跳失败率
  • 如果失败率 > protect-threshold(默认85%),触发自我保护
  • 自我保护期间,不再自动删除实例

2. 心跳失败率计算

// ServiceManager.java - 计算心跳失败率
public class ServiceManager {
    private static final double DEFAULT_PROTECT_THRESHOLD = 0.85;
    
    /**
     * 计算服务的心跳失败率
     */
    public double getHeartBeatFailRate(Service service) {
        int totalInstances = 0;
        int failInstances = 0;
        
        for (Cluster cluster : service.getClusterMap().values()) {
            Set<Instance> instances = cluster.getEphemeralInstances();
            
            for (Instance instance : instances) {
                totalInstances++;
                
                // 检查是否心跳超时
                long currentTime = System.currentTimeMillis();
                long lastBeat = instance.getLastBeat();
                long heartBeatTimeout = instance.getHeartBeatTimeout();
                
                if (currentTime - lastBeat > heartBeatTimeout) {
                    // 心跳超时,计入失败
                    failInstances++;
                }
            }
        }
        
        if (totalInstances == 0) {
            return 0.0;
        }
        
        // 失败率 = 失败实例数 / 总实例数
        return (double) failInstances / totalInstances;
    }
    
    /**
     * 判断是否触发自我保护
     */
    public boolean isProtectEnabled(Service service) {
        double protectThreshold = getProtectThreshold(service);
        double failRate = getHeartBeatFailRate(service);
        
        // 失败率超过阈值,触发自我保护
        return failRate >= protectThreshold;
    }
}

自我保护机制实现

1. 健康检查任务改造

// ServiceManager.java - 健康检查任务(带自我保护)
private void healthCheck() {
    for (Service service : getAllServices()) {
        // 1. 检查是否触发自我保护
        boolean protectEnabled = isProtectEnabled(service);
        
        if (protectEnabled) {
            // 自我保护模式:只标记不健康,不删除实例
            log.warn("Service {} is in protect mode, fail rate: {}", 
                service.getName(), getHeartBeatFailRate(service));
            
            // 只标记不健康,不删除
            markUnhealthyInstances(service);
        } else {
            // 正常模式:标记不健康并删除超时实例
            markUnhealthyInstances(service);
            removeTimeoutInstances(service);
        }
    }
}

/**
 * 标记不健康的实例
 */
private void markUnhealthyInstances(Service service) {
    for (Cluster cluster : service.getClusterMap().values()) {
        Set<Instance> instances = cluster.getEphemeralInstances();
        
        for (Instance instance : instances) {
            long currentTime = System.currentTimeMillis();
            long lastBeat = instance.getLastBeat();
            long heartBeatTimeout = instance.getHeartBeatTimeout();
            
            // 心跳超时,标记不健康
            if (currentTime - lastBeat > heartBeatTimeout) {
                if (instance.isHealthy()) {
                    instance.setHealthy(false);
                    log.warn("Instance {} marked as unhealthy", instance);
                    
                    // 触发健康状态变更事件
                    NotifyCenter.publishEvent(new InstanceHealthStatusEvent(instance));
                }
            } else {
                // 心跳正常,恢复健康
                if (!instance.isHealthy()) {
                    instance.setHealthy(true);
                    log.info("Instance {} recovered to healthy", instance);
                }
            }
        }
    }
}

/**
 * 删除超时实例(仅在非自我保护模式下执行)
 */
private void removeTimeoutInstances(Service service) {
    for (Cluster cluster : service.getClusterMap().values()) {
        Set<Instance> instances = cluster.getEphemeralInstances();
        List<Instance> toRemove = new ArrayList<>();
        
        for (Instance instance : instances) {
            long currentTime = System.currentTimeMillis();
            long lastBeat = instance.getLastBeat();
            long ipDeleteTimeout = instance.getIpDeleteTimeout();
            
            // 超过删除超时时间,加入删除列表
            if (currentTime - lastBeat > ipDeleteTimeout) {
                toRemove.add(instance);
            }
        }
        
        // 删除超时实例
        for (Instance instance : toRemove) {
            removeInstance(service, instance);
            log.warn("Instance {} removed due to timeout", instance);
        }
    }
}

2. 自我保护状态管理

// ServiceManager.java - 自我保护状态
public class ServiceManager {
    // 服务级别的自我保护状态
    private final Map<String, Boolean> protectStatusMap = new ConcurrentHashMap<>();
    
    /**
     * 获取服务的自我保护状态
     */
    public boolean isServiceProtectEnabled(String namespaceId, String serviceName) {
        String key = buildKey(namespaceId, serviceName);
        return protectStatusMap.getOrDefault(key, false);
    }
    
    /**
     * 更新自我保护状态
     */
    private void updateProtectStatus(Service service) {
        String key = buildKey(service.getNamespaceId(), service.getName());
        boolean protectEnabled = isProtectEnabled(service);
        
        boolean oldStatus = protectStatusMap.getOrDefault(key, false);
        protectStatusMap.put(key, protectEnabled);
        
        // 状态变更时记录日志
        if (oldStatus != protectEnabled) {
            if (protectEnabled) {
                log.warn("Service {} entered protect mode, fail rate: {}", 
                    service.getName(), getHeartBeatFailRate(service));
            } else {
                log.info("Service {} exited protect mode", service.getName());
            }
        }
    }
}

自我保护触发场景

1. 网络分区

场景:Nacos Server与部分Client网络不通

正常Client: 心跳正常 ✅
异常Client: 心跳失败 ❌

结果:
- 心跳失败率 > 85%
- 触发自我保护
- 异常Client的实例不会被删除
- 网络恢复后,实例自动恢复

2. 服务大量下线

场景:服务实例批量重启或故障

正常实例: 100个
故障实例: 90个

心跳失败率 = 90 / 100 = 90% > 85%

结果:
- 触发自我保护
- 故障实例不会被删除
- 等待服务恢复或手动处理

3. Nacos Server负载过高

场景:Nacos Server处理心跳请求变慢

Client发送心跳 → Server处理延迟 → 心跳超时

结果:
- 大量心跳被认为超时
- 心跳失败率上升
- 触发自我保护
- 防止误删健康实例

自我保护配置

1. 服务级别配置

// 通过元数据配置服务的自我保护阈值
spring:
  cloud:
    nacos:
      discovery:
        metadata:
          # 自定义自我保护阈值0-1之间
          protect.threshold: 0.8

2. 全局配置

# Nacos Server配置文件 application.properties
# 全局自我保护阈值
nacos.naming.protect.threshold=0.85

# 是否启用自我保护(默认true)
nacos.naming.protect.enabled=true

3. 动态配置

// 通过Nacos控制台或API动态调整
@RestController
@RequestMapping("/nacos/admin")
public class NacosAdminController {
    @PostMapping("/protect/threshold")
    public String setProtectThreshold(@RequestParam String serviceName, 
                                      @RequestParam double threshold) {
        // 动态设置服务的自我保护阈值
        serviceManager.setProtectThreshold(serviceName, threshold);
        return "ok";
    }
    
    @PostMapping("/protect/enable")
    public String enableProtect(@RequestParam String serviceName, 
                                @RequestParam boolean enabled) {
        // 动态启用/禁用自我保护
        serviceManager.setProtectEnabled(serviceName, enabled);
        return "ok";
    }
}

自我保护 vs Eureka自我保护

1. 触发条件对比

维度 Nacos Eureka
触发条件 心跳失败率 > 85% 15分钟内续约失败率 > 15%
计算方式 实时计算 15分钟窗口
阈值配置 可配置(默认85%) 固定15%
作用范围 服务级别 全局

2. 保护策略对比

Nacos自我保护

// 服务级别保护
if (isProtectEnabled(service)) {
    // 只标记不健康,不删除
    markUnhealthyInstances(service);
} else {
    // 正常删除超时实例
    removeTimeoutInstances(service);
}

Eureka自我保护

// 全局保护
if (isSelfPreservationModeEnabled()) {
    // 停止所有实例的过期删除
    // 但会继续标记不健康
    return;  // 不执行删除逻辑
}

3. 恢复机制对比

Nacos

  • 心跳失败率降低后,自动退出自我保护
  • 恢复正常删除逻辑
  • 恢复速度更快(实时计算)

Eureka

  • 15分钟内续约成功率恢复后,自动退出
  • 需要等待15分钟窗口
  • 恢复速度较慢

性能优化与监控

1. 自我保护状态缓存

// 缓存自我保护状态,减少重复计算
public class ServiceManager {
    private final Map<String, CachedProtectStatus> protectStatusCache = new ConcurrentHashMap<>();
    
    private static class CachedProtectStatus {
        private boolean protectEnabled;
        private long lastUpdateTime;
        private static final long CACHE_TTL = 5000;  // 5秒缓存
        
        public boolean isValid() {
            return System.currentTimeMillis() - lastUpdateTime < CACHE_TTL;
        }
    }
    
    public boolean isProtectEnabled(Service service) {
        String key = buildKey(service);
        CachedProtectStatus cached = protectStatusCache.get(key);
        
        if (cached != null && cached.isValid()) {
            return cached.protectEnabled;
        }
        
        // 重新计算
        boolean protectEnabled = calculateProtectStatus(service);
        protectStatusCache.put(key, new CachedProtectStatus(protectEnabled));
        
        return protectEnabled;
    }
}

2. 自我保护监控指标

// 暴露自我保护相关指标
public class ProtectMetrics {
    private final MeterRegistry meterRegistry;
    
    public void recordProtectStatus(Service service, boolean protectEnabled, double failRate) {
        // 记录自我保护状态
        meterRegistry.gauge("nacos.protect.enabled",
            Tags.of("service", service.getName()),
            protectEnabled ? 1 : 0
        );
        
        // 记录心跳失败率
        meterRegistry.gauge("nacos.heartbeat.fail.rate",
            Tags.of("service", service.getName()),
            failRate
        );
        
        // 记录自我保护触发次数
        if (protectEnabled) {
            meterRegistry.counter("nacos.protect.triggered",
                "service", service.getName()
            ).increment();
        }
    }
}

3. 自我保护告警

// 自我保护触发时发送告警
public class ProtectAlertService {
    public void checkAndAlert(Service service) {
        boolean protectEnabled = serviceManager.isProtectEnabled(service);
        double failRate = serviceManager.getHeartBeatFailRate(service);
        
        if (protectEnabled) {
            // 发送告警
            String message = String.format(
                "Service %s entered protect mode, fail rate: %.2f%%",
                service.getName(), failRate * 100
            );
            
            alertService.sendAlert("NACOS_PROTECT", message, AlertLevel.WARNING);
        }
    }
}

分布式场景考量

1. 集群环境下的自我保护

// 集群环境下,各节点独立计算自我保护状态
public class ServiceManager {
    public void healthCheck() {
        // 每个节点独立计算
        boolean protectEnabled = isProtectEnabled(service);
        
        // 自我保护状态通过Distro协议同步(AP模式)
        if (protectEnabled) {
            // 同步自我保护状态到其他节点
            syncProtectStatus(service, protectEnabled);
        }
    }
    
    private void syncProtectStatus(Service service, boolean protectEnabled) {
        // 通过Distro协议同步到其他节点
        for (Member member : clusterMembers) {
            if (!member.isSelf()) {
                distroProtocol.sync(new ProtectStatusEvent(service, protectEnabled), member);
            }
        }
    }
}

2. 自我保护与数据一致性

AP模式(临时实例)

  • 各节点独立计算自我保护状态
  • 可能出现节点A触发保护,节点B未触发的情况
  • 通过Distro协议最终一致

CP模式(持久化实例)

  • 持久化实例不适用自我保护机制
  • 健康检查结果通过Raft协议强一致

实战示例

1. 配置自我保护阈值

# Nacos Server配置
nacos:
  naming:
    protect-threshold: 0.85  # 85%心跳失败率触发保护
    protect-enabled: true     # 启用自我保护

2. 监控自我保护状态

@RestController
@RequestMapping("/nacos/monitor")
public class ProtectMonitorController {
    @Autowired
    private ServiceManager serviceManager;
    
    @GetMapping("/protect/status")
    public Map<String, Object> getProtectStatus() {
        Map<String, Object> result = new HashMap<>();
        
        for (Service service : serviceManager.getAllServices()) {
            boolean protectEnabled = serviceManager.isProtectEnabled(service);
            double failRate = serviceManager.getHeartBeatFailRate(service);
            
            Map<String, Object> serviceStatus = new HashMap<>();
            serviceStatus.put("protectEnabled", protectEnabled);
            serviceStatus.put("failRate", failRate);
            serviceStatus.put("totalInstances", service.getTotalInstanceCount());
            serviceStatus.put("unhealthyInstances", service.getUnhealthyInstanceCount());
            
            result.put(service.getName(), serviceStatus);
        }
        
        return result;
    }
}

3. 手动退出自我保护

// 紧急情况下,手动退出自我保护
@PostMapping("/protect/disable")
public String disableProtect(@RequestParam String serviceName) {
    serviceManager.setProtectEnabled(serviceName, false);
    log.warn("Manually disabled protect for service: {}", serviceName);
    return "ok";
}

面试总结

Nacos自我保护机制核心要点

  1. 触发条件
    • 心跳失败率 > 85%(可配置)
    • 实时计算,服务级别保护
  2. 保护策略
    • 自我保护期间,只标记不健康,不删除实例
    • 防止网络分区或Server负载过高时误删健康实例
  3. 恢复机制
    • 心跳失败率降低后,自动退出自我保护
    • 恢复正常删除逻辑
  4. 与Eureka对比
    • Nacos:服务级别,实时计算,阈值可配置
    • Eureka:全局级别,15分钟窗口,固定15%阈值
  5. 适用场景
    • 网络分区
    • 服务批量重启
    • Nacos Server负载过高

技术亮点

  • 服务级别的细粒度保护
  • 实时计算,响应更快
  • 可配置阈值,灵活适配不同场景
  • 完善的监控和告警机制

注意事项

  • 自我保护期间,不健康实例不会被删除,需要手动处理
  • 建议配置监控告警,及时发现自我保护触发
  • 根据实际场景调整阈值,平衡可用性和准确性