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)对每次资源调用进行统计、检查、控制。核心抽象就两个:

  1. Resource:被保护的代码块或接口,通过SphU.entry("resourceName")进入。
  2. 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;
    }
}

用一张图展示完整链路:

graph TD A[SphU.entry] --> B[lookProcessChain 缓存] B --> C[NodeSelectorSlot
构建调用树] 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 更新统计]

注意StatisticSlotFlowSlot之前,意味着每次请求都是先统计后判断。这是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...
            }
        }
    }
}

熔断器状态流转可以用图表示:

stateDiagram-v2 [*] --> CLOSED CLOSED --> OPEN: 慢调用比例/异常比例
超过阈值 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。建议服务名:方法名或直接用接口全路径,避免统计互相污染。热点参数限流要配合@SentinelResourceblockHandler

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 的核心可以浓缩为一句话:以资源为单元,用一条责任链在请求进入前完成统计与判定,用滑动窗口保证统计的实时性与准确性

回顾几个关键点:

  1. SphU.entrylookProcessChain(缓存)→ SlotChain.entry 是主流程;
  2. Slot 顺序中 StatisticSlotFlowSlot/DegradeSlot 之前,先统计后判定;
  3. LeapArray 用环形数组 + 时间戳校验实现无锁滑动窗口,是性能基石;
  4. 熔断器是 CLOSED/OPEN/HALF_OPEN 三态机,minRequestAmount 是隐形门槛;
  5. 业务异常必须 Tracer.traceEntry,否则熔断统计失真。

延伸思考:Sentinel 的滑动窗口统计的是"已进入"的请求,天然存在一个窗口的统计延迟。在高并发场景下,这个延迟可能导致瞬时超发。如果要做到严格精确的限流,需要结合令牌桶(Guava RateLimiter)或分布式限流(Redis + Lua)。而 Sentinel 的定位是"高吞吐下的近似精确 + 丰富治理能力",这个取舍值得每个架构师在选型时权衡。

下次再遇到"规则配了不生效",请顺着这条链排查:规则加载了吗 → Slot顺序对吗 → 统计窗口够吗 → minRequestAmount达标吗 → 异常用Tracer记录了吗。五步走完,问题基本现形。