热点参数限流
概念
何为热点?热点即经常访问的数据。很多时候我们希望统计热点数据中访问频次最高的 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;
}
}
}