问题引入
面试官看着你简历上的"Spring Cloud Alibaba",微微点头,然后抛出一连串问题:
"你们用 Nacos 做注册中心,那如果 Nacos 挂了,服务还能互相调用吗?"
"Nacos 做注册中心和做配置中心,底层是一回事吗?"
"你们配了 @RefreshScope,配置改了真的立刻生效吗?如果改的是一个数据库连接池大小,会发生什么?"
"Sentinel 的限流和 Hystrix 的熔断有什么区别?Slot Chain 是怎么工作的?"
"Sentinel 的规则存在内存里,重启就丢了,你们怎么解决的?"
这些问题不是孤立的。它们围绕一个核心命题:在一个动态的微服务集群里,如何让服务"找得到彼此"、"配置能热更新"、"流量可控可降级"?
困境具象化:从配置灾难到流量洪峰
2018 年的一个凌晨,某电商平台的运营团队发布了一条大促配置变更:将"满减活动开关"从 false 改为 true。但配置格式写错了一个引号,导致整个订单服务的 YAML 解析失败,所有实例在滚动重启后批量崩溃——因为配置中心用的是静态文件,不支持热更新,必须重启才能加载新配置。
同一天下午,大促流量涌入,秒杀接口的 QPS 从平峰期的 200 飙到 20000。没有限流保护,所有请求直接打到数据库,连接池耗尽,雪崩开始。等团队紧急上线降级开关时,核心链路已经瘫痪了 15 分钟。
这两个事故暴露了微服务治理的三个刚需:
- 服务发现要可靠:服务上下线必须被实时感知,注册中心故障时不能全军覆没。
- 配置管理要动态:配置变更不能依赖重启,且要有容错机制(格式错误不能拖垮服务)。
- 流量控制要前置:在入口处就把超量流量拦住,而不是等数据库被打爆后再处理。
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 进程,但底层数据模型和通信机制完全不同。
读图导引:注意 Naming 和 Config 是两条独立的数据流。Provider 向 Naming 注册并维持心跳,Consumer 向 Naming 订阅。应用向 Config 发布配置,通过长轮询监听变更。两个服务共享 Server 进程,但内部实现解耦。
Sentinel 的核心抽象
Sentinel 的设计围绕三个核心概念:
- Resource(资源):被保护的目标,可以是一个方法、一个接口、一段代码。用
@SentinelResource或SphU.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 的推送机制是**"推为主、拉为备"**:
- UDP 推送(Push):当服务列表变化时,Nacos Server 主动向 Consumer 的 UDP 端口推送变更通知。实时性最好,但 UDP 不可靠,可能丢包。
- 长轮询 fallback(Pull):Consumer 同时启动长轮询线程,默认每 30 秒向 Server 拉取一次服务列表。如果 UDP 通知丢失,长轮询兜底保证最终一致性。
读图导引:关注两种推送方式的配合。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 会报错。
读图导引:注意双向虚线箭头——Nacos Server 向 Consumer 推送,Provider 向 Nacos Server 注册。Consumer 有两级缓存:内存缓存(最快)和磁盘缓存(进程重启后恢复)。Nacos Server 故障时,Consumer 退化为纯本地缓存模式。
第二层:Nacos Config —— 配置如何热更新
2.1 配置发布的完整链路
当你在 Nacos 控制台修改配置并发布后:
读图导引:从左到右看时间线。配置先持久化到数据库,然后 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
配置优先级(从高到低):
spring.cloud.nacos.config.extension-configs[n](下标越大优先级越高)spring.cloud.nacos.config.shared-configs[n](下标越大优先级越高)- 应用自身配置(
spring.cloud.nacos.config.name) @ConfigurationProperties默认值
面试中常问:如果
common.yaml和order-service.yaml都定义了server.port,哪个生效?order-service.yaml 自身的配置优先级高于 shared-configs。
第三层:Sentinel —— 流量如何被驯服
3.1 Sentinel 的设计哲学
Hystrix 的设计是"命令模式":每个被保护的方法被包装成 HystrixCommand,命令内部实现熔断、降级、线程隔离。这种设计的缺点是粒度粗——一个 Command 只能配置一套规则,无法对同一个方法的不同参数做不同的限流。
Sentinel 的设计是"插槽链模式":将流量控制拆分为多个独立的检查 Slot,请求像小球一样依次滚过每个 Slot 的检查口,任一 Slot 不通过即抛出 BlockException。
读图导引:从左到右是请求的处理链路。每个 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 秒的滑动窗口。窗口数越多,限流越平滑,但内存占用也越大。
读图导引:滑动窗口的核心是"取最近 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(冷启动预热)
基于令牌桶算法的变种,解决冷启动问题:
读图导引:冷启动问题的本质是——系统刚启动时 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 分钟的异常数超过阈值,直接熔断。
读图导引:这是经典的熔断器状态机。关键状态有三个: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());
读图导引:这是 Sentinel 生产部署的标准模式。Dashboard 作为控制台,将规则写入 Nacos Config,应用通过 Nacos 长轮询监听规则变更。但 Dashboard 本身默认是内存存储,需要改造成持久化模式才能支撑大规模集群。
另一个暗面:Sentinel Dashboard 的高可用。Dashboard 默认将规则存在内存中,单机部署。如果 Dashboard 挂了,已推送到应用的规则不受影响(因为规则已经加载到应用内存),但新规则无法推送。生产环境建议:
- Dashboard 多节点部署,前面挂负载均衡。
- Dashboard 的存储层改造为 Nacos/MySQL 持久化。
- 规则变更走 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.yaml → order-service.yaml → order-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();
}
}
blockHandler 和 fallback 的区别:
blockHandler:只处理 Sentinel 触发的限流/降级(BlockException)。fallback:处理业务抛出的所有异常(RuntimeException、Error等)。
如果同时配置,优先走 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 配置中心的配置格式写错了,应用会崩溃吗?
不会直接崩溃,但行为取决于错误类型:
-
YAML 语法错误(如缩进错误):Nacos 客户端拉取配置后,Spring 解析 YAML 失败,会抛出
IllegalStateException,但应用不会崩溃——它继续使用上一次成功解析的配置(本地缓存)。 -
字段类型不匹配(如字符串配给 int 字段):Spring 绑定
@ConfigurationProperties时会抛出BindException,同样使用上一次成功的配置。 -
@RefreshScope Bean 重建时依赖初始化失败:如果新配置导致 Bean 初始化失败(如数据库连接池配置错误),
refresh()会抛出异常,旧 Bean 不会被销毁,继续可用。
但有一个陷阱:如果应用是首次启动,且从 Nacos 拉取的配置就是错误的,启动会直接失败。生产环境建议:
- Nacos 控制台开启配置格式校验(YAML/JSON/Properties)。
- 重要配置变更先在小范围环境验证,再全量推送。
- 配置项加合理的默认值,避免空指针。
总结
本文从"配置变更导致服务崩溃"和"流量洪峰引发雪崩"两个真实困境出发,穿透 Spring Cloud Alibaba 三大核心组件的工程实现:
-
Nacos Discovery:Provider 通过心跳(5 秒间隔)维持租约,Consumer 通过 UDP Push + 长轮询获取服务列表,本地两级缓存(内存 + 磁盘)保证 Nacos Server 故障时的可用性。
-
Nacos Config:长轮询机制实现配置的准实时推送(1 秒内感知)。
@RefreshScope通过 Bean 销毁重建实现热更新,但有并发、连接池、阻塞三大陷阱,基础设施配置应改用@ConfigurationProperties+ 自定义刷新。 -
Sentinel:Slot Chain 责任链将流量控制拆分为 9 个独立的 Slot,StatisticSlot 通过滑动时间窗口维护实时指标,FlowSlot 支持快速失败/Warm Up/匀速排队/并发线程数四种限流算法,DegradeSlot 支持 RT/异常比例/异常数三种熔断策略。规则默认存内存,生产环境必须通过 Dashboard + Nacos Config 做持久化。
面试一句话:Nacos 让服务"找得到彼此"、配置"热得起来",Sentinel 让流量"控得住、降得下"。