GatewayFlowSlot网关限流
sentinel支持api网关限流策略,其中包括 Spring Cloud Gateway。对于api网关限流,sentinel支持针对不同 route 或自定义的 API 分组进行限流,但是默认不支持 URL 粒度。
自定义分组可以通过api实现或通过dashboard页面配置,用户可以通过自定义分组将一些需要限流的url添加进来,支持精确匹配、前缀匹配、正则匹配,然后针对不同分组设置对应的限流规则,即可进行限流
通过api设置分组
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("some_customized_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/ahas"));
add(new ApiPathPredicateItem().setPattern("/product/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("another_customized_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
这里设置了两个分组,并设置了对应匹配分组的规则
或者我们可以通dashboard设置分组,接口在GatewayApiController类,我们着重看一下保存逻辑
@PostMapping("/save.json")
public Result<ApiDefinitionEntity> updateApi(HttpServletRequest request, @RequestBody UpdateApiReqVo reqVo) {
AuthService.AuthUser authUser = authService.getAuthUser(request);
String app = reqVo.getApp();
if (StringUtil.isBlank(app)) {
return Result.ofFail(-1, "app can't be null or empty");
}
authUser.authTarget(app, AuthService.PrivilegeType.WRITE_RULE);
Long id = reqVo.getId();
if (id == null) {
return Result.ofFail(-1, "id can't be null");
}
ApiDefinitionEntity entity = repository.findById(id);
if (entity == null) {
return Result.ofFail(-1, "api does not exist, id=" + id);
}
// 匹配规则列表
List<ApiPredicateItemVo> predicateItems = reqVo.getPredicateItems();
if (CollectionUtils.isEmpty(predicateItems)) {
return Result.ofFail(-1, "predicateItems can't empty");
}
List<ApiPredicateItemEntity> predicateItemEntities = new ArrayList<>();
for (ApiPredicateItemVo predicateItem : predicateItems) {
ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity();
// 匹配模式
int matchStrategy = predicateItem.getMatchStrategy();
if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX).contains(matchStrategy)) {
return Result.ofFail(-1, "Invalid matchStrategy: " + matchStrategy);
}
predicateItemEntity.setMatchStrategy(matchStrategy);
// 匹配串
String pattern = predicateItem.getPattern();
if (StringUtil.isBlank(pattern)) {
return Result.ofFail(-1, "pattern can't be null or empty");
}
predicateItemEntity.setPattern(pattern);
predicateItemEntities.add(predicateItemEntity);
}
entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities));
Date date = new Date();
entity.setGmtModified(date);
try {
entity = repository.save(entity);
} catch (Throwable throwable) {
logger.error("update gateway api error:", throwable);
return Result.ofThrowable(-1, throwable);
}
if (!publishApis(app, entity.getIp(), entity.getPort())) {
logger.warn("publish gateway apis fail after update");
}
return Result.ofSuccess(entity);
}
这里可以看到就是针对不同属性的处理,然后将配置的自定义分组推送到客户端
private boolean publishApis(String app, String ip, Integer port) {
List<ApiDefinitionEntity> apis = repository.findAllByMachine(MachineInfo.of(app, ip, port));
return sentinelApiClient.modifyApis(app, ip, port, apis);
}
public boolean modifyApis(String app, String ip, int port, List<ApiDefinitionEntity> apis) {
if (apis == null) {
return true;
}
try {
AssertUtil.notEmpty(app, "Bad app name");
AssertUtil.notEmpty(ip, "Bad machine IP");
AssertUtil.isTrue(port > 0, "Bad machine port");
String data = JSON.toJSONString(
apis.stream().map(r -> r.toApiDefinition()).collect(Collectors.toList()));
Map<String, String> params = new HashMap<>(2);
params.put("data", data);
String result = executeCommand(app, ip, port, MODIFY_GATEWAY_API_PATH, params, true).get();
logger.info("Modify gateway apis: {}", result);
return true;
} catch (Exception e) {
logger.warn("Error when modifying gateway apis", e);
return false;
}
}
发送gateway/updateApiDefinitions请求,客户端会处理逻辑在UpdateGatewayApiDefinitionGroupCommandHandler
@Override
public CommandResponse<String> handle(CommandRequest request) {
String data = request.getParam("data");
if (StringUtil.isBlank(data)) {
return CommandResponse.ofFailure(new IllegalArgumentException("Bad data"));
}
try {
data = URLDecoder.decode(data, "utf-8");
} catch (Exception e) {
RecordLog.info("Decode gateway API definition data error", e);
return CommandResponse.ofFailure(e, "decode gateway API definition data error");
}
RecordLog.info("[API Server] Receiving data change (type: gateway API definition): {0}", data);
String result = SUCCESS_MSG;
Set<ApiDefinition> apiDefinitions = parseJson(data);
GatewayApiDefinitionManager.loadApiDefinitions(apiDefinitions);
return CommandResponse.ofSuccess(result);
}
这里解析页面下发的自定义分组配置然调用loadApiDefinitions,创建自定义分组然后保存到API_MAP中
同样页面在配置网关流控规则时会被客户端UpdateGatewayRuleCommandHandler类处理
@Override
public CommandResponse<String> handle(CommandRequest request) {
String data = request.getParam("data");
if (StringUtil.isBlank(data)) {
return CommandResponse.ofFailure(new IllegalArgumentException("Bad data"));
}
try {
data = URLDecoder.decode(data, "utf-8");
} catch (Exception e) {
RecordLog.info("Decode gateway rule data error", e);
return CommandResponse.ofFailure(e, "decode gateway rule data error");
}
RecordLog.info(String.format("[API Server] Receiving rule change (type: gateway rule): %s", data));
String result = SUCCESS_MSG;
List<GatewayFlowRule> flowRules = JSONArray.parseArray(data, GatewayFlowRule.class);
//设置网关流控规则
GatewayRuleManager.loadRules(new HashSet<>(flowRules));
return CommandResponse.ofSuccess(result);
}
GatewayRuleManager负责管理网关流控规则,通过loadRules保存网关流控规则,触发GatewayRulePropertyListener回调方法
@Override
public void configUpdate(Set<GatewayFlowRule> conf) {
applyGatewayRuleInternal(conf);
RecordLog.info("[GatewayRuleManager] Gateway flow rules received: " + GATEWAY_RULE_MAP);
}
@Override
public void configLoad(Set<GatewayFlowRule> conf) {
applyGatewayRuleInternal(conf);
RecordLog.info("[GatewayRuleManager] Gateway flow rules loaded: " + GATEWAY_RULE_MAP);
}
将网关流控规则转换为热点参数规则
private synchronized void applyGatewayRuleInternal(Set<GatewayFlowRule> conf) {
//删除规则
if (conf == null || conf.isEmpty()) {
applyToConvertedParamMap(new HashSet<ParamFlowRule>());
GATEWAY_RULE_MAP.clear();
return;
}
//网关流控集合
Map<String, Set<GatewayFlowRule>> gatewayRuleMap = new ConcurrentHashMap<>();
//有参规则时候设置的下标
Map<String, Integer> idxMap = new HashMap<>();
//热点参数规则集合
Set<ParamFlowRule> paramFlowRules = new HashSet<>();
//存放没有设置热点参数的规则
Map<String, List<GatewayFlowRule>> noParamMap = new HashMap<>();
for (GatewayFlowRule rule : conf) {
//校验规则失败直接跳过
if (!isValidRule(rule)) {
RecordLog.warn("[GatewayRuleManager] Ignoring invalid rule when loading new rules: " + rule);
continue;
}
//资源名称
String resourceName = rule.getResource();
//没有热点参数
if (rule.getParamItem() == null) {
// Cache the rules with no parameter config, then skip.
List<GatewayFlowRule> noParamList = noParamMap.get(resourceName);
if (noParamList == null) {
noParamList = new ArrayList<>();
noParamMap.put(resourceName, noParamList);
}
noParamList.add(rule);
} else {
//每种资源对于热点参数索引值
int idx = getIdxInternal(idxMap, resourceName);
// Convert to parameter flow rule.
//将gatewayRule转化为paramRule,并添加到热点参数规则集合中
if (paramFlowRules.add(GatewayRuleConverter.applyToParamRule(rule, idx))) {
//更新热点参数索引值
idxMap.put(rule.getResource(), idx + 1);
}
cacheRegexPattern(rule.getParamItem());
}
// Apply to the gateway rule map.
//保存所有的规则
Set<GatewayFlowRule> ruleSet = gatewayRuleMap.get(resourceName);
if (ruleSet == null) {
ruleSet = new HashSet<>();
gatewayRuleMap.put(resourceName, ruleSet);
}
ruleSet.add(rule);
}
// Handle non-param mode rules.
//处理没有设置热点参数的规则
for (Map.Entry<String, List<GatewayFlowRule>> e : noParamMap.entrySet()) {
List<GatewayFlowRule> rules = e.getValue();
if (rules == null || rules.isEmpty()) {
continue;
}
for (GatewayFlowRule rule : rules) {
int idx = getIdxInternal(idxMap, e.getKey());
// Always use the same index (the last position).
//同样也会转换为paramFlowRule,但是参数索引值相同
paramFlowRules.add(GatewayRuleConverter.applyNonParamToParamRule(rule, idx));
}
}
applyToConvertedParamMap(paramFlowRules);
GATEWAY_RULE_MAP.clear();
GATEWAY_RULE_MAP.putAll(gatewayRuleMap);
}
private void applyToConvertedParamMap(Set<ParamFlowRule> paramFlowRules) {
Map<String, List<ParamFlowRule>> newRuleMap = ParamFlowRuleUtil.buildParamRuleMap(
new ArrayList<>(paramFlowRules));
if (newRuleMap == null || newRuleMap.isEmpty()) {
// No parameter flow rules, so clear all the metrics.
//清空所有统计信息
for (String resource : CONVERTED_PARAM_RULE_MAP.keySet()) {
ParameterMetricStorage.clearParamMetricForResource(resource);
}
RecordLog.info("[GatewayRuleManager] No gateway rules, clearing parameter metrics of previous rules");
CONVERTED_PARAM_RULE_MAP.clear();
return;
}
// Clear unused parameter metrics.
//删除无用的参数统计信息
Set<String> previousResources = CONVERTED_PARAM_RULE_MAP.keySet();
for (String resource : previousResources) {
if (!newRuleMap.containsKey(resource)) {
ParameterMetricStorage.clearParamMetricForResource(resource);
}
}
// Apply to converted rule map.
CONVERTED_PARAM_RULE_MAP.clear();
CONVERTED_PARAM_RULE_MAP.putAll(newRuleMap);
RecordLog.info("[GatewayRuleManager] Converted internal param rules: " + CONVERTED_PARAM_RULE_MAP);
}
无论是否针对请求属性进行限流,Sentinel 底层都会将网关流控规则转化为热点参数规则(ParamFlowRule),存储在 GatewayRuleManager 中,与正常的热点参数规则相隔离。转换时 Sentinel 会根据请求属性配置,为网关流控规则设置参数索引(idx),并同步到生成的热点参数规则中。
将有参网关限流规则转换为热点参数限流规则,如果pattern为空则针对所有热点参数都做流控,如果不为空则只有匹配的参数才做流控,否则直接放行
static ParamFlowRule applyToParamRule(/*@Valid*/ GatewayFlowRule gatewayRule, int idx) {
ParamFlowRule paramRule = new ParamFlowRule(gatewayRule.getResource())
.setCount(gatewayRule.getCount())
.setGrade(gatewayRule.getGrade())
.setDurationInSec(gatewayRule.getIntervalSec())
.setBurstCount(gatewayRule.getBurst())
.setControlBehavior(gatewayRule.getControlBehavior())
.setMaxQueueingTimeMs(gatewayRule.getMaxQueueingTimeoutMs())
.setParamIdx(idx);
//参数限流配置
GatewayParamFlowItem gatewayItem = gatewayRule.getParamItem();
// Apply the current idx to gateway rule item.
//设置index下标,index自增,用户热点参数
gatewayItem.setIndex(idx);
// Apply for pattern-based parameters.
// 针对有GatewayParamFlowItem.pattern
String valuePattern = gatewayItem.getPattern();
//设置例外
if (valuePattern != null) {
paramRule.getParamFlowItemList().add(generateNonMatchPassParamItem());
}
return paramRule;
}
将无参网关流控规则,转换为热点参数规则
static ParamFlowRule applyNonParamToParamRule(/*@Valid*/ GatewayFlowRule gatewayRule, int idx) {
return new ParamFlowRule(gatewayRule.getResource())
.setCount(gatewayRule.getCount())
.setGrade(gatewayRule.getGrade())
.setDurationInSec(gatewayRule.getIntervalSec())
.setBurstCount(gatewayRule.getBurst())
.setControlBehavior(gatewayRule.getControlBehavior())
.setMaxQueueingTimeMs(gatewayRule.getMaxQueueingTimeoutMs())
.setParamIdx(idx);
}
这里仅仅是属性的copy,无参网关流控规则最主要特点是,转换后热点规则的idx参数下标始终保持不变,是参数列表的最后一个元素。
网关接入限流规则通过SentinelGatewayFilter
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//路由
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
Mono<Void> asyncResult = chain.filter(exchange);
if (route != null) {
//路由id
String routeId = route.getId();
//热点参数
Object[] params = paramParser.parseParameterFor(routeId, exchange,
r -> r.getResourceMode() == SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID);
String origin = Optional.ofNullable(GatewayCallbackManager.getRequestOriginParser())
.map(f -> f.apply(exchange))
.orElse("");
asyncResult = asyncResult.transform(
new SentinelReactorTransformer<>(new EntryConfig(routeId, EntryType.IN,
1, params, new ContextConfig(contextName(routeId), origin)))
);
}
//匹配用户自定义的限流Api分组
Set<String> matchingApis = pickMatchingApiDefinitions(exchange);
for (String apiName : matchingApis) {
//热点参数
Object[] params = paramParser.parseParameterFor(apiName, exchange,
r -> r.getResourceMode() == SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME);
asyncResult = asyncResult.transform(
new SentinelReactorTransformer<>(new EntryConfig(apiName, EntryType.IN, 1, params))
);
}
return asyncResult;
}
解析获取热点参数
public Object[] parseParameterFor(String resource, T request, Predicate<GatewayFlowRule> rulePredicate) {
if (StringUtil.isEmpty(resource) || request == null || rulePredicate == null) {
return new Object[0];
}
Set<GatewayFlowRule> gatewayRules = new HashSet<>();
Set<Boolean> predSet = new HashSet<>();
boolean hasNonParamRule = false;
for (GatewayFlowRule rule : GatewayRuleManager.getRulesForResource(resource)) {
//设置了热点数据限流
if (rule.getParamItem() != null) {
gatewayRules.add(rule);
predSet.add(rulePredicate.test(rule));
} else {
hasNonParamRule = true;
}
}
if (!hasNonParamRule && gatewayRules.isEmpty()) {
return new Object[0];
}
//route:规则中设置了针对Api分组
if (predSet.size() > 1 || predSet.contains(false)) {
return new Object[0];
}
int size = hasNonParamRule ? gatewayRules.size() + 1 : gatewayRules.size();
//热点参数数组
Object[] arr = new Object[size];
for (GatewayFlowRule rule : gatewayRules) {
GatewayParamFlowItem paramItem = rule.getParamItem();
int idx = paramItem.getIndex();
//解析获取参数值
String param = parseInternal(paramItem, request);
arr[idx] = param;
}
//最后一个设置默认值
if (hasNonParamRule) {
arr[size - 1] = SentinelGatewayConstants.GATEWAY_DEFAULT_PARAM;
}
return arr;
}
这里首先根据资源名称配合的route或者资源资源分组名称获取所有的网关流控规则,然后判断这些规则是否包含没有参数的规则,然后设置返回参数数组长度。解析设置的参数放入参数数组,如果有没有参数的规则,则将参数数组的最后一位设置成默认参数$D,为了进行资源的所有无参数例外项的热点规则流控校验
private String parseInternal(GatewayParamFlowItem item, T request) {
switch (item.getParseStrategy()) {
//请求源ip
case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_CLIENT_IP:
return parseClientIp(item, request);
//host
case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HOST:
return parseHost(item, request);
//请求header中取值
case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER:
return parseHeader(item, request);
//请求url中取值
case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM:
return parseUrlParameter(item, request);
//cookie中取值
case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_COOKIE:
return parseCookie(item, request);
default:
return null;
}
}
private String parseClientIp(/*@Valid*/ GatewayParamFlowItem item, T request) {
String clientIp = requestItemParser.getRemoteAddress(request);
String pattern = item.getPattern();
if (StringUtil.isEmpty(pattern)) {
return clientIp;
}
return parseWithMatchStrategyInternal(item.getMatchStrategy(), clientIp, pattern);
}
private String parseWithMatchStrategyInternal(int matchStrategy, String value, String pattern) {
if (value == null) {
return null;
}
switch (matchStrategy) {
//精确匹配
case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_EXACT:
return value.equals(pattern) ? value : SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM;
//子串
case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_CONTAINS:
return value.contains(pattern) ? value : SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM;
//正则
case SentinelGatewayConstants.PARAM_MATCH_STRATEGY_REGEX:
Pattern regex = GatewayRegexCache.getRegexPattern(pattern);
if (regex == null) {
return value;
}
return regex.matcher(value).matches() ? value : SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM;
default:
return value;
}
}
解析参数支持请求源ip、host、header中取值、url中取值、cookie中取值。如果没有设置pattern则直接返回取值结果,如果设置pattern则可能是针对某类参数生效,支持3种模式精确匹配、子串匹配和正则匹配,如果匹配成功则返回参数值否则返回默认值$NM,它会匹配热点参数规则的参数例外项,但是例外项的阈值是100万,等同于直接放行
asyncResult = asyncResult.transform(
new SentinelReactorTransformer<>(new EntryConfig(routeId, EntryType.IN,
1, params, new ContextConfig(contextName(routeId), origin)))
);
@Override
public Publisher<T> apply(Publisher<T> publisher) {
if (publisher instanceof Mono) {
return new MonoSentinelOperator<>((Mono<T>) publisher, entryConfig);
}
if (publisher instanceof Flux) {
return new FluxSentinelOperator<>((Flux<T>) publisher, entryConfig);
}
throw new IllegalStateException("Publisher type is not supported: " + publisher.getClass().getCanonicalName());
}
@Override
public void subscribe(CoreSubscriber<? super T> actual) {
source.subscribe(new SentinelReactorSubscriber<>(entryConfig, actual, true));
}
最终都会走到SentinelReactorSubscriber拦截
@Override
protected void hookOnSubscribe(Subscription subscription) {
doWithContextOrCurrent(() -> currentContext().getOrEmpty(SentinelReactorConstants.SENTINEL_CONTEXT_KEY),
this::entryWhenSubscribed);
}
private void entryWhenSubscribed() {
//定义的配置
ContextConfig sentinelContextConfig = entryConfig.getContextConfig();
if (sentinelContextConfig != null) {
// If current we're already in a context, the context config won't work.
//初始化context
ContextUtil.enter(sentinelContextConfig.getContextName(), sentinelContextConfig.getOrigin());
}
try {
//执行限流逻辑
AsyncEntry entry = SphU.asyncEntry(entryConfig.getResourceName(), entryConfig.getEntryType(),
entryConfig.getAcquireCount(), entryConfig.getArgs());
this.currentEntry = entry;
actual.onSubscribe(this);
} catch (BlockException ex) {
// Mark as completed (exited) explicitly.
entryExited.set(true);
// Signal cancel and propagate the {@code BlockException}.
cancel();
actual.onSubscribe(this);
actual.onError(ex);
} finally {
if (sentinelContextConfig != null) {
ContextUtil.exit();
}
}
}
到这里就会进入sentinel的限流处理逻辑,进入GatewayFlowSlot处理
public class GatewayFlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resource, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
checkGatewayParamFlow(resource, count, args);
fireEntry(context, resource, node, count, prioritized, args);
}
private void checkGatewayParamFlow(ResourceWrapper resourceWrapper, int count, Object... args)
throws BlockException {
if (args == null) {
return;
}
//获取流控规则
List<ParamFlowRule> rules = GatewayRuleManager.getConvertedParamRules(resourceWrapper.getName());
if (rules == null || rules.isEmpty()) {
return;
}
//和热点参数限流处理逻辑一样
for (ParamFlowRule rule : rules) {
// Initialize the parameter metrics.
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);
}
}
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
}
这里可以看到底层还是使用ParamFlowChecker.passCheck做热点参数规则校验。
- 网关流控维度支持route或自定义分组,也就是资源名称配置支持路由或自定义分组名称
- 无论是否针对请求属性进行限流,Sentinel 底层都会将网关流控规则转化为热点参数规则(ParamFlowRule),存储在 GatewayRuleManager 中
- GatewayFlowRule中可以设置GatewayParamFlowItem,如果GatewayParamFlowItem为空则和普通流控规则相同,如果不为空比如设置了clientIp,再判断是否设置了匹配模式,如果设置了匹配模式则会根据匹配的clientIp进行统计限流,否则针对所有的clientIp进行统计限流。