Ali-Sentinel-热点流控

归档

测试

  • 控制台启动参考:WebUI-更新规则-控制台

  • 模块:sentinel-demo-cluster-embedded

  • com.alibaba.csp.sentinel.demo.cluster.app.ClusterDemoApplication

    • 更改如下:
@SpringBootApplication
public class ClusterDemoApplication {
    public static void main(String[] args) {
        System.setProperty("csp.sentinel.dashboard.server", "127.0.0.1:8080");
        System.setProperty("project.name", "My-Cluster-8866");
        System.setProperty("server.port", "10010");
        SpringApplication.run(ClusterDemoApplication.class, args);
    }
}
  • com.alibaba.csp.sentinel.demo.cluster.app.service.DemoService
    • 更改如下:
@Service
public class DemoService {

    @SentinelResource(
            value = "sayHello", // 设置资源名,不设置的话会用方法全称
            blockHandler = "sayHelloBlockHandler"
    )
    public String sayHello(String name) {
        return "Hello, " + name;
    }

    public String sayHelloBlockHandler(String name, BlockException ex) {
        return String.format("Oops, [%s] blocked by Sentinel", name);
    }
}
  • 启动后

    • 访问:http://localhost:10010/hello/test1
  • 在控制台设置规则

    • 点击 簇点链路:http://127.0.0.1:8080/#/dashboard/identity/My-Cluster-8866
      • sayHello 列,点击 + 热点
        • 参数索引 设置为:0
        • 单机阈值 设置为:20
        • 其他使用默认值
      • 点击 新增
        // POST http://127.0.0.1:8080//paramFlow/rule
        {
            "app": "My-Cluster-8866",
            "ip": "10.32.51.130",
            "port": "8720",
            "rule": {
                "resource": "sayHello",
                "grade": 1,
                "paramFlowItemList": [],
                "count": 20,
                "limitApp": "default",
                "controlBehavior": 0,
                "durationInSec": 1,
                "burstCount": 0,
                "maxQueueingTimeMs": 0,
                "clusterMode": false,
                "clusterConfig": {
                    "thresholdType": 0,
                    "fallbackToLocalWhenFail": true
                },
                "paramIdx": 0
            }
        }
        
    • 新增完会跳转到 热点规则:http://127.0.0.1:8080/#/dashboard/paramFlow/My-Cluster-8866
      • 点击 编辑
      • 再点 高级选项
        • 参数类型 选择:java.lang.String
        • 参数值 设置为:test1
        • 限流阈值 设置为:2
        • 点击 + 添加
      • 点击 保存
        // PUT http://127.0.0.1:8080//paramFlow/rule/2
        {
            "id": 2,
            "app": "My-Cluster-8866",
            "ip": "10.32.51.130",
            "port": 8720,
            "rule": {
                "id": null,
                "resource": "sayHello",
                "limitApp": "default",
                "grade": 1,
                "paramIdx": 0,
                "count": 20,
                "controlBehavior": 0,
                "maxQueueingTimeMs": 0,
                "burstCount": 0,
                "durationInSec": 1,
                "paramFlowItemList": [
                    {
                        "object": "test1",
                        "classType": "java.lang.String",
                        "count": 2
                    }
                ],
                "clusterMode": false,
                "clusterConfig": {
                    "flowId": null,
                    "thresholdType": 0,
                    "fallbackToLocalWhenFail": true,
                    "sampleCount": 10,
                    "windowIntervalMs": 1000
                }
            },
            "gmtCreate": null,
            "gmtModified": null
        }
        
  • 再测试

    • 连续刷 3 次:http://localhost:10010/hello/test1
    • 出现:Oops, [test1] blocked by Sentinel

原理

  • 模块:sentinel-parameter-flow-control

    • 其 SPI 配置文件 ../META-INF/services/com.alibaba.csp.sentinel.slotchain.ProcessorSlot
  • com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot

@Spi(order = -3000)
public class ParamFlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable 
    {
        ... // 如果资源没设置热点规则,则传给下个节点并返回

        checkFlow(resourceWrapper, count, args);                                // 检测规则,ref: sign_m_100
        fireEntry(context, resourceWrapper, node, count, prioritized, args);    // 传给下游
    }

    // sign_m_100 检测规则
    void checkFlow(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
        ... // args 为 null 校验 & 资源是否设置有规则校验

        List<ParamFlowRule> rules = ParamFlowRuleManager.getRulesOfResource(resourceWrapper.getName());

        for (ParamFlowRule rule : rules) {
            applyRealParamIdx(rule, args.length);   // 重新设置下索引,ref: sign_m_110

            // 初始化指标 (不存在则创建,存在则不处理也不重置),ref: sign_m_150
            ParameterMetricStorage.initParamMetricsFor(resourceWrapper, rule);

            if (!ParamFlowChecker.passCheck(resourceWrapper, rule, count, args)) {  // 规则检验 (不通过则报错),ref: sign_m_210
                String triggeredParam = "";
                ... // 从参数数组里获取校验索引的值,即: = args[idx]
                throw new ParamFlowException(resourceWrapper.getName(), triggeredParam, rule);
            }
        }
    }

    /**
     * sign_m_110 设置正确的索引:
     * 
     *   1. len: 5, idx: -3 (倒数第 3 个)
     *      idx = 5 + (-3) = 2
     * 
     *   2. len: 5, idx: -6 (倒数第 6 个,无效索引)
     *      idx = -(-6) = 6
     */
    void applyRealParamIdx(ParamFlowRule rule, int length) {
        int paramIdx = rule.getParamIdx();
        if (paramIdx < 0) {
            if (-paramIdx <= length) {
                rule.setParamIdx(length + paramIdx);
            } else {
                // 非法索引,给它一个非法的正值,后面的规则检查就会通过。ref: sign_m_210
                rule.setParamIdx(-paramIdx);
            }
        }
    }
}

参数计量指标

  • com.alibaba.csp.sentinel.slots.block.flow.param.ParameterMetricStorage
// 指标存储器
public final class ParameterMetricStorage {

    // 存储 map
    private static final Map<String, ParameterMetric> metricsMap = new ConcurrentHashMap<>();
    
    // sign_m_150 初始化指标 (不存在则创建,存在则不处理也不重置)
    public static void initParamMetricsFor(ResourceWrapper resourceWrapper, ParamFlowRule rule) {
        ... // 资源或名称为空,返回

        String resourceName = resourceWrapper.getName();
        ParameterMetric metric;     // 资源名对应的指标已存在,则不创建
        if ((metric = metricsMap.get(resourceName)) == null) {
            synchronized (LOCK) {   // DCL
                if ((metric = metricsMap.get(resourceName)) == null) {
                    metric = new ParameterMetric(); // 创建指标
                    metricsMap.put(resourceName, metric);
                }
            }
        }
        metric.initialize(rule);    // 初始化规则相关数据,ref: sign_m_160 
    }
}
// 对应的是单个资源的(参数)指标
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<>();

    // sign_m_160 初始化规则相关数据 (不存在则创建,存在则不处理也不重置)
    public void initialize(ParamFlowRule rule) {
        // 用规则作 key
        if (!ruleTimeCounters.containsKey(rule)) {
            synchronized (lock) {   // DCL
                if (ruleTimeCounters.get(rule) == null) {
                    long size = Math.min(BASE_PARAM_MAX_CAPACITY * rule.getDurationInSec(), TOTAL_MAX_CAPACITY);
                    // ConcurrentLinkedHashMapWrapper 只是对 ConcurrentLinkedHashMap 进行封装,
                    // 其 ref: https://blog.csdn.net/rockvine/article/details/122852332
                    ruleTimeCounters.put(rule, new ConcurrentLinkedHashMapWrapper<Object, AtomicLong>(size));
                }
            }
        }

        if (!ruleTokenCounter.containsKey(rule)) {
            ... // 类似上面初始化
        }

        /**
         * 用规则参数的索引作 key
         * 对应的指标计量参考: ParamFlowStatisticEntryCallback 和 ParamFlowStatisticExitCallback
         *          回调参考: 链路控制-StatisticSlot sign_call_100 | sign_call_200
         */
        if (!threadCountMap.containsKey(rule.getParamIdx())) {
            ... // 类似上面初始化
        }
    }
}

热点参数流控校验器

  • com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowChecker
public final class ParamFlowChecker {

    // sign_m_210 规则检验
    public static boolean passCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count,
                             Object... args) 
    {
        ... // args 为 null,返回 true 表示校验通过

        int paramIdx = rule.getParamIdx();

        ... // args 长度不满足,返回 true 表示校验通过,对应上面: sign_m_110

        Object value = args[paramIdx];

        ... // value 为 ParamFlowArgument 时,使用其 paramFlowKey
        ... // value 为 null,返回 true 表示校验通过

        ... // 集群模式校验

        return passLocalCheck(resourceWrapper, rule, count, value); // 本地校验,ref: sign_m_212
    }

    // sign_m_212 本地校验
    private static boolean passLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count,
                                          Object value) 
    {
        try {
            if (Collection.class.isAssignableFrom(value.getClass())) {
                ... // 是集合,则对集合里的每个元素进行校验
            } else if (value.getClass().isArray()) {
                ... // 是数组,则对数组的每个元素进行校验
            } else {
                return passSingleValueCheck(resourceWrapper, rule, count, value);   // 单个值校验,ref: sign_m_213
            }
        } ... // catch 

        return true;    // 校验过程中出现异常,则当作"通过"
    }

    // sign_m_213 单个值校验
    static boolean passSingleValueCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
                                        Object value) 
    {
        if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
            if (rule.getControlBehavior() == RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) {
                return passThrottleLocalCheck(resourceWrapper, rule, acquireCount, value);
            } else {
                // 默认是 CONTROL_BEHAVIOR_DEFAULT,看懂这个,其他 2 个逻辑也会觉得简单,且目前 Web 还不支持其他 2 种设置
                // 使用默认校验,ref: sign_m_214
                return passDefaultLocalCheck(resourceWrapper, rule, acquireCount, value);
            }
        } else if (rule.getGrade() == RuleConstant.FLOW_GRADE_THREAD) {
            ... // 通过线程数进行流控
        }

        return true;
    }

    // sign_m_214 默认校验
    static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
                                         Object value) 
    {
        ParameterMetric metric = getParameterMetric(resourceWrapper);
        CacheMap<Object, AtomicLong> tokenCounters = metric == null ? null : metric.getRuleTokenCounter(rule);
        CacheMap<Object, AtomicLong> timeCounters = metric == null ? null : metric.getRuleTimeCounter(rule);

        ... // 规则没有对应的 tokenCounters 或 timeCounters,则返回 true 表示校验通过

        // 计算最大 token 数(阈值)
        // rule.getParsedHotItems() 相当于: {"test1": 2}
        Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
        long tokenCount = (long) rule.getCount();   // 先用规则阈值
        if (exclusionItems.contains(value)) {       // 有参数例外项,就用单独设置的阈值
            tokenCount = rule.getParsedHotItems().get(value);
        }

        ... // tokenCount 为 0,直接不通过

        long maxCount = tokenCount + rule.getBurstCount();  // 加上突发设置数 (def: 0)

        ... // maxCount 判断

        while (true) {
            long currentTime = TimeUtil.currentTimeMillis();

            AtomicLong lastAddTokenTime = timeCounters.putIfAbsent(value, new AtomicLong(currentTime)); // sign_step_001
            if (lastAddTokenTime == null) {
                // 令牌从未添加,只需补充令牌并立即消耗 acquireCount 即可。
                tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));  // sign_step_002
                return true;    // 通过
            }

            // 计算自上次添加令牌以来的持续时间。
            long passTime = currentTime - lastAddTokenTime.get();
            // 一种简化的令牌桶算法,仅当统计窗口过去时才会补充令牌。
            if (passTime > rule.getDurationInSec() * 1000) {    // 超出时间窗口
                AtomicLong oldQps = tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));  // sign_step_003
                if (oldQps == null) {
                    /**
                     * 线程 T1 执行完 sign_step_001,还没执行 sign_step_002;
                     * 线程 T2 执行   sign_step_001,lastAddTokenTime 不为 null,
                     *         在执行 sign_step_003 时,oldQps 就会为 null,因此会进入此 if。
                     */
                    lastAddTokenTime.set(currentTime);  // 重置时间窗口 (确实可能不准)
                    return true;
                } else {
                    long restQps = oldQps.get();
                    /**
                     * 5s 后再请求进来,则 passTime = 5000;
                     *    参数 acquireCount = 1;
                     * 则:
                     *   toAddCount = (5000 * 2) / (1 * 1000) = 10;
                     *   newQps = (10 + 1) > 2 ? (2 - 1) : (1 + 10 - 1) = 1;
                     *          相当于:最新剩余的 token 数。
                     */
                    long toAddCount = (passTime * tokenCount) / (rule.getDurationInSec() * 1000);
                    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;    // CAS 成功,通过
                    }
                    Thread.yield();     // CAS 失败,再循环一次
                }
            } else {    // 在时间窗口内
                AtomicLong oldQps = tokenCounters.get(value);   // 旧的剩余 token 数 (别被命名影响)
                if (oldQps != null) {
                    long oldQpsValue = oldQps.get();
                    if (oldQpsValue - acquireCount >= 0) {      // 能满足请求
                        if (oldQps.compareAndSet(oldQpsValue, oldQpsValue - acquireCount)) {
                            return true;                        // CAS 成功则通过
                        }
                    } else {
                        return false;                           // 剩余 token 数不足,不通过
                    }
                }
                Thread.yield(); // 并发时,上面的 CAS 不成功,则休息下,再循环一次
            }
        }
    }

}
  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值