案例
下面是官方提供的配置案例。
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct
public void doInit() {
// 加载自定义API分组
initCustomizedApis();
// 加载网关流控规则
initGatewayRules();
}
}
复制代码
简单介绍一下几个组件的作用:
- SentinelGatewayBlockExceptionHandler:用于处理BlockException;
- SentinelGatewayFilter:适配SpringCloudGateway的GlobalFilter实现类;
- initCustomizedApis:加载自定义API分组;
- initGatewayRules:加载网关流控规则;
1、确定资源
默认情况下,Sentinel会使用Gateway的routeId路由id作为资源名称。
比如下面配置了两个路由,分别对应两个资源,资源名称即为route.id(aliyun_route和httpbin_route)。
spring:
cloud:
gateway:
enabled: true
routes:
- id: aliyun_route
uri: https://www.aliyun.com/
predicates:
- Path=/product/**
- id: httpbin_route
uri: https://httpbin.org
predicates:
- Path=/httpbin/**
filters:
- RewritePath=/httpbin/(?<segment>.*), /${segment}
复制代码
但是路由维度的流控规则很难满足需求。所以Sentinel在与Gateway集成时提出了API分组的概念。
比如通过Sentinel Dashboard的API管理中,可以新增API分组。
API分组也可以通过编码方式配置,如官方案例GatewayConfiguration的initCustomizedApis方法。
// GatewayConfiguration.java
@PostConstruct
public void doInit() {
// 加载自定义API分组
initCustomizedApis();
// 加载网关流控规则
initGatewayRules();
}
private void initCustomizedApis() {
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);
}
复制代码
ApiPathPredicateItem匹配模式。
public class ApiPathPredicateItem implements ApiPredicateItem {
// 模式字符串
private String pattern;
// 匹配策略 0-精确 1-前缀 2-正则
private int matchStrategy = SentinelGatewayConstants.URL_MATCH_STRATEGY_EXACT;
}
复制代码
ApiDefinition对应API分组。其中apiName会作为资源名称使用。
public class ApiDefinition {
// 自定义api分组名称
private String apiName;
// 匹配模式
private Set<ApiPredicateItem> predicateItems;
}
复制代码
2、网关流控规则
Sentinel为Gateway专门设计了网关流控规则。
Dashboard对应GatewayFlowRule。
public class GatewayFlowRule {
// 资源名称
private String resource;
// 资源类型(API类型)0-routeId 1-API分组
private int resourceMode = SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID;
// 阈值类型 0-线程数 1-QPS
private int grade = RuleConstant.FLOW_GRADE_QPS;
// 阈值
private double count;
// 间隔(时间窗口)(秒)
private long intervalSec = 1;
// 流控方式(流控效果)0-快速失败(令牌桶) 2-匀速排队(漏桶)
private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;
// 应对突发请求时额外允许的请求数目
private int burst;
// 在流控方式为匀速排队的情况下,排队最大时长
private int maxQueueingTimeoutMs = 500;
// 针对请求属性 如ip、host、header
private GatewayParamFlowItem paramItem;
}
复制代码
每个Gateway的流控规则,支持配置一个GatewayParamFlowItem,支持从ip、host、header中提取参数,作为方法入参,即SphU.entry方法的args参数。后文称配置了GatewayParamFlowItem的网关流控规则为:有参网关流控规则,反之为无参网关流控规则。
public class GatewayParamFlowItem {
// 热点规则参数下标 --- 通过计算得到
private Integer index;
// 解析策略 如 ip or header
private int parseStrategy;
// 参数名称
private String fieldName;
// 匹配参数值模式
private String pattern;
// 匹配策略
private int matchStrategy = SentinelGatewayConstants.PARAM_MATCH_STRATEGY_EXACT;
}
复制代码
3、管理网关流控规则
由于Sentinel区分了普通流控规则和网关流控规则,所以网关流控规则需要有单独的Manager管理。
GatewayRuleManager负责管理网关流控规则。
不仅如此,它还将GatewayFlowRule转换为了ParamFlowRule,这意味着网关热点参数规则复用了普通热点参数规则的逻辑。(普通热点参数规则是不支持普通流控规则的,后面看他怎么做二次适配)
public final class GatewayRuleManager {
// 原始网关流控规则
private static final Map<String, Set<GatewayFlowRule>> GATEWAY_RULE_MAP = new ConcurrentHashMap<>();
// 转换后 热点参数规则
private static final Map<String, List<ParamFlowRule>> CONVERTED_PARAM_RULE_MAP = new ConcurrentHashMap<>();
}
复制代码
GatewayRuleManager.GatewayRulePropertyListener的applyGatewayRuleInternal方法,将网关流控规则,转换为热点参数规则。主要逻辑分为有参网关流控规则和无参网关流控规则。
// GatewayRuleManager.GatewayRulePropertyListener.java
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) {
String resourceName = rule.getResource();
if (rule.getParamItem() == null) {
// ... 统计noParamMap
} else {
// 转换有参网关流控规则
int idx = getIdxInternal(idxMap, resourceName);
if (paramFlowRules.add(GatewayRuleConverter.applyToParamRule(rule, idx))) {
idxMap.put(rule.getResource(), idx + 1);
}
cacheRegexPattern(rule.getParamItem());
}
Set<GatewayFlowRule> ruleSet = gatewayRuleMap.get(resourceName);
if (ruleSet == null) {
ruleSet = new HashSet<>();
gatewayRuleMap.put(resourceName, ruleSet);
}
ruleSet.add(rule);
}
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());
// 转换无参网关流控规则
paramFlowRules.add(GatewayRuleConverter.applyNonParamToParamRule(rule, idx));
}
}
// 保存热点参数规则 到CONVERTED_PARAM_RULE_MAP
applyToConvertedParamMap(paramFlowRules);
// 保存原始网关流控规则 到GATEWAY_RULE_MAP
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()) {
for (String resource : CONVERTED_PARAM_RULE_MAP.keySet()) {
ParameterMetricStorage.clearParamMetricForResource(resource);
}
CONVERTED_PARAM_RULE_MAP.clear();
return;
}
// 清理老的热点流控规则Metric
for (Map.Entry<String, List<ParamFlowRule>> entry : CONVERTED_PARAM_RULE_MAP.entrySet()) {
// ...
}
// 保存热点流控规则
CONVERTED_PARAM_RULE_MAP.clear();
CONVERTED_PARAM_RULE_MAP.putAll(newRuleMap);
}
复制代码
重点关注两个转换方法。
GatewayRuleConverter.applyToParamRule,将有参网关流控规则,转换为热点参数规则。
除了GatewayFlowRule拷贝属性到ParamFlowRule以外,对于GatewayParamFlowItem有配置pattern的情况,设置了热点规则的参数例外项。
generateNonMatchPassParamItem方法生成参数例外项。参数类型是String,阈值是100万,参数值匹配$NM字符串,理论上不会匹配任何入参。为什么这么做,也是为了适配原来的热点流控规则(方法没有入参,不会走热点流控规则校验)。
// GatewayRuleConverter.java
static ParamFlowRule applyToParamRule(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();
// 设置index下标,用于后续热点规则校验,同一个资源的不同网关流控规则,index自增
gatewayItem.setIndex(idx);
// 针对有GatewayParamFlowItem.pattern的情况,需要给热点参数规则加例外项
String valuePattern = gatewayItem.getPattern();
if (valuePattern != null) {
paramRule.getParamFlowItemList().add(generateNonMatchPassParamItem());
}
return paramRule;
}
static ParamFlowItem generateNonMatchPassParamItem() {
return new ParamFlowItem().setClassType(String.class.getName())
.setCount(1000_0000)
.setObject(SentinelGatewayConstants.GATEWAY_NOT_MATCH_PARAM);
}
// SentinelGatewayConstants.java
public final class SentinelGatewayConstants {
public static final String GATEWAY_NOT_MATCH_PARAM = "$NM";
}
复制代码
GatewayRuleConverter.applyNonParamToParamRule,将无参网关流控规则,转换为热点参数规则,仅仅是拷贝GatewayFlowRule属性到ParamFlowRule上。无参网关流控规则最主要特点是,转换后热点规则的idx参数下标始终保持不变,是参数列表的最后一个元素。
// GatewayRuleConverter.java
static ParamFlowRule applyNonParamToParamRule(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);
}
复制代码
以上GatewayFlowRule转换为ParamFlowRule仅仅是网关流控适配热点参数规则的第一步。
总结来说如下图:
-
无参网关流控规则:热点规则参数下标等于有参规则数量,例外项为空,利用热点参数规则实现了普通流控规则;
-
有参网关流控规则:热点规则参数下标自增
- 无pattern的情况下,代表不需要做模式匹配,例外项为空,意思是针对args[x]不同参数分别做流量控制;
- 有pattern的情况下,代表需要做模式匹配,例外项为$NM,阈值100万,意思是仅针对匹配上的args[x]做流量控制,其他没匹配上的直接放行(走例外项阈值100万),仅做流量统计;
4、GlobalFilter
Sentinel实现了自己的全局拦截器SentinelGatewayFilter,在SpringCloudGateway中接入网关流控规则逻辑。
public class SentinelGatewayFilter implements GatewayFilter, GlobalFilter, Ordered {
// 参数解析器
private final GatewayParamParser<ServerWebExchange> paramParser = new GatewayParamParser<>(
new ServerWebExchangeItemParser());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
Mono<Void> asyncResult = chain.filter(exchange);
// 路由id作为资源名称
if (route != null) {
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, ResourceTypeConstants.COMMON_API_GATEWAY,
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, ResourceTypeConstants.COMMON_API_GATEWAY,
EntryType.IN, 1, params))
);
}
return asyncResult;
}
}
复制代码
SentinelGatewayFilter的filter方法:
- 根据请求,选择资源,routeId是肯定存在的,另外自定义API分组可能存在;
- 使用GatewayParamParser解析器,解析params参数,作为后期SphU.entry的args入参;
- SentinelReactorTransformer负责实际拦截逻辑;
4-1、GatewayParamParser
GatewayParamParser解析器,是适配热点参数规则的第二步。
public class GatewayParamParser<T> {
private final RequestItemParser<T> requestItemParser;
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];
}
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();
// 从请求中匹配参数。如果pattern非空且匹配不中,返回$NM,命中例外项,不限制阈值;
// 否则使用资源统一阈值
String param = parseInternal(paramItem, request);
arr[idx] = param;
}
if (hasNonParamRule) {
// $D
arr[size - 1] = SentinelGatewayConstants.GATEWAY_DEFAULT_PARAM;
}
return arr;
}
private String parseInternal(GatewayParamFlowItem item, T request) {
switch (item.getParseStrategy()) {
case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_CLIENT_IP: // ip
return parseClientIp(item, request);
case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HOST: // host
return parseHost(item, request);
case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER: // header
return parseHeader(item, request);
case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM: // urlParam
return parseUrlParameter(item, request);
case SentinelGatewayConstants.PARAM_PARSE_STRATEGY_COOKIE: // cookie
return parseCookie(item, request);
default:
return null;
}
}
}
复制代码
parseParameterFor方法,关注几个点:
- 入参数组大小:如果资源没有无参网关流控规则,数组大小=有参网关流控规则数量;反之,数组大小=无参网关流控规则数量+1;
- 无参网关流控规则存在的情况下,入参数组最后一位,用GATEWAY_DEFAULT_PARAM($D)填充。这一步是为了让最后一个参数存在,能进入热点参数规则,进行资源的所有无参数例外项的热点规则流控校验;
- 有参网关流控规则,用parseInternal方法解析规则参数,填充入参数组,数组下标取自于GatewayRuleConverter.applyToParamRule。parseInternal支持5种策略:ip、host、header、urlParam、cookie;
以GatewayParamFlowItem匹配ip为例,会进入parseClientIp方法。
如果pattern为空,表示当前网关流控规则没针对ip,只是取ip作为args入参;
如果pattern不为空,表示当前网关流控规则,针对某类ip生效,进入parseWithMatchStrategyInternal方法继续解析。
// GatewayParamParser.java
private String parseClientIp(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);
}
复制代码
parseWithMatchStrategyInternal根据matchStrategy匹配策略、pattern模式匹配字符串(ip字符串需要满足的模式)匹配value原始字符串(ip)。
目前支持精确、包含、正则三种匹配策略,如果匹配成功,返回原始字符串,否则返回GATEWAY_NOT_MATCH_PARAM($NM)。
这与前面GatewayRuleConverter.applyToParamRule转换有参网关流控规则相呼应,代表ip如果不匹配,热点规则校验时流控阈值为100万,近似于直接放行。
// GatewayParamParser.java
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;
}
}
复制代码
至此可以理解到网关流控规则和热点参数规则之间的关系:
- 无参网关流控规则,使用$D作为args中的最后一个入参,这是为了能够进入热点规则校验逻辑(如果参数为空,热点规则会直接放行,见ParamFlowChecker#passCheck)。对应热点参数规则没参数例外项,所以会用配置的阈值作为热点规则校验的阈值;
- 有参网关流控规则,在参数模式匹配成功的情况下,用原始字符串作为args入参,他不会匹配热点参数规则的参数例外项,所以也会使用配置的阈值作为热点规则校验的阈值;
- 有参网关流控规则,在参数模式匹配失败的情况下,用$NM作为args中的入参,它会匹配热点参数规则的参数例外项,但是例外项的阈值是100万,等同于直接放行;
4-2、SentinelReactorTransformer
SentinelReactorTransformer转换器,在sentinel-reactor-adapter模块中,用于适配基于reactor模式开发的框架,例如SpringWebFlux、SpringCloudGateway。每次请求,都会new一个SentinelReactorTransformer。
public class SentinelReactorTransformer<T> implements Function<Publisher<T>, Publisher<T>> {
private final EntryConfig entryConfig;
public SentinelReactorTransformer(String resourceName) {
this(new EntryConfig(resourceName));
}
public SentinelReactorTransformer(EntryConfig entryConfig) {
AssertUtil.notNull(entryConfig, "entryConfig cannot be null");
this.entryConfig = entryConfig;
}
@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());
}
}
复制代码
SentinelReactorTransformer构造时传入了EntryConfig,里面存储了所有执行sentinel api需要的信息。
public class EntryConfig {
// 资源名称
private final String resourceName;
// 入口流量 or 出口流量
private final EntryType entryType;
// 资源类型 route.id or api分组
private final int resourceType;
// 获取数量
private final int acquireCount;
// 入参
private final Object[] args;
// 上下文配置
private final ContextConfig contextConfig;
}
public class ContextConfig {
// 上下文
private final String contextName;
// 来源
private final String origin;
}
复制代码
SentinelReactorTransformer的apply方法,将原有publisher做了一次包装。例如MonoSentinelOperator。
public class MonoSentinelOperator<T> extends MonoOperator<T, T> {
private final EntryConfig entryConfig;
@Override
public void subscribe(CoreSubscriber<? super T> actual) {
source.subscribe(new SentinelReactorSubscriber<>(entryConfig, actual, true));
}
}
复制代码
SentinelReactorSubscriber才实际实现拦截逻辑。
public class SentinelReactorSubscriber<T> extends InheritableBaseSubscriber<T> {
// sentinel api需要的参数信息
private final EntryConfig entryConfig;
// 理解为被拦截的后续业务逻辑
private final CoreSubscriber<? super T> actual;
// Mono or Flux
private final boolean unary;
// 当前请求对应的AsyncEntry
private volatile AsyncEntry currentEntry;
// 是否已经执行过AsyncEntry.exit
private final AtomicBoolean entryExited = new AtomicBoolean(false);
}
复制代码
SentinelReactorSubscriber的hookOnSubscribe定义拦截逻辑。
// SentinelReactorSubscriber.java
@Override
protected void hookOnSubscribe(Subscription subscription) {
doWithContextOrCurrent(() -> currentContext().getOrEmpty(SentinelReactorConstants.SENTINEL_CONTEXT_KEY),
this::entryWhenSubscribed);
}
private void doWithContextOrCurrent(Supplier<Optional<com.alibaba.csp.sentinel.context.Context>> contextSupplier, Runnable f) {
Optional<com.alibaba.csp.sentinel.context.Context> contextOpt = contextSupplier.get();
if (!contextOpt.isPresent()) {
f.run();
} else {
ContextUtil.runOnContext(contextOpt.get(), f);
}
}
复制代码
entryWhenSubscribed方法最终创建AsyncEntry,这里entryConfig.getArgs是上面GatewayParamParser解析得到的。AsyncEntry被存储到SentinelReactorSubscriber中,用于后续退出和记录异常。
// SentinelReactorSubscriber.java
private void entryWhenSubscribed() {
ContextConfig sentinelContextConfig = entryConfig.getContextConfig();
if (sentinelContextConfig != null) {
ContextUtil.enter(sentinelContextConfig.getContextName(), sentinelContextConfig.getOrigin());
}
try {
AsyncEntry entry = SphU.asyncEntry(entryConfig.getResourceName(), entryConfig.getResourceType(),
entryConfig.getEntryType(), entryConfig.getAcquireCount(), entryConfig.getArgs());
this.currentEntry = entry;
actual.onSubscribe(this);
} catch (BlockException ex) {
entryExited.set(true);
cancel();
actual.onSubscribe(this);
actual.onError(ex);
} finally {
if (sentinelContextConfig != null) {
ContextUtil.exit();
}
}
}
复制代码
在请求正常完成后hookOnComplete,或发生异常后hookOnError,都会执行tryCompleteEntry退出AsyncEntry。
// SentinelReactorSubscriber.java
@Override
protected void hookOnComplete() {
tryCompleteEntry();
actual.onComplete();
}
@Override
protected void hookOnError(Throwable t) {
if (currentEntry != null && currentEntry.getAsyncContext() != null) {
Tracer.traceContext(t, 1, currentEntry.getAsyncContext());
}
tryCompleteEntry();
actual.onError(t);
}
private boolean tryCompleteEntry() {
if (currentEntry != null && entryExited.compareAndSet(false, true)) {
currentEntry.exit(1, entryConfig.getArgs());
return true;
}
return false;
}
复制代码
5、GatewayFlowSlot
最后对于SpringCloudGateway,Sentinel注入了GatewayFlowSlot负责处理网关流控规则。
其底层还是使用ParamFlowChecker.passCheck做热点参数规则校验,只不过规则的数据来源是GatewayRuleManager。
@Spi(order = -4000)
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) {
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);
}
}
复制代码
GatewayFlowSlot在Sentinel规则校验责任链所处位置如下:
总结
1、网关流控规则,默认可以使用SpringCloudGateway的Route的id作为资源名称。可以通过配置自定义API分组的方式,匹配多个自定义请求路径,将API分组名称作为资源名称。
2、Sentinel为Gateway专门设计了GatewayFlowRule模型,代表网关流控规则,由GatewayRuleManager管理并转换为热点参数规则ParamFlowRule。网关流控规则底层是适配了热点参数规则。
3、GatewayFlowRule可以设置GatewayParamFlowItem,对应上图表单中几个控件(针对请求属性=true,参数属性、属性值匹配、匹配模式、匹配串)。不同的配置方式,结果不同:
- 如果不针对请求属性,GatewayParamFlowItem为null。代表这个资源被整体流控,类似普通流控;
- 如果针对请求属性,参数属性选择Client IP(GatewayParamFlowItem.parseStrategy=0),但不勾选属性值匹配(GatewayParamFlowItem.pattern=null) 。代表针对不同的ClientIP统计不同ip的流量,执行热点规则校验;
- 如果针对请求属性,参数属性选择Client IP(GatewayParamFlowItem.parseStrategy=0),且勾选属性值匹配,匹配模式为精确(GatewayParamFlowItem.matchStrategy=0),匹配串为192.169.0.105(GatewayParamFlowItem.pattern) 。代表针对192.169.0.105的ip的请求,才执行热点规则校验,这是与热点规则校验含义最不一样的地方;
上述这些逻辑,都是通过适配热点参数规则来完成的,比较难理解。
4、Sentinel实现了自己的全局拦截器SentinelGatewayFilter,在SpringCloudGateway中接入网关流控规则逻辑。
5、Sentinel注入了GatewayFlowSlot负责处理网关流控规则。其底层还是使用ParamFlowChecker.passCheck做热点参数规则校验,只不过规则的数据来源是GatewayRuleManager。