Sentinel 高级限流:黑白名单授权与热点参数限流完全指南

图片[1]-Sentinel 高级限流:黑白名单授权与热点参数限流完全指南 - 速优课-速优课

本文导读

在前面的文章中,我们系统学习了 Sentinel 的流量控制、熔断降级和系统自适应限流。这些都是比较通用的限流手段。但在实际业务场景中,我们经常需要更精细化的限流能力:

  • 如何只允许特定的服务调用方访问?
  • 如何拦截某些恶意 IP 的请求?
  • 如何对热门商品的下单请求单独限流?
  • 如何针对接口的不同参数值实现差异化限流?

这就需要用到 Sentinel 的两大高级限流功能:

  1. 黑白名单限流:基于调用来源的授权控制
  2. 热点参数限流:基于请求参数的精细化限流

本文将从原理、配置、源码实现等多个维度,深入剖析这两大功能的实现机制,帮助你掌握 Sentinel 的高级限流技巧。


一、黑白名单限流:基于调用来源的授权控制

1.1 什么是黑白名单限流

黑白名单过滤是软件开发中最经典的过滤规则之一,应用场景非常广泛:

  • IP 黑白名单:用于接口安全防护
  • 手机号黑白名单:用于短信、来电的防骚扰
  • 服务黑白名单:用于微服务间的调用授权

Sentinel 中的黑白名单限流,本质上是一种授权机制。它将调用来源简单地分为”有权限”和”无权限”两种情况:

  • 黑名单模式:来源在黑名单中 → 拒绝请求
  • 白名单模式:来源在白名单中 → 放行请求;否则拒绝

Sentinel 不支持一个规则同时配置黑名单和白名单,因此不存在优先级冲突的问题。

Sentinel 在命名上使用的是 Authority(授权)而非 BlackWhiteList,这也体现了它的定位——黑白名单本质上是一种授权限流。

1.2 核心类一览

在深入源码之前,我们先认识几个关键类:

类名 作用
AuthoritySlot 实现黑白名单授权功能的切入点(ProcessorSlot)
AuthorityRule 授权规则类,定义黑白名单规则
AuthorityRuleChecker 授权检测类,实现具体的判断逻辑
AuthorityRuleManager 授权规则管理者,提供规则加载 API
AuthorityException 授权检测异常,继承自 BlockException

1.3 授权规则配置

AuthorityRule 是 Sentinel 中最容易理解的规则之一,配置项非常简洁:

public class AuthorityRule extends AbstractRule {
    private int strategy = RuleConstant.AUTHORITY_WHITE;
}

从父类继承和自身定义的核心配置项:

配置项 说明
resource 资源名称,从父类继承
limitApp 限制的来源名称,支持多个,用英文逗号分隔
strategy 限流策略:白名单(AUTHORITY_WHITE)或黑名单(AUTHORITY_BLACK)

策略说明:

  • strategy = AUTHORITY_WHITE 时,limitApp 即为白名单
  • strategy = AUTHORITY_BLACK 时,limitApp 即为黑名单

使用示例:

AuthorityRule rule = new AuthorityRule();
// 资源名称
rule.setResource("GET:/hello");
// 白名单策略
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
// 只允许 serviceA 和 serviceC 访问
rule.setLimitApp("serviceA,serviceC");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));

上述规则表示:资源”GET:/hello”只允许来自 serviceA 和 serviceC 的请求访问。

1.4 AuthoritySlot 的执行时机

在使用默认的 SlotChainBuilder 情况下,AuthoritySlot 被放在 SystemSlotFlowSlotDegradeSlot前面,优先级最高。

为什么这么设计?主要有两个原因:

  1. 授权限流不需要统计指标数据,可以快速判断
  2. 性能考虑:未授权的请求没必要继续判断熔断、系统负载、QPS 等,直接拒绝即可

这和用户授权功能的设计思路是一样的:未登录的用户不需要判断是否有权限访问某个具体资源,直接拦截。

AuthoritySlot 的实现源码如下:

public class AuthoritySlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, 
                      boolean prioritized, Object... args) throws Throwable {
        checkBlackWhiteAuthority(resourceWrapper, context);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        fireExit(context, resourceWrapper, count, args);
    }

    void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
        // (1) 获取所有授权规则
        Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();
        if (authorityRules == null) {
            return;
        }
        // (2) 获取当前资源的授权规则
        Set<AuthorityRule> rules = authorityRules.get(resource.getName());
        if (rules == null) {
            return;
        }
        // (3) 遍历规则逐一检查
        for (AuthorityRule rule : rules) {
            if (!AuthorityRuleChecker.passCheck(rule, context)) {
                throw new AuthorityException(context.getOrigin(), rule);
            }
        }
    }
}

整个流程非常清晰:

  1. AuthorityRuleManager 获取当前配置的所有授权规则
  2. 根据资源名称获取该资源对应的授权规则
  3. 遍历规则,调用 AuthorityRuleChecker#passCheck 判断是否拒绝,拒绝则抛出 AuthorityException

1.5 AuthorityRuleChecker 核心逻辑

AuthorityRuleChecker 负责实现黑白名单的具体过滤逻辑,其 passCheck 方法是核心:

static boolean passCheck(AuthorityRule rule, Context context) {
    // 获取调用来源
    String requester = context.getOrigin();
    // 来源为空或规则未配置 limitApp,直接放行
    if (StringUtil.isEmpty(requester) || StringUtil.isEmpty(rule.getLimitApp())) {
        return true;
    }
    // 第一步:使用 indexOf 快速过滤
    int pos = rule.getLimitApp().indexOf(requester);
    boolean contain = pos > -1;
    // 第二步:精确匹配
    if (contain) {
        boolean exactlyMatch = false;
        String[] appArray = rule.getLimitApp().split(",");
        for (String app : appArray) {
            if (requester.equals(app)) {
                exactlyMatch = true;
                break;
            }
        }
        contain = exactlyMatch;
    }
    // 根据策略判断是否放行
    int strategy = rule.getStrategy();
    // 黑名单:来源在名单中则拒绝
    if (strategy == RuleConstant.AUTHORITY_BLACK && contain) {
        return false;
    }
    // 白名单:来源不在名单中则拒绝
    if (strategy == RuleConstant.AUTHORITY_WHITE && !contain) {
        return false;
    }
    return true;
}

这段代码虽然简单,但有几个值得注意的设计细节:

1. 前置条件检查

首先从 Context 获取调用来源名称。只有调用来源不为空,且规则配置了 limitApp 的情况下,才会走过滤逻辑。

这意味着:要使用黑白名单限流,前提是每个服务消费端在发起请求时必须携带自身服务名称。这一点由 Sentinel 的各主流框架适配器保证。

2. 性能优化:先快速过滤,再精确匹配

Sentinel 并没有直接 split 然后遍历匹配,而是先用 indexOf 做一次快速过滤,只有匹配上了才做精确匹配。

为什么要这么做?

  • indexOf 的时间复杂度较低,能快速排除大多数不匹配的情况
  • 只有在可能匹配的情况下,才执行 split 和遍历的重操作
  • 这是一种典型的”快速失败”性能优化思路

3. 策略判断逻辑

  • 黑名单策略:来源在名单中 → 返回 false(拒绝)
  • 白名单策略:来源不在名单中 → 返回 false(拒绝)
  • 其他情况都返回 true(放行)

二、热点参数限流:基于参数的精细化限流

2.1 为什么需要热点参数限流

前面介绍的限流都是针对资源整体的,但实际业务中经常需要更细粒度的限流。

举个典型的电商场景:

  • 都是调用下单接口,但购买的商品不同
  • 主播带货的热门商品下单流量巨大
  • 普通商品的下单量很小
  • 同时,热门商品的库存有限,不可能每个下单请求都能成功

如果能实现根据商品 ID 单独限流:

  • 将热门商品的流量控制在库存总量左右
  • 普通商品不受影响
  • 再配合 QPS 限流做兜底

这种有针对性的限流,能将接口的有效流量最大化。

这就是热点参数限流:根据方法调用传递的参数实现限流,针对访问频繁的参数值单独限流。

热点参数限流不在 Sentinel 的 core 模块中实现,而是在扩展模块 sentinel-parameter-flow-control 中。另外,Sentinel 的 API Gateway 网关限流也是基于参数限流实现的。

2.2 基于滑动窗口的热点参数指标统计

热点参数限流使用的指标数据不是 core 模块中的统计数据,而是重新实现了一套指标统计功能,底层依旧是基于滑动窗口。

核心类:

类名 作用
ParamMapBucket 实现参数指标统计的 Bucket,统计每个参数不同取值的通过数、被限流数
HotParameterLeapArray 实现滑动窗口,持有 WindowWrap 数组,WindowWrap 包装 ParamMapBucket

与 core 模块的区别:

core 模块的 MetricBucket 只统计每个指标的数值,更像是 Redis 中的 String 结构;而 ParamMapBucket 需要统计每个指标、参数的每种取值的数值,更像 Redis 中的 Hash 结构。

ParamMapBucket 的源码如下:

public class ParamMapBucket {

    // 数组元素类型为 CacheMap<Object, AtomicInteger>
    private final CacheMap<Object, AtomicInteger>[] data;

    public ParamMapBucket() {
        this(DEFAULT_MAX_CAPACITY);
    }

    public ParamMapBucket(int capacity) {
        RollingParamEvent[] events = RollingParamEvent.values();
        // 根据需要统计的指标创建数组
        this.data = new CacheMap[events.length];
        // RollingParamEvent 可取值为 REQUEST_PASSED、REQUEST_BLOCKED
        for (RollingParamEvent event : events) {
            data[event.ordinal()] = new ConcurrentLinkedHashMapWrapper<Object, AtomicInteger>(capacity);
        }
    }
}

字段说明:

  • data:数组,下标 0 存储请求通过数,下标 1 存储请求被拒绝数
  • CacheMap:key 为参数取值(如商品 ID),value 为指标数值

HotParameterLeapArray 继承自 LeapArray,实现了滑动窗口。ParamMapBucket 不存储窗口时间信息,窗口时间信息仍由 WindowWrap 存储。

从这里我们也能体会到 Sentinel 将滑动窗口抽象为 LeapArray 的好处:为扩展实现自定义指标数据的滑动窗口提供了便利。

HotParameterLeapArray 提供的几个核心 API:

public class HotParameterLeapArray extends LeapArray<ParamMapBucket> {
    
    public void addValue(RollingParamEvent event, int count, Object value) {
        // 添加参数的指标数值
    }

    public Map<Object, Double> getTopValues(RollingParamEvent event, int number) {
        // 获取热点参数的排名,如 QPS 排名前 10 的参数取值
    }

    public long getRollingSum(RollingParamEvent event, Object value) {
        // 计算某个指标、某个参数取值的总请求数
    }

    public double getRollingAvg(RollingParamEvent event, Object value) {
        // 获取某个指标、某个参数取值的平均 QPS
    }
}

注意内存占用问题:如果是分钟级的滑动窗口,一分钟内参数的取值越多,占用的内存就越多。在使用热点参数限流时,一定要考虑参数取值的可能性数量。

2.3 参数限流中的 Node 体系

热点参数限流也有自己的 Node 体系,两个核心类:

类名 类比 core 模块 作用
ParameterMetric ClusterNode 实现参数级别的统计功能
ParameterMetricStorage EntranceNode 管理和存储每个资源对应的 ParameterMetric

ParameterMetric 有三个关键字段:

public class ParameterMetric {

    private final Map<ParamFlowRule, CacheMap<Object, AtomicLong>> ruleTimeCounters = new HashMap<>();
    private final Map<ParamFlowRule, CacheMap<Object, AtomicLong>> ruleTokenCounter = new HashMap<>();
    private final Map<Integer, CacheMap<Object, AtomicInteger>> threadCountMap = new HashMap<>();

}

各字段说明:

字段 作用
ruleTimeCounters 用于匀速流量控制效果,key 为参数限流规则,value 为参数不同取值对应的上次生产令牌时间
ruleTokenCounter 用于匀速流量控制效果,key 为参数限流规则,value 为参数不同取值对应的当前令牌桶令牌数
threadCountMap key 为参数索引,value 为参数不同取值对应的当前并行占用线程总数

ParameterMetricStorage 使用 ConcurrentHashMap 缓存每个资源对应的 ParameterMetric只会为配置了参数限流规则的资源创建

public final class ParameterMetricStorage {
    private static final Map<String, ParameterMetric> metricsMap = new ConcurrentHashMap<>();
    private static final Object LOCK = new Object();

    public static void initParamMetricsFor(ResourceWrapper resourceWrapper, ParamFlowRule rule) {
        if (resourceWrapper == null || resourceWrapper.getName() == null) {
            return;
        }
        String resourceName = resourceWrapper.getName();
        ParameterMetric metric;
        // 双重检测,线程安全地创建 ParameterMetric
        if ((metric = metricsMap.get(resourceName)) == null) {
            synchronized (LOCK) {
                if ((metric = metricsMap.get(resourceName)) == null) {
                    metric = new ParameterMetric();
                    metricsMap.put(resourceWrapper.getName(), metric);
                }
            }
        }
        // 初始化 ParameterMetric
        metric.initialize(rule);
    }
}

initParamMetricsFor 方法由 ParamFlowSlot 在资源被访问时调用,且只有配置了参数限流规则的资源才会被调用。

2.4 热点参数限流的入口:ParamFlowSlot

sentinel-parameter-flow-control 模块通过 Java SPI 注册自定义的 SlotChainBuilder,即 HotParamSlotChainBuilder,将 ParamFlowSlot 放置在 StatisticSlot 的后面。

ParamFlowSlot 就是实现热点参数限流功能的切入点:

public class ParamFlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
            fireEntry(context, resourceWrapper, node, count, prioritized, args);
            return;
        }
        checkFlow(resourceWrapper, count, args);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        fireExit(context, resourceWrapper, count, args);
    }
}

参数是怎么传递过来的呢?答案是 ProcessorSlot#entry 方法的最后一个参数 args,通过 SphU#entry 方法一层层往下传递。

使用示例:

@GetMapping("/hello")
public String apiHello(String name) throws BlockException {
    ContextUtil.enter("my_context");
    Entry entry = null;
    try {
        // name 参数通过 entry 方法传递
        entry = SphU.entry("GET:/hello", EntryType.IN, 1, name);
        doBusiness();
        return "Hello!";
    } catch (Exception e) {
        if (!(e instanceof BlockException)) {
            Tracer.trace(e);
        }
        throw e;
    } finally {
        if (entry != null) {
            entry.exit(1);
        }
        ContextUtil.exit();
    }
}

当调用到 ParamFlowSlot#entry 时,ParamFlowSlot 调用 checkFlow 方法判断是否需要限流:

void checkFlow(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
    // (1) 参数为空或无规则,直接返回
    if (args == null) {
        return;
    }
    if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
        return;
    }
    List<ParamFlowRule> rules = ParamFlowRuleManager.getRulesOfResource(resourceWrapper.getName());
    // (2) 遍历规则逐一检查
    for (ParamFlowRule rule : rules) {
        applyRealParamIdx(rule, args.length);
        // 初始化参数指标统计
        ParameterMetricStorage.initParamMetricsFor(resourceWrapper, rule);
        if (!ParamFlowChecker.passCheck(resourceWrapper, rule, count, args)) {
            String triggeredParam = "";
            if (args.length > rule.getParamIdx()) {
                Object value = args[rule.getParamIdx()];
                triggeredParam = String.valueOf(value);
            }
            throw new ParamFlowException(resourceWrapper.getName(), triggeredParam, rule);
        }
    }
}

流程很清晰:

  1. 检查参数和规则,无规则则放行
  2. 遍历规则,初始化 ParameterMetric
  3. 调用 ParamFlowChecker#passCheck 判断是否放行
  4. 拒绝时抛出 ParamFlowException,携带触发限流的参数值

2.5 参数限流规则配置

在深入判断逻辑之前,我们先了解 ParamFlowRule 的配置项:

public class ParamFlowRule extends AbstractRule {
    private int grade = RuleConstant.FLOW_GRADE_QPS;
    private double count;
    private Integer paramIdx;
    private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;
    private int maxQueueingTimeMs = 0;
    private long durationInSec = 1;
    private int burstCount = 0;
}

各配置项详解:

配置项 说明
grade 阈值类型,同 FlowRule,支持 QPS 和线程数
count 限流阈值,同 FlowRule
paramIdx 参数索引,从 0 开始,指定对哪个参数限流
controlBehavior 流控效果,同 FlowRule,但只支持快速失败和匀速排队
maxQueueingTimeMs 匀速排队的最大等待时间,同 FlowRule
durationInSec 统计窗口时间大小,单位秒
burstCount 突发流量容许数,即令牌桶的额外容量

使用示例:

对资源”GET:/hello”的 name 参数限流,当 name 为”jackson”时,QPS 阈值为 5:

ParamFlowRule rule = new ParamFlowRule();
rule.setResource("GET:/hello");
rule.setParamIdx(0);        // 索引 0 对应 name 参数
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(5);          // QPS 阈值 5
ParamFlowRuleManager.loadRules(Collections.singletonList(rule));

2.6 ParamFlowChecker 核心判断逻辑

ParamFlowChecker#passCheck 是参数限流判断的入口:

public static boolean passCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count,
                             Object... args) {
    if (args == null) {
        return true;
    }
    // 参数索引不合法,放行
    int paramIdx = rule.getParamIdx();
    if (args.length <= paramIdx) {
        return true;
    }
    // 参数值为空,放行
    Object value = args[paramIdx];
    if (value == null) {
        return true;
    }
    // 集群限流
    if (rule.isClusterMode() && rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
        return passClusterCheck(resourceWrapper, rule, count, value);
    }
    // 单机限流
    return passLocalCheck(resourceWrapper, rule, count, value);
}

三种直接放行的情况:

  • 参数为空
  • 参数索引超出范围
  • 参数值为 null

然后分集群限流和单机限流两种情况处理。我们重点看单机限流。

passLocalCheck 方法处理不同类型的参数值:

private static boolean passLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count,
                                      Object value) {
    try {
        // 集合类型:遍历每个元素
        if (Collection.class.isAssignableFrom(value.getClass())) {
            for (Object param : ((Collection)value)) {
                if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
                    return false;
                }
            }
        }
        // 数组类型:遍历每个元素
        else if (value.getClass().isArray()) {
            int length = Array.getLength(value);
            for (int i = 0; i < length; i++) {
                Object param = Array.get(value, i);
                if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
                    return false;
                }
            }
        }
        // 单值类型:直接检查
        else {
            return passSingleValueCheck(resourceWrapper, rule, count, value);
        }
    } catch (Throwable e) {
    }
    return true;
}

参数可能是集合、数组或单值,Sentinel 对三种情况都做了处理。核心逻辑都在 passSingleValueCheck 中。

passSingleValueCheck 根据阈值类型分流控效果:

static boolean passSingleValueCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, 
                                     int acquireCount, Object value) {
    // QPS 模式
    if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
        if (rule.getControlBehavior() == RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) {
            return passThrottleLocalCheck(resourceWrapper, rule, acquireCount, value);
        } else {
            return passDefaultLocalCheck(resourceWrapper, rule, acquireCount, value);
        }
    } 
    // 线程数模式
    else if (rule.getGrade() == RuleConstant.FLOW_GRADE_THREAD) {
        Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
        long threadCount = getParameterMetric(resourceWrapper).getThreadCount(rule.getParamIdx(), value);
        if (exclusionItems.contains(value)) {
            int itemThreshold = rule.getParsedHotItems().get(value);
            return ++threadCount <= itemThreshold;
        }
        long threshold = (long)rule.getCount();
        return ++threadCount <= threshold;
    }
    return true;
}

线程数模式的逻辑很简单:获取当前参数值对应的并行线程数,+1 后如果超过阈值则限流。

你可能好奇:线程数是在哪里自增和自减的呢?

答案是:由 ParamFlowStatisticEntryCallbackParamFlowStatisticExitCallback 两个 Callback 实现,分别在 StatisticSlot 的 entry 和 exit 方法中被回调。

下面我们重点分析 QPS 模式下的两种流控效果。

2.7 快速失败:基于令牌桶算法

快速失败基于令牌桶算法实现。passDefaultLocalCheck 方法控制每个时间窗口只生产一次令牌,将令牌放入令牌桶,每个请求从桶中取令牌,令牌足够则放行,不足则直接拒绝。

ParameterMetrictokenCounters 用作令牌桶,timeCounters 存储最近一次生产令牌的时间。

源码如下:

static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
                                     Object value) {
    // (1) 获取 ParameterMetric 和令牌桶、时间记录器
    ParameterMetric metric = getParameterMetric(resourceWrapper);
    CacheMap<Object, AtomicLong> tokenCounters = metric == null ? null : metric.getRuleTokenCounter(rule);
    CacheMap<Object, AtomicLong> timeCounters = metric == null ? null : metric.getRuleTimeCounter(rule);
    if (tokenCounters == null || timeCounters == null) {
        return true;
    }
    // (2) 计算限流阈值(支持热点参数单独配置)
    Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
    long tokenCount = (long)rule.getCount();
    if (exclusionItems.contains(value)) {
        tokenCount = rule.getParsedHotItems().get(value);
    }
    if (tokenCount == 0) {
        return false;
    }
    // (3) 计算最大令牌数(含突发容量)
    long maxCount = tokenCount + rule.getBurstCount();
    if (acquireCount > maxCount) {
        return false;
    }
    while (true) {
        // (4) 首次访问:初始化令牌桶
        long currentTime = TimeUtil.currentTimeMillis();
        AtomicLong lastAddTokenTime = timeCounters.putIfAbsent(value, new AtomicLong(currentTime));
        if (lastAddTokenTime == null) {
            tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
            return true;
        }
        // (5) 计算与上次生产令牌的时间间隔
        long passTime = currentTime - lastAddTokenTime.get();
        if (passTime > rule.getDurationInSec() * 1000) {
            // (6) 超过一个窗口:重新计算令牌数
            AtomicLong oldQps = tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
            if (oldQps == null) {
                lastAddTokenTime.set(currentTime);
                return true;
            } else {
                long restQps = oldQps.get();
                // 计算需要新增的令牌数
                long toAddCount = (passTime * tokenCount) / (rule.getDurationInSec() * 1000);
                // 新令牌数 = 剩余 + 新增(不超过 maxCount),然后扣除本次请求
                long newQps = toAddCount + restQps > maxCount ? (maxCount - acquireCount)
                    : (restQps + toAddCount - acquireCount);
                if (newQps < 0) {
                    return false;
                }
                if (oldQps.compareAndSet(restQps, newQps)) {
                    lastAddTokenTime.set(currentTime);
                    return true;
                }
                Thread.yield();
            }
        } else {
            // (7) 同一窗口内:直接从令牌桶取令牌
            AtomicLong oldQps = tokenCounters.get(value);
            if (oldQps != null) {
                long oldQpsValue = oldQps.get();
                if (oldQpsValue - acquireCount >= 0) {
                    // 令牌足够,扣减并放行
                    if (oldQps.compareAndSet(oldQpsValue, oldQpsValue - acquireCount)) {
                        return true;
                    }
                } else {
                    // 令牌不足,拒绝
                    return false;
                }
            }
            Thread.yield();
        }
    }
}

整个流程可以总结为:

  1. 获取参数统计对象和令牌桶
  2. 计算限流阈值(支持热点参数单独配置不同阈值)
  3. 首次访问时初始化令牌桶
  4. 超过一个窗口时间,重新计算令牌数并补充
  5. 同一窗口内,直接从令牌桶取令牌,足够则放行,不足则拒绝

2.8 匀速排队:虚拟队列机制

匀速排队的实现原理与 RateLimiterController 一样:让请求在虚拟队列中排队,控制请求通过的时间间隔。如果当前请求的排队等待时间超过 maxQueueingTimeMs,则拒绝。

passThrottleLocalCheck 方法源码:

static boolean passThrottleLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, 
                                       int acquireCount, Object value) {
    // (1) 获取 ParameterMetric 和时间记录器
    ParameterMetric metric = getParameterMetric(resourceWrapper);
    CacheMap<Object, AtomicLong> timeRecorderMap = metric == null ? null : metric.getRuleTimeCounter(rule);
    if (timeRecorderMap == null) {
        return true;
    }
    // (2) 计算限流阈值
    Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
    long tokenCount = (long)rule.getCount();
    if (exclusionItems.contains(value)) {
        tokenCount = rule.getParsedHotItems().get(value);
    }
    if (tokenCount == 0) {
        return false;
    }
    // (3) 计算请求通过的时间间隔(costTime)
    long costTime = Math.round(1.0 * 1000 * acquireCount * rule.getDurationInSec() / tokenCount);
    while (true) {
        long currentTime = TimeUtil.currentTimeMillis();
        // (4) 首次访问:直接放行
        AtomicLong timeRecorder = timeRecorderMap.putIfAbsent(value, new AtomicLong(currentTime));
        if (timeRecorder == null) {
            return true;
        }
        long lastPassTime = timeRecorder.get();
        // 计算当前请求的期望通过时间
        long expectedTime = lastPassTime + costTime;
        // (5) 判断是否可以放行或需要排队
        if (expectedTime <= currentTime 
             || expectedTime - currentTime < rule.getMaxQueueingTimeMs()) {
            AtomicLong lastPastTimeRef = timeRecorderMap.get(value);
            if (lastPastTimeRef.compareAndSet(lastPassTime, currentTime)) {
                long waitTime = expectedTime - currentTime;
                if (waitTime > 0) {
                    lastPastTimeRef.set(expectedTime);
                    try {
                        // 排队等待
                        TimeUnit.MILLISECONDS.sleep(waitTime);
                    } catch (InterruptedException e) {
                        RecordLog.warn("passThrottleLocalCheck: wait interrupted", e);
                    }
                }
                return true;
            } else {
                Thread.yield();
            }
        } else {
            // 等待时间超上限,拒绝
            return false;
        }
    }
}

核心逻辑:

  1. 计算时间间隔:根据阈值和窗口时间,计算每个请求通过的时间间隔
  2. 虚拟队列:用 timeRecorder 记录队列尾部请求的期望通过时间
  3. 排队等待:当前请求的期望通过时间 = 尾部时间 + 间隔;如果等待时间在允许范围内,则 sleep 等待后放行
  4. 超时拒绝:如果等待时间超过 maxQueueingTimeMs,直接拒绝

举个例子:阈值 200 QPS,窗口 1 秒,那么 costTime = 5ms,即每 5ms 只允许通过一个请求。


总结与思考

本文详细介绍了 Sentinel 的两大高级限流功能:黑白名单限流和热点参数限流。下面做一个全面总结。

核心要点回顾

1. 黑白名单限流

特性 说明
本质 基于调用来源的授权控制
策略 白名单(只允许名单内)、黑名单(拒绝名单内)
配置 limitApp 支持多个,用逗号分隔
性能优化 先用 indexOf 快速过滤,再精确匹配
执行时机 在 Slot 链路最前面,优先判断

2. 热点参数限流

特性 说明
本质 基于请求参数值的精细化限流
模块 sentinel-parameter-flow-control(扩展模块)
统计 独立实现滑动窗口,基于 ParamMapBucket
流控效果 支持快速失败和匀速排队
阈值类型 支持 QPS 和线程数

3. 热点参数限流的两种流控效果

  • 快速失败:基于令牌桶,支持突发流量(burstCount)
  • 匀速排队:基于虚拟队列,控制请求通过间隔,支持最大等待时间

实践建议

关于黑白名单限流:

  1. 确保调用来源传递:使用框架适配器时,确保 origin 被正确设置
  2. 白名单优先:安全场景下优先使用白名单,默认拒绝更安全
  3. 结合网关使用:在网关层配置 IP 黑白名单效果更好

关于热点参数限流:

  1. 评估参数取值数量:参数取值越多,内存占用越大,务必提前评估
  2. 线程数模式更省内存:线程数模式下,线程数为 0 时会移除对应 key,内存占用可控
  3. 合理设置窗口时间:durationInSec 不宜过大,否则内存占用高
  4. 热点参数单独配置阈值:利用 exclusionItems 为热点参数配置单独的阈值
  5. 注意参数类型:参数可以是单值、集合或数组,都会被遍历检查

内存占用的注意事项

热点参数限流对内存的影响与参数取值的可能性数量成正比。参数取值越多,占用内存越大,对性能影响也越大。

举例说明

  • 根据商品 ID 限流,如果有十万个商品,CacheMap 中就会有十万个 key-value
  • 这些数据不会被自动移除,会随着进程运行持续增长
  • 线程数模式不存在这个问题,因为线程数为 0 时会移除对应的 key

因此,在使用热点参数限流时,一定要充分考虑参数的取值范围,避免内存溢出风险。

黑白名单限流和热点参数限流是 Sentinel 的两大高级功能,掌握它们能够帮助你实现更精细化的流量控制。下一篇文章我们将探讨如何自定义 ProcessorSlot 实现开关降级,敬请期待。

© 版权声明
THE END
喜欢就支持一下吧
点赞14
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!