Sentinel架构篇 - 热点参数限流

热点参数限流

概念

何为热点?热点即经常访问的数据。很多时候我们希望统计热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制。
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制。

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看作是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,并结合令牌桶算法来进行参数级别的流控。

令牌桶算法(Token Bucket Strategy)

假设令牌的生成速率为 2r/s,则每隔 500 毫秒就往令牌桶中添加一个令牌。

令牌桶的容量是固定的,当桶满时,新添加的令牌会被拒绝或者丢弃。

当一个 n 字节大小的数据包到达时,从令牌桶中删除 n 个令牌,接着数据包会被发送到网络上。

如果令牌桶中不足 n 个令牌,则不会删除令牌,并且数据包会被限流(要么丢弃、要么在缓冲区等待)。

令牌桶算法可以应对突发流量。

热点参数规则

对应 ParamFlowRule。

属性 说明 默认值
resource 资源名
count 限流阈值
grade 限流模式(0-线程数、1-QPS) 1(即QPS模式)
durationInSec 统计时间窗口长度(单位为秒) 1(即1秒)
controlBehavior 流控效果(支持 0-快速失败、2-匀速排队) 0(即快速失败)
maxQueueingTimeMs 最大排队等待时长(仅在匀速排队模式下生效) 0
paramIdx 热点参数的索引
paramFlowItemList 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型。
clusterMode 是否是集群参数流控规则 false
clusterConfig 集群流控相关配置

实际操作

在 Nacos 控制台中的 配置管理/配置列表的 public 命名空间添加一个如下配置:

dataId:spring-cloud-demo-consumer-sentinel-param-flow

group:DEFAULT_GROUP

配置内容为:

[
    {
    
    
        "resource": "flow",
        "limitApp": "default",
        "count": 30,
        "grade": 1,
        "paramIdx": 0,
        "controlBehavior": 2,
        "maxQueueingTimeMs": 5000,
        "durationInSec": 3,
        "paramFlowItemList": [
            {
    
    
                "object": "flow",
                "count": 2,
                "classType": "java.lang.String"
            }
        ]
    }
]

当三秒内 QPS 超过 30 时,多余的请求进入排队。如果 5000 毫秒内这些请求没有得到处理就会导致请求失败。如果请求的第一个参数的参数值为 flow,则 QPS 阈值改为 2 再进行限流判断。

在这里插入图片描述

对应的客户端的配置文件如下:

spring:
    application:
        name: spring-cloud-demo-consumer
    cloud:
        nacos:
            discovery:
                server-addr: 10.211.55.11:8848,10.211.55.12:8848,10.211.55.13:8848
                enabled: true
        sentinel:
            transport:
                dashboard: 127.0.0.1:9000
            eager: true
            datasource:
                param-flow-nacos-datasource:
                    nacos:
                        server-addr: 10.211.55.11:8848,10.211.55.12:8848,10.211.55.13:8848
                        group-id: DEFAULT_GROUP
                        namespace: public
                        data-id: ${
    
    spring.application.name}-sentinel-param-flow
                        data-type: json
                        rule-type: param-flow
                        username: nacos

ParamFlowSlot

ParamFlowSlot 继承于 AbstractLinkedProcessorSlot<DefaultNode>,而且标注了 @Spi(order = -3000) 注解。

首先看下它对于 entry、exit 方法的具体实现。

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    
    
  	// 如果ParamFlowRuleManager没有加载到任何热点参数规则,则直接交给下一个ProcessSlot继续处理
    if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
    
    
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
        return;
    }
	// 热点参数规则的校验 
    checkFlow(resourceWrapper, count, args);
  	// 交给下一个ProcessSlot继续处理
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
    
    
  	// 交给下一个ProcessSlot继续处理
    fireExit(context, resourceWrapper, count, args);
}

接下来看下 checkFlow 方法的具体逻辑。

void checkFlow(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
    
    
  	// 校验请求参数列表是否为空
    if (args == null) {
    
    
        return;
    }
  	// 校验热点参数规则列表是否为空
    if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
    
    
        return;
    }
  	// 获取指定资源的热点参数规则列表
    List<ParamFlowRule> rules = ParamFlowRuleManager.getRulesOfResource(resourceWrapper.getName());
		// 遍历热点参数规则列表
    for (ParamFlowRule rule : rules) {
    
    
      	// 处理热点参数规则中的paramIdx参数值小于0的情况
        applyRealParamIdx(rule, args.length);
		// 初始化相关指标
        ParameterMetricStorage.initParamMetricsFor(resourceWrapper, rule);
		// 使用参数流控检查器检查请求是否可以通过,如果没有通过,则抛出ParamFlowException异常
        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、applyRealParamIdx

处理热点参数规则中的paramIdx参数值小于0的情况

void applyRealParamIdx(/*@NonNull*/ ParamFlowRule rule, int length) {
    
    
  	// 获取热点参数规则中的paramIdx参数值
    int paramIdx = rule.getParamIdx();
  	// 对paramIdx参数值小于0的处理
    if (paramIdx < 0) {
    
    
        if (-paramIdx <= length) {
    
    
            rule.setParamIdx(length + paramIdx);
        } else {
    
    
            rule.setParamIdx(-paramIdx);
        }
    }
}

2、initParamMetricsFor

初始化相关指标

public static void initParamMetricsFor(ResourceWrapper resourceWrapper, /*@Valid*/ ParamFlowRule rule) {
    
    
    if (resourceWrapper == null || resourceWrapper.getName() == null) {
    
    
        return;
    }
    String resourceName = resourceWrapper.getName();
    ParameterMetric metric;
    if ((metric = metricsMap.get(resourceName)) == null) {
    
    
        synchronized (LOCK) {
    
    
            if ((metric = metricsMap.get(resourceName)) == null) {
    
    
                metric = new ParameterMetric();
              	// 使用双重检查锁将资源名称、ParameterMetric实例放到缓存中
                metricsMap.put(resourceWrapper.getName(), metric);
                RecordLog.info("[ParameterMetricStorage] Creating parameter metric for: {}", resourceWrapper.getName());
            }
        }
    }
  	// 初始化热点参数规则相关的计数器。
    metric.initialize(rule);
}

initialize

初始化热点参数规则相关的计数器。

public void initialize(ParamFlowRule rule) {
    
    
    if (!ruleTimeCounters.containsKey(rule)) {
    
    
        synchronized (lock) {
    
    
            if (ruleTimeCounters.get(rule) == null) {
    
    
                long size = Math.min(BASE_PARAM_MAX_CAPACITY * rule.getDurationInSec(), TOTAL_MAX_CAPACITY);
                ruleTimeCounters.put(rule, new ConcurrentLinkedHashMapWrapper<Object, AtomicLong>(size));
            }
        }
    }

    if (!ruleTokenCounter.containsKey(rule)) {
    
    
        synchronized (lock) {
    
    
            if (ruleTokenCounter.get(rule) == null) {
    
    
                long size = Math.min(BASE_PARAM_MAX_CAPACITY * rule.getDurationInSec(), TOTAL_MAX_CAPACITY);
                ruleTokenCounter.put(rule, new ConcurrentLinkedHashMapWrapper<Object, AtomicLong>(size));
            }
        }
    }

    if (!threadCountMap.containsKey(rule.getParamIdx())) {
    
    
        synchronized (lock) {
    
    
            if (threadCountMap.get(rule.getParamIdx()) == null) {
    
    
                threadCountMap.put(rule.getParamIdx(),
                    new ConcurrentLinkedHashMapWrapper<Object, AtomicInteger>(THREAD_COUNT_MAX_CAPACITY));
            }
        }
    }
}

3、passCheck

校验请求是否可以通过

public static boolean passCheck(ResourceWrapper resourceWrapper, /*@Valid*/ ParamFlowRule rule, /*@Valid*/ int count,
                         Object... args) {
    
    
    // 如果请求参数列表为空,则返回true,表示请求通过
  	if (args == null) {
    
    
        return true;
    }
	// 获取热点参数规则中的paramIdx参数值
    int paramIdx = rule.getParamIdx();
  	// 如果paramIdx参数值小于等于请求参数列表的长度,则返回true,表示请求通过
    if (args.length <= paramIdx) {
    
    
        return true;
    }

    // 获取指定下标的参数值
    Object value = args[paramIdx];

    if (value instanceof ParamFlowArgument) {
    
    
        value = ((ParamFlowArgument) value).paramFlowKey();
    }
    // 如果参数值为空,则返回true,表示请求通过
    if (value == null) {
    
    
        return true;
    }

  	// 如果热点参数规则的clusterMode设置为true(集群模式),并且grade设置为1(QPS模式)
    if (rule.isClusterMode() && rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
    
    
      	// 进入集群热点参数规则的判断(该部分在集群流控的文章中进一步分析)
        return passClusterCheck(resourceWrapper, rule, count, value);
    }
	// 进入本地热点参数规则的判断
    return passLocalCheck(resourceWrapper, rule, count, value);
}

3.1 本地热点参数规则的判断

接下来先看下 passLocalCheck 方法的具体处理逻辑。

private static boolean passLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count,
                                      Object value) {
    
    
    try {
    
    
      	// 如果是一个集合
        if (Collection.class.isAssignableFrom(value.getClass())) {
    
    
          	// 对集合进行遍历,只要有一个元素没有通过校验,则返回false
            for (Object param : ((Collection)value)) {
    
    
                if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
    
    
                    return false;
                }
            }
        // 如果是一个数组
        } else if (value.getClass().isArray()) {
    
    
            int length = Array.getLength(value);
          	// 对数组进行遍历,只要有一个元素没有通过校验,则返回false
            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) {
    
    
        RecordLog.warn("[ParamFlowChecker] Unexpected error", e);
    }

    return true;
}

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;
        }
      	// 获取热点参数规则中的count参数值
        long threshold = (long)rule.getCount();
      	// 对线程数加一,再判断是否<=阈值
        return ++threadCount <= threshold;
    }

    return true;
}

passDefaultLocalCheck

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);

    if (tokenCounters == null || timeCounters == null) {
    
    
        return true;
    }

    Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
  	// 获取热点参数规则中的count参数值
    long tokenCount = (long)rule.getCount();
    if (exclusionItems.contains(value)) {
    
    
        tokenCount = rule.getParsedHotItems().get(value);
    }

    if (tokenCount == 0) {
    
    
        return false;
    }
	// 计算令牌桶的最大容量	
    long maxCount = tokenCount + rule.getBurstCount();
  	// 如果请求的令牌数 > 最大令牌数,则返回false,表示请求失败
    if (acquireCount > maxCount) {
    
    
        return false;
    }

    while (true) {
    
    
        long currentTime = TimeUtil.currentTimeMillis();
		// 更新最近的获取令牌的时间,并返回上一次的获取令牌的时间
        AtomicLong lastAddTokenTime = timeCounters.putIfAbsent(value, new AtomicLong(currentTime));
      	// 如果是第一次获取令牌
        if (lastAddTokenTime == null) {
    
    
            // 更新剩余令牌数
            tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
          	// 返回true,表示请求成功
            return true;
        }

        // 获取现在距离上一次获取令牌时的间隔时间
        long passTime = currentTime - lastAddTokenTime.get();
        // 如果间隔时间大于一个统计周期  
        if (passTime > rule.getDurationInSec() * 1000) {
    
    
            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);
              	// 计算剩余的令牌数
                long newQps = toAddCount + restQps > maxCount ? (maxCount - acquireCount)
                    : (restQps + toAddCount - acquireCount);
								// 如果剩余的令牌数 < 0,则返回false,表示请求失败
                if (newQps < 0) {
    
    
                    return false;
                }
              	// 更新剩余令牌数以及上一次获取令牌的时间,并返回true,表示请求成功
                if (oldQps.compareAndSet(restQps, newQps)) {
    
    
                    lastAddTokenTime.set(currentTime);
                    return true;
                }
                Thread.yield();
            }
        // 如果间隔时间少于或者等于一个统计周期  
        } else {
    
    
            AtomicLong oldQps = tokenCounters.get(value);
            if (oldQps != null) {
    
    
                long oldQpsValue = oldQps.get();
              	// 如果请求的令牌数 <= 剩余的令牌数
                if (oldQpsValue - acquireCount >= 0) {
    
    
                  	// 更新剩余令牌数,并返回true,表示请求成功
                    if (oldQps.compareAndSet(oldQpsValue, oldQpsValue - acquireCount)) {
    
    
                        return true;
                    }
                // 否则返回false,表示请求失败 
                } else {
    
    
                    return false;
                }
            }
            Thread.yield();
        }
    }
}

passThrottleLocalCheck

static boolean passThrottleLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
                                      Object value) {
    
    
    ParameterMetric metric = getParameterMetric(resourceWrapper);
    CacheMap<Object, AtomicLong> timeRecorderMap = metric == null ? null : metric.getRuleTimeCounter(rule);
    if (timeRecorderMap == null) {
    
    
        return true;
    }

    Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
  	// 获取热点参数规则的count参数值
    long tokenCount = (long)rule.getCount();
    if (exclusionItems.contains(value)) {
    
    
        tokenCount = rule.getParsedHotItems().get(value);
    }

    if (tokenCount == 0) {
    
    
        return false;
    }
	// 计算每次获取一个令牌花费的时间
    long costTime = Math.round(1.0 * 1000 * acquireCount * rule.getDurationInSec() / tokenCount);
    while (true) {
    
    
        long currentTime = TimeUtil.currentTimeMillis();
        AtomicLong timeRecorder = timeRecorderMap.putIfAbsent(value, new AtomicLong(currentTime));
        if (timeRecorder == null) {
    
    
            return true;
        }
        long lastPassTime = timeRecorder.get();
        long expectedTime = lastPassTime + costTime;
		// 如果下一次拿到令牌的时间 <= 当前时间 或者 下一次拿到令牌的时间 < 当前时间 + 热点参数规则设置的最大排队时间
        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 {
    
    
                      	// 线程sleep
                        TimeUnit.MILLISECONDS.sleep(waitTime);
                    } catch (InterruptedException e) {
    
    
                        RecordLog.warn("passThrottleLocalCheck: wait interrupted", e);
                    }
                }
              	// 返回true,表示请求成功
                return true;
            } else {
    
    
                Thread.yield();
            }
        // 返回false,表示请求失败  
        } else {
    
    
            return false;
        }
    }
}

猜你喜欢

转载自blog.csdn.net/qq_34561892/article/details/129425041