问题引入

面试官看着你简历上的"Spring Cloud Alibaba",微微点头,然后抛出一连串问题:

"你们用 Nacos 做注册中心,那如果 Nacos 挂了,服务还能互相调用吗?"

"Nacos 做注册中心和做配置中心,底层是一回事吗?"

"你们配了 @RefreshScope,配置改了真的立刻生效吗?如果改的是一个数据库连接池大小,会发生什么?"

"Sentinel 的限流和 Hystrix 的熔断有什么区别?Slot Chain 是怎么工作的?"

"Sentinel 的规则存在内存里,重启就丢了,你们怎么解决的?"

这些问题不是孤立的。它们围绕一个核心命题:在一个动态的微服务集群里,如何让服务"找得到彼此"、"配置能热更新"、"流量可控可降级"?

困境具象化:从配置灾难到流量洪峰

2018 年的一个凌晨,某电商平台的运营团队发布了一条大促配置变更:将"满减活动开关"从 false 改为 true。但配置格式写错了一个引号,导致整个订单服务的 YAML 解析失败,所有实例在滚动重启后批量崩溃——因为配置中心用的是静态文件,不支持热更新,必须重启才能加载新配置。

同一天下午,大促流量涌入,秒杀接口的 QPS 从平峰期的 200 飙到 20000。没有限流保护,所有请求直接打到数据库,连接池耗尽,雪崩开始。等团队紧急上线降级开关时,核心链路已经瘫痪了 15 分钟。

这两个事故暴露了微服务治理的三个刚需:

  1. 服务发现要可靠:服务上下线必须被实时感知,注册中心故障时不能全军覆没。
  2. 配置管理要动态:配置变更不能依赖重启,且要有容错机制(格式错误不能拖垮服务)。
  3. 流量控制要前置:在入口处就把超量流量拦住,而不是等数据库被打爆后再处理。

Spring Cloud Alibaba(SCA)就是围绕这三个刚需构建的——Nacos 解决前两个,Sentinel 解决第三个。


核心概念

Spring Cloud Alibaba 组件栈

Spring Cloud Alibaba 是国内微服务的事实标准,核心组件包括:

组件 职责 对标 Netflix 栈
Nacos Discovery 服务注册与发现 Eureka
Nacos Config 配置管理 Spring Cloud Config
Sentinel 流量控制(限流、降级、热点) Hystrix
Seata 分布式事务
RocketMQ 消息驱动
Dubbo RPC 框架 Feign + Ribbon

本文聚焦 Nacos Discovery + Nacos Config + Sentinel 三大核心组件。

Nacos 的双线架构

Nacos 同时承担两个角色:

  • Naming 服务(注册中心):管理服务的注册、发现、健康检查。
  • Config 服务(配置中心):管理配置的发布、订阅、版本控制。

这两条线共享 Nacos Server 进程,但底层数据模型和通信机制完全不同。

graph TD subgraph "Nacos Server" A[Naming Service] --> B[服务实例表] A --> C[心跳检测] D[Config Service] --> E[配置存储] D --> F[长轮询队列] end G[Provider] -->|注册/心跳| A H[Consumer] -->|订阅| A I[应用] -->|发布/监听| D

读图导引:注意 Naming 和 Config 是两条独立的数据流。Provider 向 Naming 注册并维持心跳,Consumer 向 Naming 订阅。应用向 Config 发布配置,通过长轮询监听变更。两个服务共享 Server 进程,但内部实现解耦。

Sentinel 的核心抽象

Sentinel 的设计围绕三个核心概念:

  • Resource(资源):被保护的目标,可以是一个方法、一个接口、一段代码。用 @SentinelResourceSphU.entry("resourceName") 声明。
  • Rule(规则):对资源的保护策略,包括流量控制规则(FlowRule)、熔断降级规则(DegradeRule)、系统保护规则(SystemRule)等。
  • Slot Chain(槽链):责任链模式,请求依次通过多个 Slot 的检查,任一 Slot 不通过即触发流控。

原理分析

第一层:Nacos Discovery —— 服务如何被发现

1.1 服务注册

服务启动时,NacosServiceRegistry.register 被调用:

java 复制代码
@Override
public void register(Registration registration) {
    if (StringUtils.isEmpty(registration.getServiceId())) {
        return;
    }
    
    String serviceId = registration.getServiceId();
    String group = nacosDiscoveryProperties.getGroup();
    
    // 构建 Nacos 实例
    Instance instance = getNacosInstanceFromRegistration(registration);
    
    // 注册到 Nacos
    namingService.registerInstance(serviceId, group, instance);
}

getNacosInstanceFromRegistration 构建的 Instance 包含:

  • IP、端口
  • 权重(默认 1.0)
  • 健康状态(默认 true)
  • 元数据(如 preserved.register.source=SPRING_CLOUD

1.2 心跳机制

注册后,Nacos 客户端启动定时心跳线程,默认每 5 秒发送一次心跳:

java 复制代码
public class BeatReactor {
    private final Map<String, BeatInfo> dome2Beat = new ConcurrentHashMap<>();
    
    public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        // 提交定时任务
        executorService.schedule(new BeatTask(beatInfo), 
            beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
    }
    
    class BeatTask implements Runnable {
        @Override
        public void run() {
            try {
                // 发送 HTTP PUT /nacos/v1/ns/instance/beat
                serverProxy.sendBeat(beatInfo);
                // 重新调度
                executorService.schedule(this, beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
            } catch (Exception e) {
                // 失败重试
            }
        }
    }
}

Nacos Server 收到心跳后,更新实例的最后心跳时间。如果实例超过 15 秒(默认)没有心跳,标记为不健康;超过 30 秒,直接剔除。

1.3 服务订阅与地址推送

Consumer 通过 NacosNamingService.subscribe 订阅服务:

java 复制代码
@Override
public void subscribe(String serviceName, String groupName, EventListener listener) {
    // 1. 立即获取当前服务列表
    List<Instance> instances = namingService.getAllInstances(serviceName, groupName);
    listener.onEvent(new NamingEvent(serviceName, instances));
    
    // 2. 建立 UDP 推送通道
    String key = ServiceInfo.getKey(serviceName, groupName, clusters);
    notifier.registerListener(key, listener);
    
    // 3. UDP 失败时退化为长轮询
    executorService.execute(new NamingRefreshTask(serviceName, groupName, clusters));
}

Nacos 的推送机制是**"推为主、拉为备"**:

  1. UDP 推送(Push):当服务列表变化时,Nacos Server 主动向 Consumer 的 UDP 端口推送变更通知。实时性最好,但 UDP 不可靠,可能丢包。
  2. 长轮询 fallback(Pull):Consumer 同时启动长轮询线程,默认每 30 秒向 Server 拉取一次服务列表。如果 UDP 通知丢失,长轮询兜底保证最终一致性。
sequenceDiagram participant C as Consumer participant NS as Nacos Server participant P as Provider P->>NS: registerInstance(serviceA) NS-->>P: 注册成功 C->>NS: subscribe(serviceA) NS-->>C: 返回当前实例列表 [P1] Note over C: 启动 UDP 监听 + 长轮询线程 P->>NS: 心跳(每5秒) P->>P: 实例宕机 P--xNS: 心跳停止 NS->>NS: 15秒未心跳,标记不健康 NS->>NS: 30秒未心跳,剔除实例 NS->>C: UDP Push: 服务列表更新 [空] alt UDP 丢失 C->>NS: 长轮询(30秒间隔) NS-->>C: 返回最新列表 [空] end

读图导引:关注两种推送方式的配合。UDP Push 提供实时性,长轮询提供可靠性兜底。Provider 心跳停止后,Nacos Server 有 15 秒"不健康"缓冲期,30 秒才彻底剔除——这个时间窗口内 Consumer 仍可能向已宕机的节点发请求。

1.4 健康检查的双保险

Nacos 支持两种健康检查方式:

  • 客户端心跳:Provider 主动上报心跳,Nacos Server 被动检测。
  • 服务端主动探测:Nacos Server 主动向 Provider 的 HTTP/TCP 端口发探测请求。

生产环境建议同时开启:客户端心跳用于快速感知(5 秒间隔),服务端探测用于防止"僵尸实例"(Provider 进程僵死但还在发心跳)。

1.5 Nacos 挂了怎么办?

Nacos 客户端会本地缓存服务列表:

java 复制代码
// NacosNamingService.getAllInstances 内部
public List<Instance> getAllInstances(String serviceName, String groupName) {
    ServiceInfo serviceInfo = serviceInfoHolder.getServiceInfo(key);
    if (serviceInfo == null || !serviceInfo.isValid()) {
        // 本地缓存 miss,向 Server 拉取
        serviceInfo = updateServiceNow(serviceName, groupName, clusters);
    }
    return serviceInfo.getHosts();
}

serviceInfoHolder 持有本地缓存。即使 Nacos Server 全部宕机,Consumer 仍可以使用缓存的地址列表继续调用。但新上线的 Provider 不会被发现,已下线的 Provider 也不会被剔除——本地缓存有 10 秒的过期时间,过期后如果没有 Server 响应,Consumer 会报错。

graph LR subgraph "Consumer 本地" A[serviceInfoHolder] --> B[内存缓存] A --> C[磁盘缓存 ~/.nacos/naming] end D[Nacos Server] -.->|推送| A E[Provider] -.->|注册| D

读图导引:注意双向虚线箭头——Nacos Server 向 Consumer 推送,Provider 向 Nacos Server 注册。Consumer 有两级缓存:内存缓存(最快)和磁盘缓存(进程重启后恢复)。Nacos Server 故障时,Consumer 退化为纯本地缓存模式。


第二层:Nacos Config —— 配置如何热更新

2.1 配置发布的完整链路

当你在 Nacos 控制台修改配置并发布后:

graph TD A[Nacos 控制台] -->|发布配置| B[Nacos Config Server] B --> C[配置持久化 MySQL/ Derby] B --> D[通知长轮询客户端] D --> E[应用 A 收到变更] D --> F[应用 B 收到变更] E --> G[@RefreshScope Bean 销毁并重建] F --> H[@ConfigurationProperties 字段重新绑定]

读图导引:从左到右看时间线。配置先持久化到数据库,然后 Nacos Server 主动通知所有长轮询的客户端。客户端收到通知后,对 @RefreshScope Bean 执行销毁重建,对 @ConfigurationProperties 执行字段重新绑定。注意 G 和 H 是两种不同的刷新机制。

2.2 长轮询机制

Nacos 客户端通过**长轮询(Long Polling)**监听配置变更:

java 复制代码
public class ClientWorker {
    public void addListeners(String dataId, String group, List<? extends Listener> listeners) {
        CacheData cache = addCacheDataIfAbsent(dataId, group);
        cache.setListeners(listeners);
    }
    
    // 长轮询线程
    class LongPollingRunnable implements Runnable {
        @Override
        public void run() {
            // 1. 获取本地缓存的配置 MD5
            List<CacheData> cacheDatas = cacheMap.values();
            
            // 2. 发起长轮询请求
            // timeout = 30000ms(默认)
            List<String> changedGroups = checkUpdateDataIds(cacheDatas, inInitializingCacheList);
            
            // 3. 如果有变更,拉取最新配置
            for (String groupKey : changedGroups) {
                String[] key = GroupKey.parseKey(groupKey);
                String dataId = key[0], group = key[1], tenant = key[2];
                String content = getServerConfig(dataId, group, tenant, 3000L);
                
                // 4. 更新本地缓存并触发监听器
                CacheData cache = cacheMap.get(GroupKey.getKeyTenant(dataId, group, tenant));
                cache.setContent(content);
            }
            
            // 5. 再次调度
            executorService.execute(this);
        }
    }
}

长轮询的关键设计:

  • 客户端发送请求时带上本地配置的 MD5 值。
  • Server 收到请求后,对比 MD5。如果一致,挂起请求 29.5 秒(默认 30 秒超时),期间如果配置变更,立即返回。
  • 如果 29.5 秒内无变更,返回空响应,客户端立即重新发起长轮询。

这种设计比定时轮询更高效:没有变更时几乎不消耗带宽,有变更时能在 1 秒内感知。

2.3 @RefreshScope 的原理与陷阱

Spring Cloud 的 @RefreshScope 是实现配置热更新的核心注解:

java 复制代码
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
public @interface RefreshScope {
}

它的底层是一个自定义的 Scope 实现:RefreshScope

java 复制代码
public class RefreshScope implements Scope, DisposableBean {
    private RefreshScopeCache cache;
    
    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        // 从缓存获取 Bean
        BeanWrapper value = cache.get(name);
        if (value == null) {
            // 首次创建
            value = new BeanWrapper(name, objectFactory);
            cache.put(name, value);
        }
        return value.getBean();
    }
}

RefreshScope 维护了一个独立的 Bean 缓存。当配置变更时,ContextRefresher.refresh() 被触发:

java 复制代码
public synchronized Set<String> refresh() {
    // 1. 从 Nacos 拉取最新配置
    Map<String, Object> newConfig = extractEnvironment();
    
    // 2. 更新 Environment
    addConfigFilesToEnvironment();
    
    // 3. 比较变更的 keys
    Set<String> keys = changes(newConfig, this.context.getEnvironment()).keySet();
    
    // 4. 发布 EnvironmentChangeEvent
    this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
    
    // 5. 销毁 @RefreshScope Bean 缓存
    this.scope.refreshAll();
    
    return keys;
}

scope.refreshAll() 会清空 RefreshScopeCache,下次获取 @RefreshScope Bean 时重新创建——这就是"热更新"的本质。

@RefreshScope 的三大陷阱

陷阱 1:Bean 重建期间的并发问题

java 复制代码
@RefreshScope
@Service
public class OrderService {
    @Value("${order.timeout:3000}")
    private int timeout;
    
    private final RestTemplate restTemplate = new RestTemplate();  // 非注入
    
    public void createOrder() {
        // 配置刷新时,Bean 被销毁,restTemplate 变成 null
        restTemplate.postForObject(...);
    }
}

问题:timeout 字段可以刷新,但 restTemplate 是在构造时创建的实例变量。Bean 重建后,如果其他线程正在使用旧 Bean,可能拿到未完全初始化的状态。

陷阱 2:连接池/线程池配置无法安全刷新

yaml 复制代码
spring:
  datasource:
    hikari:
      maximum-pool-size: 20  # 改这个不能简单刷新

如果通过 @RefreshScope 刷新 DataSource Bean,旧连接池的物理连接不会立即释放,新连接池又创建一批,可能导致数据库连接数瞬间翻倍。

陷阱 3:刷新阻塞

ContextRefresher.refresh() 是同步执行的:

java 复制代码
public synchronized Set<String> refresh() {  // synchronized!

如果刷新逻辑耗时(如重建大量 Bean),期间新的刷新请求会被阻塞。

更安全的替代方案

对于连接池、线程池等基础设施配置,不要用 @RefreshScope,而是用 @ConfigurationProperties + @ConstructorBinding + 自定义刷新逻辑:

java 复制代码
@Component
@ConfigurationProperties("thread.pool")
@ConstructorBinding
public class ThreadPoolProperties {
    private final int coreSize;
    private final int maxSize;
    
    // getter...
}

@Component
public class ThreadPoolRefresher {
    @Autowired
    private ThreadPoolExecutor executor;
    
    @EventListener
    public void onRefresh(EnvironmentChangeEvent event) {
        if (event.getKeys().stream().anyMatch(k -> k.startsWith("thread.pool"))) {
            // 动态调整线程池参数,而不是重建 Bean
            executor.setCorePoolSize(newCoreSize);
            executor.setMaximumPoolSize(newMaxSize);
        }
    }
}

2.4 配置的优先级与共享

Nacos Config 支持三种配置加载方式:

yaml 复制代码
spring:
  cloud:
    nacos:
      config:
        server-addr: nacos-server:8848
        file-extension: yaml
        # 主配置
        name: order-service
        # 共享配置
        shared-configs:
          - data-id: common.yaml
            group: DEFAULT_GROUP
            refresh: true
        # 扩展配置
        extension-configs:
          - data-id: redis.yaml
            group: DEFAULT_GROUP
            refresh: true

配置优先级(从高到低):

  1. spring.cloud.nacos.config.extension-configs[n](下标越大优先级越高)
  2. spring.cloud.nacos.config.shared-configs[n](下标越大优先级越高)
  3. 应用自身配置(spring.cloud.nacos.config.name
  4. @ConfigurationProperties 默认值

面试中常问:如果 common.yamlorder-service.yaml 都定义了 server.port,哪个生效?order-service.yaml 自身的配置优先级高于 shared-configs


第三层:Sentinel —— 流量如何被驯服

3.1 Sentinel 的设计哲学

Hystrix 的设计是"命令模式":每个被保护的方法被包装成 HystrixCommand,命令内部实现熔断、降级、线程隔离。这种设计的缺点是粒度粗——一个 Command 只能配置一套规则,无法对同一个方法的不同参数做不同的限流。

Sentinel 的设计是"插槽链模式":将流量控制拆分为多个独立的检查 Slot,请求像小球一样依次滚过每个 Slot 的检查口,任一 Slot 不通过即抛出 BlockException

graph LR A[请求进入] --> B[NodeSelectorSlot] B --> C[ClusterBuilderSlot] C --> D[LogSlot] D --> E[StatisticSlot] E --> F[AuthoritySlot] F --> G[SystemSlot] G --> H[ParamFlowSlot] H --> I[FlowSlot] I --> J[DegradeSlot] J --> K[到达业务方法] I -.->|触发限流| L[BlockException FlowException] J -.->|触发降级| M[BlockException DegradeException] G -.->|系统负载过高| N[BlockException SystemException]

读图导引:从左到右是请求的处理链路。每个 Slot 是一个独立的检查口。注意 I 和 J 的虚线箭头——FlowSlot 和 DegradeSlot 是"拦截器",不通过时直接抛异常。StatisticSlot 是核心统计节点,为后面的 FlowSlot 和 DegradeSlot 提供实时 QPS/RT 数据。

3.2 Slot Chain 的九个节点

Slot 职责 关键行为
NodeSelectorSlot 构建调用链路树 将资源按调用路径组织成树状结构
ClusterBuilderSlot 构建集群节点 创建 ClusterNode,聚合统计
LogSlot 记录日志 打印 BlockException 日志
StatisticSlot 统计指标 记录 pass/block/exception/rt 等指标
AuthoritySlot 黑白名单控制 按调用来源做权限控制
SystemSlot 系统保护 检查系统负载(CPU、RT、线程数等)
ParamFlowSlot 热点参数限流 按方法参数值限流
FlowSlot 流量控制 基于 QPS/并发数的限流判断
DegradeSlot 熔断降级 基于 RT/异常率的熔断判断

3.3 StatisticSlot —— 统计的心脏

StatisticSlot 是 Sentinel 最核心的 Slot,它负责维护资源的实时运行指标:

java 复制代码
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, 
                  int count, boolean prioritized, Object... args) throws Throwable {
    try {
        // 继续进入下一个 Slot
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
        
        // 请求通过,记录 pass
        node.increaseThreadNum();
        node.addPassRequest(count);
        
        if (context.getCurEntry().getOriginNode() != null) {
            context.getCurEntry().getOriginNode().increaseThreadNum();
            context.getCurEntry().getOriginNode().addPassRequest(count);
        }
    } catch (PriorityWaitException ex) {
        node.increaseThreadNum();
        // ...
    } catch (BlockException e) {
        // 被限流,记录 block
        node.increaseBlockQps(count);
        throw e;
    } catch (Throwable e) {
        // 业务异常,记录 exception
        node.increaseExceptionQps(count);
        throw e;
    }
}

统计数据的存储使用滑动时间窗口算法

java 复制代码
public class SlidingWindowMetric {
    private final LeapArray<MetricBucket> data;
    
    public SlidingWindowMetric(int sampleCount, int intervalInMs) {
        // sampleCount = 2(默认)
        // intervalInMs = 1000(1秒)
        this.data = new LeapArray<>(sampleCount, intervalInMs);
    }
    
    public long pass() {
        data.currentWindow().value().addPass(1);
    }
}

默认配置下,1 秒被划分为 2 个 500ms 的时间窗口。统计时取最近两个窗口的数据,形成 1 秒的滑动窗口。窗口数越多,限流越平滑,但内存占用也越大。

graph LR subgraph "滑动时间窗口" A[0-500ms] --> B[500-1000ms] B --> C[1000-1500ms] C --> D[1500-2000ms] end E[统计 QPS] --> F[取当前窗口 + 上一个窗口 共 1000ms]

读图导引:滑动窗口的核心是"取最近 N 个窗口的数据"。假设当前时间落在 C 窗口,统计 1 秒 QPS 时取 B + C 两个窗口。窗口边界处的突发流量会被平滑处理,不像固定窗口那样在边界处瞬间翻倍。

3.4 FlowSlot —— 流量控制的四种算法

FlowSlot 根据 FlowRule 判断是否放行。Sentinel 支持四种限流算法:

1. 快速失败(直接拒绝)

java 复制代码
static boolean passCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount) {
    double curCount = avgUsedTokens(node, rule);
    if (curCount + acquireCount > rule.getCount()) {
        // 超过阈值,触发限流
        return false;
    }
    return true;
}

超过阈值直接抛 FlowException

2. Warm Up(冷启动预热)

基于令牌桶算法的变种,解决冷启动问题:

graph LR A[系统刚启动] --> B[令牌生成速度慢 冷状态] B --> C[预热时间 默认10秒] C --> D[令牌生成速度逐渐提升] D --> E[达到最大速率 热状态]

读图导引:冷启动问题的本质是——系统刚启动时 JVM 未预热(JIT 未优化、连接池未填满、缓存未加载),如果瞬间接收全量流量,性能会很差。Warm Up 让令牌生成速度从低到高逐渐爬升,给系统预留预热时间。

Warm Up 使用 Guava 的 RateLimiter 算法,通过"冷却因子"控制令牌生成速度:

java 复制代码
// 令牌生成间隔从 coldFactor * stableInterval 逐渐降到 stableInterval
long warmUpPeriodInSec = rule.getWarmUpPeriodSec();
double coldFactor = 3;  // 冷启动因子,默认 3

3. 匀速排队(Rate Limiter)

基于漏桶算法:请求按固定速率通过,多余的请求排队等待,而不是直接拒绝。

java 复制代码
// 计算请求的等待时间
long waitTime = paceController.canPass(node, acquireCount, rule.getCount());
if (waitTime >= 0) {
    // 等待 waitTime 毫秒后放行
    Thread.sleep(waitTime);
    return true;
} else {
    // 排队超时,拒绝
    return false;
}

适用于脉冲流量削峰填谷场景——比如消息消费,即使上游瞬间涌入大量消息,也要匀速消费,避免数据库被打挂。

4. 并发线程数控制

按当前并发线程数限流,而不是 QPS:

java 复制代码
int curThreadNum = node.curThreadNum();
if (curThreadNum + acquireCount > rule.getCount()) {
    return false;
}

适用于慢调用隔离场景——某个下游接口响应慢,大量线程阻塞等待,通过并发线程数限制保护线程池不被耗尽。

3.5 DegradeSlot —— 熔断降级的三种策略

DegradeSlot 根据 DegradeRule 判断是否熔断。Sentinel 支持三种熔断策略:

1. 平均响应时间(RT)

java 复制代码
if (avgRT > rule.getCount()) {
    // 平均 RT 超过阈值,进入熔断
    if (cut.compareAndSet(false, true)) {
        // 设置熔断恢复时间窗口
        resetNextRetryTimestamp(rule);
    }
}

当资源的平均响应时间超过阈值(如 500ms),且时间窗口内的请求数 ≥ 5,触发熔断。熔断后进入半开状态,经过一个时间窗口(如 10 秒)后允许一个探测请求通过,如果成功则关闭熔断,失败则继续保持熔断。

2. 异常比例

java 复制代码
double exceptionRatio = (double) exceptionCount / totalCount;
if (exceptionRatio > rule.getCount()) {
    // 异常比例超过阈值,进入熔断
}

当异常比例超过阈值(如 50%),且时间窗口内请求数 ≥ 5,触发熔断。

3. 异常数

java 复制代码
if (exceptionCount > rule.getCount()) {
    // 异常数超过阈值,进入熔断
}

当近 1 分钟的异常数超过阈值,直接熔断。

graph LR A[熔断器关闭 正常放行] -->|请求异常/慢| B{是否达到阈值} B -->|否| A B -->|是| C[熔断器打开 拒绝请求] C --> D[等待恢复时间窗口 如10秒] D --> E[半开状态 允许探测请求] E --> F{探测成功} F -->|是| A F -->|否| C

读图导引:这是经典的熔断器状态机。关键状态有三个:Closed(关闭,正常)、Open(打开,拒绝)、Half-Open(半开,探测)。状态转换的触发条件是请求统计,不是定时器。

3.6 ParamFlowSlot —— 热点参数限流

热点参数限流是 Sentinel 相比 Hystrix 的显著优势。它可以根据方法参数的值做限流:

java 复制代码
@SentinelResource(value = "queryOrder", blockHandler = "queryOrderBlock")
public Order queryOrder(Long userId, Long orderId) {
    return orderDao.findById(orderId);
}

配置热点规则:

  • 参数索引:0(userId)
  • 单机阈值:1000 QPS
  • 参数例外项:userId=10086,阈值 10 QPS
java 复制代码
// ParamFlowSlot 核心逻辑
public boolean checkFlow(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count, Object value) {
    // 1. 获取参数值对应的统计节点
    ParameterMetric metric = ParameterMetricStorage.getParamMetric(resourceWrapper);
    CacheMap<Object, AtomicLong> tokenCounter = metric.getRuleTokenCounter(rule);
    CacheMap<Object, AtomicLong> timeCounter = metric.getRuleTimeCounter(rule);
    
    // 2. 检查是否超过阈值
    long currentCount = tokenCounter.get(value).addAndGet(count);
    if (currentCount > rule.getCount()) {
        return false;  // 限流
    }
    return true;
}

上面的配置意味着:普通用户的查询限流 1000 QPS,但用户 10086(比如爬虫账号)的查询限流 10 QPS。

3.7 Sentinel 与 Dubbo 的整合

Sentinel 通过 Dubbo Filter 整合:

java 复制代码
@Activate(group = "provider")
public class SentinelDubboProviderFilter extends BaseSentinelDubboFilter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String resourceName = getResourceName(invoker, invocation, DubboConfig.getDubboProviderPrefix());
        
        Entry interfaceEntry = null;
        Entry methodEntry = null;
        try {
            // 1. 接口级入口
            interfaceEntry = SphU.entry(invoker.getInterface().getName());
            // 2. 方法级入口
            methodEntry = SphU.entry(resourceName);
            
            return invoker.invoke(invocation);
        } catch (BlockException e) {
            // 返回限流响应
            return new AsyncRpcResult(AsyncRpcResult.newDefaultAsyncResult(
                new SentinelRpcException(e), invocation));
        } finally {
            if (methodEntry != null) methodEntry.exit();
            if (interfaceEntry != null) interfaceEntry.exit();
        }
    }
}

Sentinel 为 Dubbo 提供两层保护:

  • 接口级:限制整个接口的总流量。
  • 方法级:限制单个方法的流量。

如果接口级已经触发限流,方法级不会再执行,减少统计开销。

3.8 Sentinel 的暗面:规则持久化

Sentinel 的默认规则存储在内存中:

java 复制代码
public class FlowRuleManager {
    private static volatile Map<String, List<FlowRule>> flowRules = new HashMap<>();
    
    public static void loadRules(List<FlowRule> rules) {
        flowRules = new HashMap<>();
        // ...
    }
}

应用重启后,所有规则丢失。生产环境必须通过 Sentinel Dashboard 推送规则到外部存储(如 Nacos Config):

java 复制代码
// 从 Nacos 读取规则
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = 
    new NacosDataSource<>(remoteAddress, groupId, dataId,
        source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
graph LR A[Sentinel Dashboard] -->|推送规则| B[Nacos Config] B -->|长轮询通知| C[应用] C --> D[Sentinel 客户端 加载规则]

读图导引:这是 Sentinel 生产部署的标准模式。Dashboard 作为控制台,将规则写入 Nacos Config,应用通过 Nacos 长轮询监听规则变更。但 Dashboard 本身默认是内存存储,需要改造成持久化模式才能支撑大规模集群。

另一个暗面:Sentinel Dashboard 的高可用。Dashboard 默认将规则存在内存中,单机部署。如果 Dashboard 挂了,已推送到应用的规则不受影响(因为规则已经加载到应用内存),但新规则无法推送。生产环境建议:

  1. Dashboard 多节点部署,前面挂负载均衡。
  2. Dashboard 的存储层改造为 Nacos/MySQL 持久化。
  3. 规则变更走 Nacos Config,不依赖 Dashboard 的内存状态。

实战/源码

实战 1:Nacos 配置的多环境管理

yaml 复制代码
spring:
  profiles:
    active: dev
  cloud:
    nacos:
      config:
        server-addr: nacos-server:8848
        file-extension: yaml
        # 主配置:order-service.yaml
        name: order-service
        # 共享配置
        shared-configs:
          - data-id: common.yaml
            refresh: true
      discovery:
        server-addr: nacos-server:8848
        metadata:
          version: v1
          region: beijing

Nacos 上的配置:

Data ID Group 内容
order-service.yaml DEFAULT_GROUP 通用配置
order-service-dev.yaml DEFAULT_GROUP dev 环境覆盖配置
common.yaml DEFAULT_GROUP 共享配置

加载顺序:common.yamlorder-service.yamlorder-service-dev.yaml(后面的覆盖前面的)。

实战 2:Sentinel 规则代码定义

java 复制代码
@Configuration
public class SentinelConfig {
    @PostConstruct
    public void initRules() {
        List<FlowRule> rules = new ArrayList<>();
        
        // 1. QPS 限流规则
        FlowRule qpsRule = new FlowRule();
        qpsRule.setResource("queryOrder");
        qpsRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        qpsRule.setCount(1000);  // 1000 QPS
        qpsRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
        qpsRule.setWarmUpPeriodSec(10);  // 预热 10 秒
        rules.add(qpsRule);
        
        // 2. 并发线程数限流
        FlowRule threadRule = new FlowRule();
        threadRule.setResource("createOrder");
        threadRule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
        threadRule.setCount(50);  // 最多 50 并发线程
        rules.add(threadRule);
        
        // 3. 熔断降级规则
        List<DegradeRule> degradeRules = new ArrayList<>();
        DegradeRule degradeRule = new DegradeRule();
        degradeRule.setResource("payOrder");
        degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
        degradeRule.setCount(500);  // RT > 500ms
        degradeRule.setTimeWindow(10);  // 熔断 10 秒
        degradeRule.setMinRequestAmount(5);  // 至少 5 个请求才判断
        degradeRules.add(degradeRule);
        
        FlowRuleManager.loadRules(rules);
        DegradeRuleManager.loadRules(degradeRules);
    }
}

实战 3:@SentinelResource 的降级处理

java 复制代码
@Service
public class OrderService {
    
    @SentinelResource(
        value = "createOrder",
        blockHandler = "createOrderBlock",      // 限流/降级时的处理方法
        fallback = "createOrderFallback"        // 业务异常时的处理方法
    )
    public Order createOrder(CreateOrderRequest request) {
        // 业务逻辑
        return orderDao.insert(request);
    }
    
    // 方法签名必须与原方法一致,最后加 BlockException 参数
    public Order createOrderBlock(CreateOrderRequest request, BlockException ex) {
        log.warn("订单创建被限流: {}", request);
        throw new BizException("系统繁忙,请稍后再试");
    }
    
    // fallback 处理业务异常
    public Order createOrderFallback(CreateOrderRequest request, Throwable ex) {
        log.error("订单创建异常: {}", request, ex);
        return Order.builder().status(OrderStatus.FAILED).build();
    }
}

blockHandlerfallback 的区别:

  • blockHandler:只处理 Sentinel 触发的限流/降级(BlockException)。
  • fallback:处理业务抛出的所有异常(RuntimeExceptionError 等)。

如果同时配置,优先走 blockHandler

实战 4:Nacos 服务元数据做版本路由

java 复制代码
// Provider 注册时带上版本号
spring:
  cloud:
    nacos:
      discovery:
        metadata:
          version: v2

// Consumer 通过自定义负载均衡选择指定版本
public class VersionLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        String targetVersion = getTargetVersionFromRequest(request);
        
        List<ServiceInstance> instances = getInstances();
        List<ServiceInstance> versionInstances = instances.stream()
            .filter(i -> targetVersion.equals(i.getMetadata().get("version")))
            .collect(Collectors.toList());
        
        if (!versionInstances.isEmpty()) {
            return Mono.just(new DefaultResponse(
                versionInstances.get(ThreadLocalRandom.current().nextInt(versionInstances.size()))
            ));
        }
        
        //  fallback 到默认版本
        return Mono.just(new DefaultResponse(
            instances.get(ThreadLocalRandom.current().nextInt(instances.size()))
        ));
    }
}

常见问题

Q1:Nacos 和 Eureka 有什么区别?

维度 Nacos Eureka
架构 注册中心 + 配置中心一体 仅注册中心
推送机制 UDP Push + 长轮询 Pull 纯 Pull(30秒间隔)
健康检查 客户端心跳 + 服务端探测 仅客户端心跳
一致性 CP/AP 可切换(默认 AP) 纯 AP
自我保护 无(通过一致性协议保证) 有(防止误剔除)
集群方式 自研 Raft(Distro 协议) 纯 P2P 复制
元数据 丰富(权重、标签、自定义) 较少

核心差异:Nacos 的推送是"推为主"(UDP Push + 长轮询 fallback),Eureka 是纯拉模式(Consumer 每 30 秒拉一次)。Nacos 的实时性更好,但 UDP 可能丢包,需要长轮询兜底。

Q2:@RefreshScope 和 @ConfigurationProperties 哪个更适合配置刷新?

场景 推荐方案 原因
普通业务配置(开关、阈值) @Value + @RefreshScope 简单直接
复杂对象配置 @ConfigurationProperties 类型安全,支持校验
连接池/线程池配置 @ConfigurationProperties + 自定义刷新 避免 Bean 重建
需要原子性更新的多字段 @ConfigurationProperties 整对象刷新,无中间态

最佳实践

java 复制代码
@ConfigurationProperties("order")
@Data
public class OrderProperties {
    private int timeout = 3000;
    private int retry = 3;
    private boolean enabled = true;
}

// 使用
@Service
public class OrderService {
    @Autowired
    private OrderProperties orderProperties;
    
    public void createOrder() {
        int timeout = orderProperties.getTimeout();
        // ...
    }
}

@ConfigurationProperties 的刷新不需要 @RefreshScope,Spring Cloud 会自动重新绑定字段值。

Q3:Sentinel 的限流和 Hystrix 的熔断有什么区别?

维度 Sentinel Hystrix
设计理念 流量控制(限流为主) 容错保护(熔断为主)
实现模式 Slot Chain 责任链 Command 命令模式
限流算法 滑动窗口 + 令牌桶 + 漏桶 基于线程池/信号量隔离
热点参数 支持 不支持
系统保护 支持(CPU、RT、线程数) 不支持
规则动态配置 支持(Dashboard + DataSource) 有限支持
线程隔离 信号量(默认)/ 线程池 线程池(默认)/ 信号量

核心区别:Sentinel 以"限流"为核心设计目标,熔断只是其中一个 Slot;Hystrix 以"熔断"为核心,限流通过线程池大小间接实现。Sentinel 的 Slot Chain 更灵活,可以插拔各种控制逻辑。

Q4:Sentinel 的滑动窗口和固定窗口有什么区别?

固定窗口

复制代码
时间: 0    1    2    3    4    5    6
      |----|----|----|----|----|----|
QPS:  0    100  0    100  0    100

问题:窗口边界处(如 0.99 秒和 1.01 秒)可能瞬间通过 200 请求,而限流阈值是 100。

滑动窗口(默认 2 个窗口,每个 500ms):

复制代码
时间: 0   0.5  1   1.5  2   2.5  3
      |--|--|--|--|--|--|
窗口:  W1  W2  W1  W2  W1  W2

任意时刻统计的是当前窗口 + 上一个窗口,共 1 秒的数据。边界处的突发流量被平滑处理。

Sentinel 默认使用滑动窗口,通过 sampleCount 控制窗口数量。sampleCount=1 退化为固定窗口。

Q5:Nacos 配置中心的配置格式写错了,应用会崩溃吗?

不会直接崩溃,但行为取决于错误类型:

  1. YAML 语法错误(如缩进错误):Nacos 客户端拉取配置后,Spring 解析 YAML 失败,会抛出 IllegalStateException,但应用不会崩溃——它继续使用上一次成功解析的配置(本地缓存)。

  2. 字段类型不匹配(如字符串配给 int 字段):Spring 绑定 @ConfigurationProperties 时会抛出 BindException,同样使用上一次成功的配置。

  3. @RefreshScope Bean 重建时依赖初始化失败:如果新配置导致 Bean 初始化失败(如数据库连接池配置错误),refresh() 会抛出异常,旧 Bean 不会被销毁,继续可用。

但有一个陷阱:如果应用是首次启动,且从 Nacos 拉取的配置就是错误的,启动会直接失败。生产环境建议:

  • Nacos 控制台开启配置格式校验(YAML/JSON/Properties)。
  • 重要配置变更先在小范围环境验证,再全量推送。
  • 配置项加合理的默认值,避免空指针。

总结

本文从"配置变更导致服务崩溃"和"流量洪峰引发雪崩"两个真实困境出发,穿透 Spring Cloud Alibaba 三大核心组件的工程实现:

  1. Nacos Discovery:Provider 通过心跳(5 秒间隔)维持租约,Consumer 通过 UDP Push + 长轮询获取服务列表,本地两级缓存(内存 + 磁盘)保证 Nacos Server 故障时的可用性。

  2. Nacos Config:长轮询机制实现配置的准实时推送(1 秒内感知)。@RefreshScope 通过 Bean 销毁重建实现热更新,但有并发、连接池、阻塞三大陷阱,基础设施配置应改用 @ConfigurationProperties + 自定义刷新。

  3. Sentinel:Slot Chain 责任链将流量控制拆分为 9 个独立的 Slot,StatisticSlot 通过滑动时间窗口维护实时指标,FlowSlot 支持快速失败/Warm Up/匀速排队/并发线程数四种限流算法,DegradeSlot 支持 RT/异常比例/异常数三种熔断策略。规则默认存内存,生产环境必须通过 Dashboard + Nacos Config 做持久化。

面试一句话:Nacos 让服务"找得到彼此"、配置"热得起来",Sentinel 让流量"控得住、降得下"。