Sentinel流控降级源码分析
引言
去年双十一大促前夜,我所在团队负责的一个订单查询服务突然在压测中出现雪崩:下游库存服务因为一次慢SQL导致响应时间从50ms飙到3s,订单服务线程池瞬间被打满,紧接着整个调用链路像多米诺骨牌一样倒下。事后复盘时我们发现,如果当时在订单服务对库存接口配置了熔断降级规则,这场事故完全可以避免。
这就是流量治理要解决的核心问题:当系统的一部分开始不可用时,如何阻止故障向上游蔓延。Sentinel作为阿里开源的流量治理组件,正是为此而生。很多同学会用Sentinel的@SentinelResource注解和Dashboard配置规则,但一旦遇到"规则为什么没生效"、"SlotChain执行顺序到底是什么"这类问题就抓瞎。本文将从源码层面把Sentinel的核心机制彻底讲透。
核心概念:把Sentinel想象成一家餐厅的"大堂经理"
先打个比方。想象你经营一家网红餐厅:
- 流量控制(Flow Control):大堂经理站在门口,当店内座位(线程/资源)快满时,礼貌地让新来的客人排队或直接劝返(快速失败)。这就是QPS限流。
- 熔断降级(Circuit Breaking):如果后厨(下游服务)连续做出几道糊掉的菜(异常/慢调用),经理会暂时挂出"后厨维修中"的牌子,让客人直接去隔壁(降级逻辑),避免客人越积越多。
- 系统自适应保护(System Adaptive):当整个餐厅的CPU、负载、入口QPS达到阈值时,经理主动限流保命。
- 热点参数限流(Hot Spot):某个爆款菜(热点参数)被疯狂下单,只针对这个菜限流,其他菜照常供应。
技术定义上,Sentinel以资源(Resource)为最小治理单元,通过一条责任链(Slot Chain)对每次资源调用进行统计、检查、控制。核心抽象就两个:
- Resource:被保护的代码块或接口,通过
SphU.entry("resourceName")进入。 - Slot Chain:处理链,每个Slot负责一类职责(统计、限流、熔断、日志等)。
源码/原理深度分析
1. 入口:SphU.entry 做了什么
所有流控的起点都是SphU.entry()。我们看核心调用链:
// SphU.java
public static Entry entry(String name) throws BlockException {
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}
// CtSph.java
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
StringResourceWrapper resource = new StringResourceWrapper(name, type);
return entry(resource, count, args);
}
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
// 1. 根据资源获取或构建 ProcessorSlotChain
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
// 2. 构建 Entry
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
// 3. 触发责任链的 entry 方法(核心!)
chain.entry(context, resourceWrapper, null, count, args);
} catch (BlockException e1) {
// 被拦截,退出并抛出
e.exit(count, args);
throw e1;
}
return e;
}关键在lookProcessChain。它用ConcurrentHashMap缓存每个资源对应的SlotChain,保证同一资源全局只有一条链:
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// SPI 加载 SlotChainBuilder,默认 DefaultSlotChainBuilder
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap =
new HashMap<>(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}2. 默认责任链的构建顺序
DefaultSlotChainBuilder按固定顺序组装Slot,这个顺序极其重要:
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
// 1. NodeSelectorSlot:构建调用树,为每个资源创建 DefaultNode
chain.addLast(new NodeSelectorSlot());
// 2. ClusterBuilderSlot:构建 ClusterNode,聚合所有来源的统计
chain.addLast(new ClusterBuilderSlot());
// 3. LogSlot:记录被 block 的日志
chain.addLast(new LogSlot());
// 4. StatisticSlot:统计 RT、QPS、线程数(核心统计)
chain.addLast(new StatisticSlot());
// 5. AuthoritySlot:黑白名单
chain.addLast(new AuthoritySlot());
// 6. SystemSlot:系统自适应保护
chain.addLast(new SystemSlot());
// 7. FlowSlot:流控规则检查
chain.addLast(new FlowSlot());
// 8. DegradeSlot:熔断降级规则检查
chain.addLast(new DegradeSlot());
return chain;
}
}用一张图展示完整链路:
构建调用树] C --> D[ClusterBuilderSlot
聚合ClusterNode] D --> E[LogSlot
记录block日志] E --> F[StatisticSlot
核心统计] F --> G[AuthoritySlot
黑白名单] G --> H[SystemSlot
系统保护] H --> I[FlowSlot
流控检查] I --> J[DegradeSlot
熔断检查] J --> K{是否通过} K -->|通过| L[执行业务逻辑] K -->|拒绝| M[抛BlockException] L --> N[entry.exit 更新统计]
注意:StatisticSlot在FlowSlot之前,意味着每次请求都是先统计后判断。这是Sentinel的一个设计取舍——统计的是"已经进入"的请求,所以QPS限流是"近似"的,存在短暂的统计误差窗口。
3. StatisticSlot:滑动窗口统计的精髓
StatisticSlot是理解Sentinel的钥匙。它使用滑动时间窗口统计QPS和RT,而不是简单的计数器。看核心代码:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj,
int count, boolean prioritized, Object... args) throws Throwable {
try {
// 先放行,让后续Slot继续执行
fireEntry(context, resourceWrapper, obj, count, prioritized, args);
// 走到这里说明没有被 block,统计通过数
DefaultNode node = (DefaultNode) context.getCurNode();
// 增加线程数
node.increaseThreadNum();
// 更新统计:passQps、rt、successQps
node.addPassRequest(count);
if (context.getCurEntry().getBlockError() == null) {
// 没有block,记录成功
node.increaseExceptionQps(count); // 视情况
}
} catch (PriorityWaitException ex) {
// 优先级等待,也算通过
node.increaseThreadNum();
node.addPassRequest(count);
throw ex;
} catch (BlockException e) {
// 被 block,记录 block 数
context.getCurEntry().setBlockError(e);
node.increaseBlockQps(count);
throw e;
} catch (Throwable e) {
// 业务异常
node.increaseExceptionQps(count);
throw e;
}
}真正干活的统计器是ArrayMetric,底层是LeapArray(跳跃数组):
public class ArrayMetric implements Metric {
private final LeapArray<MetricBucket> data;
public ArrayMetric(int sampleCount, int intervalInMs) {
// sampleCount=2, intervalInMs=1000 表示1秒分2个窗口,每个500ms
this.data = new BucketLeapArray(sampleCount, intervalInMs);
}
@Override
public long pass() {
// 获取当前时间窗口,累加pass计数
data.currentWindow().value().add(MetricEvent.PASS, 1);
return 0;
}
}LeapArray是Sentinel性能的关键。它用环形数组 + 时间戳校验实现无锁滑动窗口:
public abstract class LeapArray<T> {
protected int windowLengthInMs; // 每个窗口长度,如500ms
protected int sampleCount; // 窗口数量,如2
protected int intervalInMs; // 总时间跨度,如1000ms
protected final AtomicReferenceArray<WindowWrap<T>> array;
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) return null;
// 计算时间点落在数组哪个位置
int idx = calculateTimeIdx(timeMillis);
// 计算该窗口的起始时间
long windowStart = calculateWindowStart(timeMillis);
while (true) {
WindowWrap<T> old = array.get(idx);
if (old == null) {
// 窗口不存在,CAS创建
WindowWrap<T> window = new WindowWrap<>(windowLengthInMs, windowStart, newEmptyBucket());
if (array.compareAndSet(idx, null, window)) {
return window;
} else {
Thread.yield();
}
} else if (windowStart == old.windowStart()) {
// 命中当前窗口
return old;
} else if (windowStart > old.windowStart()) {
// 窗口过期,需要重置(复用旧对象,避免GC)
if (updateLock.tryLock()) {
try {
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
// 时间回拨,返回新窗口兜底
return new WindowWrap<>(windowLengthInMs, windowStart, newEmptyBucket());
}
}
}
private int calculateTimeIdx(long timeMillis) {
long timeId = timeMillis / windowLengthInMs;
// 取模,映射到数组索引
return (int)(timeId % array.length());
}
}为什么要用滑动窗口而不是固定窗口? 固定窗口(如每整秒清零)存在"临界问题":在0.9s和1.1s各来100个请求,虽然限流100 QPS,但瞬时可能承受200个。滑动窗口把1秒切成2个500ms窗口,任意时刻统计的是"过去1秒",有效缓解临界突刺。
4. FlowSlot:限流的判定逻辑
FlowSlot根据规则选择不同的TrafficShapingController:
@Override
public void entry(...) throws Throwable {
// 根据资源名获取所有流控规则
Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
if (rules != null) {
for (FlowRule rule : rules) {
// 逐个检查,任一不通过就抛 FlowException
if (!canPassCheck(rule, context, node, count)) {
if (rule.isClusterMode()) {
// 集群模式特殊处理
if (!passClusterCheck(rule, context, node, count)) {
throw new FlowException(rule.getLimitApp(), rule);
}
} else {
throw new FlowException(rule.getLimitApp(), rule);
}
}
}
}
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
private boolean canPassCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount) {
// 根据 grade 选择控制器
TrafficShapingController controller = rule.getRater();
return controller.canPass(node, acquireCount, prioritized);
}限流控制器的四种策略:
| 策略 | 实现类 | 行为 |
|---|---|---|
| 快速失败 | DefaultController | 超过阈值直接抛异常 |
| 预热 | WarmUpController | 冷启动时阈值从低到高,防止冷系统被打挂 |
| 匀速排队 | RateLimiterController | 请求按固定间隔排队,类似漏桶 |
| 预热+排队 | WarmUpRateLimiterController | 组合策略 |
看匀速排队的实现,它本质上是一个漏桶算法:
public class RateLimiterController implements TrafficShapingController {
private final int maxQueueingTimeMs; // 最大排队时间
private final double count; // 阈值 QPS
private final AtomicLong latestPassedTime = new AtomicLong(-1);
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
if (acquireCount <= 0) return true;
if (count <= 0) return false;
long currentTime = TimeUtil.currentTimeMillis();
// 每个请求应该间隔的时间
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
// 预计可放行时间
long expectedTime = costTime + latestPassedTime.get();
if (expectedTime <= currentTime) {
// 无需排队,直接放行
latestPassedTime.set(currentTime);
return true;
} else {
// 需要排队,计算等待时间
long waitTime = costTime + latestPassedTime.get() - currentTime;
if (waitTime > maxQueueingTimeMs) {
// 超过最大排队时间,拒绝
return false;
} else {
// 尝试更新 latestPassedTime,模拟排队
long oldTime = latestPassedTime.addAndGet(costTime);
try {
waitTime = oldTime - currentTime;
if (waitTime > maxQueueingTimeMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
return false;
}
}
}
}
}5. DegradeSlot:熔断器的状态机
Sentinel的熔断器基于状态机,有三个状态:CLOSED(关闭,正常放行)、OPEN(打开,拒绝请求)、HALF_OPEN(半开,探测恢复)。
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(...) throws Throwable {
performChecking(context, resourceWrapper);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
void performChecking(Context context, ResourceWrapper r) throws BlockException {
for (CircuitBreaker cb : circuitBreakers) {
if (!cb.tryPass(context)) {
throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
}
}
}
}核心在ResponseTimeCircuitBreaker(慢调用比例)和ExceptionCircuitBreaker(异常比例/异常数)。以慢调用比例为例:
public class ResponseTimeCircuitBreaker extends AbstractCircuitBreaker {
// 统计窗口,默认1秒(滑动窗口)
private final LeapArray<SlowRequestCounter> slidingCounter;
private final double maxSlowRequestRatio; // 慢调用比例阈值
private final long maxRt; // 慢调用RT阈值(ms)
private final int minRequestAmount; // 最小请求数
@Override
public boolean tryPass(Context context) {
// CLOSED 状态直接放行
if (currentState.get() == State.CLOSED) {
return true;
}
// OPEN 状态需要判断是否到时间进入 HALF_OPEN
if (currentState.get() == State.OPEN) {
if (retryTimeoutArrived()) {
// 尝试 CAS 切换到 HALF_OPEN
if (fromOpenToHalfOpen(context)) {
return true;
}
}
return false;
}
// HALF_OPEN 状态放行探测请求
return true;
}
@Override
protected void handleStateChangeWhenThresholdExceeded(Throwable e) {
// 由 StatisticSlot 在 exit 时调用,统计一次调用结果
SlowRequestCounter counter = slidingCounter.currentWindow().value();
long completeStatTime = TimeUtil.currentTimeMillis();
// 累加总请求数
counter.totalCount.add(1L);
// 判断本次调用是否"慢"
if (e == null) {
long rt = completeStatTime - context.getCurEntry().createTime();
if (rt > maxRt) {
counter.slowCount.add(1L);
} else {
// 这里还处理了 RT 的 P99 等情况
}
} else if (e instanceof BlockException) {
return;
} else {
// 异常也算慢调用
counter.slowCount.add(1L);
}
// 判断是否达到熔断条件
doCheck();
}
private void doCheck() {
long totalCount = 0, slowCount = 0;
for (WindowWrap<SlowRequestCounter> w : slidingCounter.list()) {
SlowRequestCounter c = w.value();
totalCount += c.totalCount.sum();
slowCount += c.slowCount.sum();
}
// 请求数不够,不熔断
if (totalCount < minRequestAmount) {
return;
}
// 慢调用比例超过阈值,触发熔断
if (1.0 * slowCount / totalCount > maxSlowRequestRatio) {
// 切换到 OPEN
if (currentState.compareAndSet(State.CLOSED, State.OPEN)) {
// 记录熔断时间,用于后续 HALF_OPEN 判断
resetStat();
// 发布事件
CircuitBreakerStateChangeObserver...
}
}
}
}熔断器状态流转可以用图表示:
超过阈值 OPEN --> HALF_OPEN: 熔断时长到期
探测请求到达 HALF_OPEN --> CLOSED: 探测请求成功 HALF_OPEN --> OPEN: 探测请求失败
一个容易踩的坑:Sentinel的熔断是基于统计窗口的,窗口默认1秒。如果你的minRequestAmount设成10,而实际QPS只有2,那永远凑不够最小请求数,熔断器永远不会触发。这就是"规则配了但没生效"的常见原因。
实战代码
示例1:手写一个基于SphU的限流(不依赖Spring)
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 演示:手工定义流控规则 + SphU.entry 手动埋点
* 依赖:com.alibaba.csp:sentinel-core:1.8.6
*/
public class ManualFlowDemo {
private static final String RESOURCE = "queryOrder";
private static final AtomicInteger passCount = new AtomicInteger();
private static final AtomicInteger blockCount = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
initFlowRules();
// 模拟 20 个并发线程,每个请求 10 次
int threads = 20;
int perThread = 10;
ExecutorService pool = Executors.newFixedThreadPool(threads);
CountDownLatch latch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
pool.submit(() -> {
for (int j = 0; j < perThread; j++) {
queryOrder();
}
latch.countDown();
});
}
latch.await();
pool.shutdown();
System.out.println("通过: " + passCount.get() + ", 被限流: " + blockCount.get());
// 预期:QPS 阈值 10,1 秒内通过数约 10,其余被限流
}
/** 定义流控规则:QPS 阈值=10,快速失败 */
private static void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource(RESOURCE);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // QPS 模式
rule.setCount(10); // 阈值 10
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 快速失败
rules.add(rule);
FlowRuleManager.loadRules(rules); // 加载规则(可动态刷新)
}
private static void queryOrder() {
Entry entry = null;
try {
// 进入资源,被限流会抛 BlockException
entry = SphU.entry(RESOURCE);
// 业务逻辑
passCount.incrementAndGet();
} catch (BlockException e) {
// 被限流,执行降级逻辑
blockCount.incrementAndGet();
// 真实场景:返回缓存、默认值、友好提示
} finally {
if (entry != null) {
// 必须 exit,否则统计不准、线程数不释放
entry.exit();
}
}
}
}示例2:自定义 BlockExceptionHandler + 熔断降级
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreakerStrategy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* 演示:慢调用比例熔断 + 业务异常统计 + 降级兜底
*/
public class DegradeDemo {
private static final String RESOURCE = "callInventory";
public static void main(String[] args) throws Exception {
initDegradeRules();
// 持续调用 30 秒,观察熔断触发
long end = System.currentTimeMillis() + 30_000;
int total = 0, degraded = 0;
while (System.currentTimeMillis() < end) {
String result = callInventory();
total++;
if ("DEGRADED".equals(result)) degraded++;
Thread.sleep(50); // 约 20 QPS
}
System.out.println("总调用: " + total + ", 降级次数: " + degraded);
}
/** 慢调用比例熔断:RT>200ms 算慢,比例>50% 且请求数>=5 触发,熔断 5 秒 */
private static void initDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule(RESOURCE)
.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
.setCount(0.5) // 慢调用比例阈值 0.5
.setSlowRatioThreshold(0.5) // 慢调用比例阈值(部分版本用此字段)
.setTimeWindow(5) // 熔断时长 5 秒
.setMinRequestAmount(5) // 最小请求数
.setStatIntervalMs(1000);// 统计窗口 1 秒
// 注意:慢调用RT阈值通过 count 或 maxRt 设置,视版本而定
rule.setCount(200); // 慢调用 RT 阈值 200ms
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
private static String callInventory() {
Entry entry = null;
try {
entry = SphU.entry(RESOURCE);
// 模拟下游库存服务:30% 概率慢调用(300ms),10% 概率抛异常
int r = ThreadLocalRandom.current().nextInt(100);
if (r < 30) {
Thread.sleep(300); // 慢调用
} else if (r < 40) {
throw new RuntimeException("inventory service error");
}
return "OK";
} catch (BlockException e) {
// 熔断/限流触发,走降级
return "DEGRADED";
} catch (Throwable t) {
// 业务异常必须用 Tracer 记录,否则熔断器统计不到异常
Tracer.traceEntry(t, entry);
return "ERROR";
} finally {
if (entry != null) {
entry.exit();
}
}
}
}示例3:Spring Cloud Alibaba 集成 + 自定义 BlockHandler
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 演示:@SentinelResource 注解 + blockHandler + fallback
* 依赖:spring-cloud-starter-alibaba-sentinel
*
* 注意:blockHandler 只处理 BlockException(限流/熔断),
* fallback 处理方法签名要匹配原方法(可多一个 Throwable 参数)
*/
@SpringBootApplication
@RestController
public class SentinelSpringDemo {
public static void main(String[] args) {
SpringApplication.run(SentinelSpringDemo.class, args);
}
/**
* blockHandler:被限流/熔断时调用
* fallback:业务异常时调用
* exceptionsToIgnore:指定不进入 fallback 的异常
*/
@SentinelResource(
value = "getUserInfo",
blockHandler = "handleBlock",
fallback = "handleFallback",
exceptionsToIgnore = {IllegalArgumentException.class}
)
@GetMapping("/user")
public String getUserInfo(Long userId) {
if (userId == null) {
throw new IllegalArgumentException("userId 不能为空"); // 不触发 fallback
}
if (userId < 0) {
throw new RuntimeException("非法 userId"); // 触发 fallback
}
return "user-" + userId;
}
/** blockHandler:方法签名 = 原方法 + BlockException,且必须同类型返回 */
public String handleBlock(Long userId, BlockException ex) {
return "被限流啦,请稍后重试。原因: " + ex.getClass().getSimpleName();
}
/** fallback:方法签名 = 原方法 + Throwable */
public String handleFallback(Long userId, Throwable t) {
return "服务降级,返回默认值。异常: " + t.getMessage();
}
}对应的application.yml:
server:
port: 8080
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8858 # Sentinel Dashboard 地址
eager: true # 启动即建立心跳,避免首次请求延迟
web-context-unify: false # 关闭 URL 收敛,否则所有接口统计到同一 context方案对比:Sentinel vs Hystrix vs Resilience4j
| 维度 | Sentinel | Hystrix | Resilience4j |
|---|---|---|---|
| 隔离策略 | 信号量(默认)+ 并发线程数 | 线程池/信号量 | 信号量 + 装饰器 |
| 熔断统计 | 滑动窗口(LeapArray) | 滑动窗口(RxJava) | 环形缓冲区 |
| 限流 | 内置丰富(QPS/线程/热点/集群) | 无 | 内置 RateLimiter |
| 规则动态 | Dashboard 实时推送 | 需配合 Archaius | 需自行集成 |
| 维护状态 | 活跃 | 已停止维护 | 活跃 |
| 性能开销 | 极低(无额外线程) | 线程池隔离开销大 | 低 |
选型建议:
- 已有阿里技术栈、需要Dashboard可视化、要热点/集群流控 → Sentinel
- 纯熔断降级、想避免线程池开销、轻量级 → Resilience4j
- 老项目已用 → 逐步迁移,不要新项目再上Hystrix
一个关键差异:Hystrix默认线程池隔离,每个依赖一个线程池,虽然隔离彻底但线程上下文切换开销大、线程数膨胀。Sentinel采用信号量隔离(通过并发线程数限流),零额外线程,但要求下游有超时控制,否则慢调用会拖垮调用线程。
最佳实践与避坑指南
1. 入口埋点用注解,出口埋点别漏 exit
SphU.entry()和entry.exit()必须成对出现,且exit()放finally。漏掉exit()会导致线程数统计虚高,最终触发"线程数限流"误判。
2. 业务异常必须用 Tracer.traceEntry 记录
try {
entry = SphU.entry("res");
doSomething(); // 抛业务异常
} catch (Throwable t) {
Tracer.traceEntry(t, entry); // 关键!否则熔断器统计不到异常
throw t;
} finally {
if (entry != null) entry.exit();
}3. 熔断规则的最小请求数是隐形门槛
minRequestAmount默认5。低QPS接口设太大,永远不熔断。建议 minRequestAmount ≈ 阈值QPS × 统计窗口秒数 × 0.5。
4. 资源命名要区分层级
不要所有接口都叫api。建议服务名:方法名或直接用接口全路径,避免统计互相污染。热点参数限流要配合@SentinelResource的blockHandler。
5. 规则持久化不能只放内存
FlowRuleManager.loadRules()加载的规则重启即丢。生产必须接入Nacos/Apollo/ZK,通过DynamicRuleDataSource持久化。Dashboard改的规则默认只在内存,重启丢失。
6. 预热模式别用在有状态服务上
WarmUpController假设冷系统处理能力弱,阈值从count/3线性升到count。若你的服务本身无状态、启动即满血,用预热反而误伤。
7. 集群流控需要 Token Server
集群模式依赖独立的token-server分配令牌,单点故障会导致整个集群失效。务必部署token-server集群,并设置fallbackToLocalWhenFail=true兜底。
8. 注意 Sentinel 的 Context 传播
异步调用、线程池切换时,Context 不会自动传播。需要用ContextUtil.runOnContext()或SphU.asyncEntry(),否则异步逻辑的统计会挂到错误节点。
总结
Sentinel 的核心可以浓缩为一句话:以资源为单元,用一条责任链在请求进入前完成统计与判定,用滑动窗口保证统计的实时性与准确性。
回顾几个关键点:
SphU.entry→lookProcessChain(缓存)→SlotChain.entry是主流程;- Slot 顺序中
StatisticSlot在FlowSlot/DegradeSlot之前,先统计后判定; LeapArray用环形数组 + 时间戳校验实现无锁滑动窗口,是性能基石;- 熔断器是 CLOSED/OPEN/HALF_OPEN 三态机,
minRequestAmount是隐形门槛; - 业务异常必须
Tracer.traceEntry,否则熔断统计失真。
延伸思考:Sentinel 的滑动窗口统计的是"已进入"的请求,天然存在一个窗口的统计延迟。在高并发场景下,这个延迟可能导致瞬时超发。如果要做到严格精确的限流,需要结合令牌桶(Guava RateLimiter)或分布式限流(Redis + Lua)。而 Sentinel 的定位是"高吞吐下的近似精确 + 丰富治理能力",这个取舍值得每个架构师在选型时权衡。
下次再遇到"规则配了不生效",请顺着这条链排查:规则加载了吗 → Slot顺序对吗 → 统计窗口够吗 → minRequestAmount达标吗 → 异常用Tracer记录了吗。五步走完,问题基本现形。