官网:https://sentinelguard.io/zh-cn/docs/api-gateway-flow-control.html
sentinel支持对主流的网关应用(zuul、gateway等)进行限流
*************
实现原理
网关流控规则处理
# 规则加载
GatewayRuleManager加载网关流控规则(GatewayFlowRule),
# 规则转换
无论是否针对请求属性进行限流,都会将网关流控规则转化为热点参数规则(ParamFlowRule)
转换时根据请求属性配置,为网关流控规则设置参数索引(idx)
如果规则不针对请求属性,就在参数最后一个位置置入预设的常量,达到普通流控的效果
# 规则存储
将转换后的热点参数规则存储在GatewayRuleManager中,与正常的热点参数规则隔离
网关请求处理
# 请求过滤:SentinelGatewayFilter
外部请求进入网关时会用SentinelGatewayFilter过滤,执行route id、api分组匹配;
# 请求属性处理:GatewayFlowRule、ParamFlowRule
根据配置的网关流控规则来解析请求属性(用于热点参数限流),
依照参数索引顺序组装参数数组,最终传入SphU.entry(res, args);
# 网关限流:GatewayFlowSlot
sentinel在slot chain中添加GatewayFlowSlot,
从GatewayRuleManager提取网关限流规则,执行限流操作
GatewayRuleManager:处理网关规则
public final class GatewayRuleManager {
private static final Map> GATEWAY_RULE_MAP = new ConcurrentHashMap();
private static final Map> CONVERTED_PARAM_RULE_MAP = new ConcurrentHashMap();
private static final GatewayRuleManager.GatewayRulePropertyListener LISTENER = new GatewayRuleManager.GatewayRulePropertyListener();
private static SentinelProperty> currentProperty = new DynamicSentinelProperty();
private static final Set FIELD_REQUIRED_SET;
public static void register2Property(SentinelProperty> property) {
AssertUtil.notNull(property, "property cannot be null");
synchronized(LISTENER) {
RecordLog.info("[GatewayRuleManager] Registering new property to gateway flow rule manager", new Object[0]);
currentProperty.removeListener(LISTENER);
property.addListener(LISTENER);
currentProperty = property;
}
}
public static boolean loadRules(Set rules) {
return currentProperty.updateValue(rules);
}
public static Set getRules() { //获取所有规则
Set rules = new HashSet();
Iterator var1 = GATEWAY_RULE_MAP.values().iterator();
while(var1.hasNext()) {
Set ruleSet = (Set)var1.next();
rules.addAll(ruleSet);
}
return rules;
}
public static Set getRulesForResource(String resourceName) {
//获取指定资源的限流规则
if (StringUtil.isBlank(resourceName)) {
return new HashSet();
} else {
Set set = (Set)GATEWAY_RULE_MAP.get(resourceName);
return set == null ? new HashSet() : new HashSet(set);
}
}
public static List getConvertedParamRules(String resourceName) {
//获取转换后的热点参数规则
return (List)(StringUtil.isBlank(resourceName) ? new ArrayList() : (List)CONVERTED_PARAM_RULE_MAP.get(resourceName));
}
public static boolean isValidRule(GatewayFlowRule rule) { //检验是否是有效规则
if (rule != null && !StringUtil.isBlank(rule.getResource()) && rule.getResourceMode() >= 0 && rule.getGrade() >= 0 && !(rule.getCount() < 0.0D) && rule.getBurst() >= 0 && rule.getControlBehavior() >= 0) {
if (rule.getGrade() == 2 && rule.getMaxQueueingTimeoutMs() < 0) {
return false;
} else if (rule.getIntervalSec() <= 0L) {
return false;
} else {
GatewayParamFlowItem item = rule.getParamItem();
return item != null ? isValidParamItem(item) : true;
}
} else {
return false;
}
}
static boolean isValidParamItem(GatewayParamFlowItem item) {
private GatewayRuleManager() {
}
static {
currentProperty.addListener(LISTENER);
FIELD_REQUIRED_SET = new HashSet(Arrays.asList(3, 2, 4));
}
*********
内部类:GatewayRulePropertyListener
private static final class GatewayRulePropertyListener implements PropertyListener> {
private GatewayRulePropertyListener() {
}
public void configUpdate(Set conf) {
this.applyGatewayRuleInternal(conf);
RecordLog.info("[GatewayRuleManager] Gateway flow rules received: " + GatewayRuleManager.GATEWAY_RULE_MAP, new Object[0]);
}
public void configLoad(Set conf) {
this.applyGatewayRuleInternal(conf);
RecordLog.info("[GatewayRuleManager] Gateway flow rules loaded: " + GatewayRuleManager.GATEWAY_RULE_MAP, new Object[0]);
}
private int getIdxInternal(Map idxMap, String resourceName) {
private void cacheRegexPattern(GatewayParamFlowItem item) {
private synchronized void applyGatewayRuleInternal(Set conf) {
private void applyToConvertedParamMap(Set paramFlowRules) {
SentinelGatewayFilter:处理网关请求
public class SentinelGatewayFilter implements GatewayFilter, GlobalFilter, Ordered {
private final int order;
private final GatewayParamParser paramParser;
public SentinelGatewayFilter() {
this(-2147483648);
}
public SentinelGatewayFilter(int order) {
this.paramParser = new GatewayParamParser(new ServerWebExchangeItemParser());
this.order = order;
}
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
Mono asyncResult = chain.filter(exchange);
String apiName;
if (route != null) {
String routeId = route.getId(); //解析routrId数据
Object[] params = this.paramParser.parseParameterFor(routeId, exchange, (r) -> {
return r.getResourceMode() == 0;
});
apiName = (String)Optional.ofNullable(GatewayCallbackManager.getRequestOriginParser()).map((f) -> {
return (String)f.apply(exchange);
}).orElse("");
//解析自定义分组数据
asyncResult = asyncResult.transform(new SentinelReactorTransformer(new EntryConfig(routeId, 3, EntryType.IN, 1, params, new ContextConfig(this.contextName(routeId), apiName))));
}
Set matchingApis = this.pickMatchingApiDefinitions(exchange);
Object[] params;
for(Iterator var10 = matchingApis.iterator(); var10.hasNext(); asyncResult = asyncResult.transform(new SentinelReactorTransformer(new EntryConfig(apiName, 3, EntryType.IN, 1, params)))) {
apiName = (String)var10.next();
params = this.paramParser.parseParameterFor(apiName, exchange, (r) -> {
return r.getResourceMode() == 1;
});
}
return asyncResult;
}
private String contextName(String route) {
return "sentinel_gateway_context$$route$$" + route;
}
Set pickMatchingApiDefinitions(ServerWebExchange exchange) {
return (Set)GatewayApiMatcherManager.getApiMatcherMap().values().stream().filter((m) -> {
return m.test(exchange);
}).map(AbstractApiMatcher::getApiName).collect(Collectors.toSet());
}
public int getOrder() {
return this.order;
}
}
GatewayFlowSlot:添加到slotChain中,根据规则执行网关限流操作
@SpiOrder(-4000)
public class GatewayFlowSlot extends AbstractLinkedProcessorSlot {
public GatewayFlowSlot() {
}
public void entry(Context context, ResourceWrapper resource, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
this.checkGatewayParamFlow(resource, count, args); //检验限流规则
this.fireEntry(context, resource, node, count, prioritized, args); //触发限流操作
}
private void checkGatewayParamFlow(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
if (args != null) {
List rules = GatewayRuleManager.getConvertedParamRules(resourceWrapper.getName());
//获取转换后的热点限流规则
if (rules != null && !rules.isEmpty()) {
Iterator var5 = rules.iterator();
ParamFlowRule rule;
do {
if (!var5.hasNext()) {
return;
}
rule = (ParamFlowRule)var5.next();
ParameterMetricStorage.initParamMetricsFor(resourceWrapper, rule);
} while(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);
}
}
}
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
this.fireExit(context, resourceWrapper, count, args);
}
}
spring cloud gateway可处理route、api维度的请求数据:
route维度数据:spring配置文件定义的路由分组,资源名为routeId
api维度数据:用户自定义api分组
相关依赖
com.alibaba.cloud
spring-cloud-alibaba-sentinel-gateway
注入bean实例
@Configuration
public class GatewayConfiguration {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
} //注入SentinelGatewayBlockExceptionHandler实例,处理限流异常(block exception)
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
} //注入SentinelGatewayFilter实例,处理网关请求
}
application.yml:定义route维度数据
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
enabled: true
discovery:
locator:
lower-case-service-id: true
routes:
# Add your routes here.
- id: product_route
uri: lb://product
predicates:
- Path=/product/**
- id: httpbin_route
uri: https://httpbin.org
predicates:
- Path=/httpbin/**
filters:
- RewritePath=/httpbin/(?.*), /$\{segment}
自定义api分组
private void initCustomizedApis() {
Set definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("some_customized_api")
.setPredicateItems(new HashSet() {{
add(new ApiPathPredicateItem().setPattern("/product/baz"));
add(new ApiPathPredicateItem().setPattern("/product/foo/**")
.setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("another_customized_api")
.setPredicateItems(new HashSet() {{
add(new ApiPathPredicateItem().setPattern("/ahas"));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
sentinel处理说明
# sentinel 限流资源
route ID:product_route、httpbin_route
API name:some_customized_api、another_customized_api
说明:Spring Cloud Gateway可能会在route前面拼一个前缀,
如ReactiveCompositeDiscoveryClient_xxx
# URL为http://localhost:8090/product/foo/22
统计到product_route、some_customized_api资源上面
# url:http://localhost:8090/httpbin/json
统计到httpbin_route资源上面
GatewayCallbackManager:注册回调,自定义限流处理
public final class GatewayCallbackManager {
private static final Function DEFAULT_ORIGIN_PARSER = (w) -> {
return "";
};
private static volatile BlockRequestHandler blockHandler = new DefaultBlockRequestHandler();
private static volatile Function requestOriginParser;
public static BlockRequestHandler getBlockHandler() {
return blockHandler;
}
public static void resetBlockHandler() {
blockHandler = new DefaultBlockRequestHandler();
}
public static void setBlockHandler(BlockRequestHandler blockHandler) {
AssertUtil.notNull(blockHandler, "blockHandler cannot be null");
GatewayCallbackManager.blockHandler = blockHandler;
} //网关请求被限流后,自定义的处理操作,默认实现为DefaultBlockRequestHandler,抛出异常信息
public static Function getRequestOriginParser() {
return requestOriginParser;
}
public static void resetRequestOriginParser() {
requestOriginParser = DEFAULT_ORIGIN_PARSER;
}
public static void setRequestOriginParser(Function requestOriginParser) {
AssertUtil.notNull(requestOriginParser, "requestOriginParser cannot be null");
GatewayCallbackManager.requestOriginParser = requestOriginParser;
}
private GatewayCallbackManager() {
}
static {
requestOriginParser = DEFAULT_ORIGIN_PARSER;
}
}
注意事项
# 网关流控粒度
sentinel网关流控粒度是 route、API维度,默认不支持 URL 粒度
若通过Spring Cloud Alibaba接入,请将spring.cloud.sentinel.filter.enabled配置项置为 false(若在网关流控控制台上看到了 URL 资源,就是此配置项没有置为 false)
# 网关规则持久化(数据源模块)
若使用Spring Cloud Alibaba Sentinel数据源模块,需要注意网关流控规则数据源类型是gw-flow,
若将网关流控规则数据源指定为flow则不生效
SentinelGatewayConstants
public final class SentinelGatewayConstants {
public static final int APP_TYPE_GATEWAY = 1;
public static final int RESOURCE_MODE_ROUTE_ID = 0;
public static final int RESOURCE_MODE_CUSTOM_API_NAME = 1;
public static final int PARAM_PARSE_STRATEGY_CLIENT_IP = 0;
public static final int PARAM_PARSE_STRATEGY_HOST = 1;
public static final int PARAM_PARSE_STRATEGY_HEADER = 2;
public static final int PARAM_PARSE_STRATEGY_URL_PARAM = 3;
public static final int PARAM_PARSE_STRATEGY_COOKIE = 4;
public static final int URL_MATCH_STRATEGY_EXACT = 0;
public static final int URL_MATCH_STRATEGY_PREFIX = 1;
public static final int URL_MATCH_STRATEGY_REGEX = 2;
public static final int PARAM_MATCH_STRATEGY_EXACT = 0;
public static final int PARAM_MATCH_STRATEGY_PREFIX = 1;
public static final int PARAM_MATCH_STRATEGY_REGEX = 2;
public static final int PARAM_MATCH_STRATEGY_CONTAINS = 3;
public static final String GATEWAY_CONTEXT_DEFAULT = "sentinel_gateway_context_default";
public static final String GATEWAY_CONTEXT_PREFIX = "sentinel_gateway_context$$";
public static final String GATEWAY_CONTEXT_ROUTE_PREFIX = "sentinel_gateway_context$$route$$";
public static final String GATEWAY_NOT_MATCH_PARAM = "$NM";
public static final String GATEWAY_DEFAULT_PARAM = "$D";
private SentinelGatewayConstants() {
}
}
SentinelGatewayProperties
@ConfigurationProperties(
prefix = "spring.cloud.sentinel.scg"
)
public class SentinelGatewayProperties {
@NestedConfigurationProperty
private FallbackProperties fallback;
private Integer order = -2147483648;
public SentinelGatewayProperties() {
}
public FallbackProperties getFallback() {
return this.fallback;
}
public SentinelGatewayProperties setFallback(FallbackProperties fallback) {
this.fallback = fallback;
return this;
}
public Integer getOrder() {
return this.order;
}
public void setOrder(Integer order) {
this.order = order;
}
}
FallbackProperties
public class FallbackProperties {
private String mode;
private String redirect;
private String responseBody;
private Integer responseStatus;
private String contentType;
public FallbackProperties() {
this.responseStatus = HttpStatus.TOO_MANY_REQUESTS.value();
this.contentType = MediaType.APPLICATION_JSON.toString();
}
GatewayApiDefinitionManager
public final class GatewayApiDefinitionManager {
private static final Map API_MAP = new ConcurrentHashMap();
private static final GatewayApiDefinitionManager.ApiDefinitionPropertyListener LISTENER = new GatewayApiDefinitionManager.ApiDefinitionPropertyListener();
private static SentinelProperty> currentProperty = new DynamicSentinelProperty();
private static final Map API_CHANGE_OBSERVERS = new ConcurrentHashMap();
public GatewayApiDefinitionManager() {
}
private static void initializeApiChangeObserverSpi() {
List listeners = SpiLoader.loadInstanceList(ApiDefinitionChangeObserver.class);
Iterator var1 = listeners.iterator();
while(var1.hasNext()) {
ApiDefinitionChangeObserver e = (ApiDefinitionChangeObserver)var1.next();
API_CHANGE_OBSERVERS.put(e.getClass().getCanonicalName(), e);
RecordLog.info("[GatewayApiDefinitionManager] ApiDefinitionChangeObserver added: " + e.getClass().getCanonicalName(), new Object[0]);
}
}
public static void register2Property(SentinelProperty> property) {
AssertUtil.notNull(property, "property cannot be null");
synchronized(LISTENER) {
RecordLog.info("[GatewayApiDefinitionManager] Registering new property to gateway API definition manager", new Object[0]);
currentProperty.removeListener(LISTENER);
property.addListener(LISTENER);
currentProperty = property;
}
}
public static boolean loadApiDefinitions(Set apiDefinitions) {
//加载api资源
return currentProperty.updateValue(apiDefinitions);
}
public static ApiDefinition getApiDefinition(String apiName) {
return apiName == null ? null : (ApiDefinition)API_MAP.get(apiName);
}
public static Set getApiDefinitions() {
return new HashSet(API_MAP.values());
}
private static void notifyDownstreamListeners(Set definitions) {
try {
Iterator var1 = API_CHANGE_OBSERVERS.entrySet().iterator();
while(var1.hasNext()) {
Entry, ApiDefinitionChangeObserver> entry = (Entry)var1.next();
((ApiDefinitionChangeObserver)entry.getValue()).onChange(definitions);
}
} catch (Exception var3) {
RecordLog.warn("[GatewayApiDefinitionManager] WARN: failed to notify downstream api listeners", var3);
}
}
public static boolean isValidApi(ApiDefinition apiDefinition) {
return apiDefinition != null && StringUtil.isNotBlank(apiDefinition.getApiName()) && apiDefinition.getPredicateItems() != null;
}
static void addApiChangeListener(ApiDefinitionChangeObserver listener) {
AssertUtil.notNull(listener, "listener cannot be null");
API_CHANGE_OBSERVERS.put(listener.getClass().getCanonicalName(), listener);
}
static void removeApiChangeListener(Class> clazz) {
AssertUtil.notNull(clazz, "class cannot be null");
API_CHANGE_OBSERVERS.remove(clazz.getCanonicalName());
}
static {
try {
currentProperty.addListener(LISTENER);
initializeApiChangeObserverSpi();
} catch (Throwable var1) {
RecordLog.warn("[GatewayApiDefinitionManager] Failed to initialize", var1);
var1.printStackTrace();
}
}
***********
内部类:ApiDefinitionPropertyListener
private static final class ApiDefinitionPropertyListener implements PropertyListener> {
private ApiDefinitionPropertyListener() {
}
public void configUpdate(Set set) {
applyApiUpdateInternal(set);
RecordLog.info("[GatewayApiDefinitionManager] Api definition updated: " + GatewayApiDefinitionManager.API_MAP, new Object[0]);
}
public void configLoad(Set set) {
applyApiUpdateInternal(set);
RecordLog.info("[GatewayApiDefinitionManager] Api definition loaded: " + GatewayApiDefinitionManager.API_MAP, new Object[0]);
}
private static synchronized void applyApiUpdateInternal(Set set) {
ApiDefinition
public class ApiDefinition {
private String apiName;
private Set predicateItems;
public ApiDefinition() {
}
public ApiDefinition(String apiName) {
this.apiName = apiName;
}
public ApiDefinition setApiName(String apiName) {
public ApiDefinition setPredicateItems(Set predicateItems) {
public String getApiName() {
public Set getPredicateItems() {
public boolean equals(Object o) {
public int hashCode() {
public String toString() {
ApiPredicateItem
public interface ApiPredicateItem {
}
ApiPathPredicateItem
public class ApiPathPredicateItem implements ApiPredicateItem {
private String pattern;
private int matchStrategy = 0;
public ApiPathPredicateItem() {
}
public ApiPathPredicateItem setPattern(String pattern) {
public ApiPathPredicateItem setMatchStrategy(int matchStrategy) {
public String getPattern() {
public int getMatchStrategy() {
public boolean equals(Object o) {
public int hashCode() {
public String toString() {
ApiPredicateGroupItem
public class ApiPredicateGroupItem implements ApiPredicateItem {
private final Set items = new HashSet();
public ApiPredicateGroupItem() {
}
public ApiPredicateGroupItem addItem(ApiPredicateItem item) {
AssertUtil.notNull(item, "item cannot be null");
this.items.add(item);
return this;
}
public Set getItems() {
return this.items;
}
}
************
网关应用
application.yml
spring:
application:
name: nacos-gateway
cloud:
gateway:
routes:
- id: hello
uri: http://localhost:8081
predicates:
- Path=/service/hello
- id: hello2
uri: http://localhost:8081
predicates:
- Path=/service/hello2
- id: test
uri: http://localhost:8081
predicates:
- Path=/service/test
- id: test2
uri: http://localhost:8081
predicates:
- Path=/service/test2
CustomApiDefinitionConfig
public class CustomApiDefinitionConfig implements InitFunc {
@Override
public void init() throws Exception {
Set definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("hello-service")
.setPredicateItems(new HashSet() {{
add(new ApiPathPredicateItem().setPattern("/hello"));
add(new ApiPathPredicateItem().setPattern("/hello2/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api1);
ApiDefinition api2 = new ApiDefinition("test-service")
.setPredicateItems(new HashSet() {{
add(new ApiPathPredicateItem().setPattern("/test"));
add(new ApiPathPredicateItem().setPattern("/test2/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}
CustomGatewayFlowRule
public class CustomGatewayFlowRule implements InitFunc {
@Override
public void init() throws Exception {
Set rules = new HashSet<>();
rules.add(new GatewayFlowRule("hello")
.setCount(1).setIntervalSec(1));
rules.add(new GatewayFlowRule("hello2")
.setCount(1).setIntervalSec(1).setBurst(1)
.setParamItem(new GatewayParamFlowItem()
.setFieldName("name")
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM)
.setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_EXACT)
));
rules.add(new GatewayFlowRule("test")
.setCount(1).setIntervalSec(1));
rules.add(new GatewayFlowRule("test2")
.setCount(1).setIntervalSec(1)
.setParamItem(new GatewayParamFlowItem()
.setFieldName("age")
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM)
.setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_EXACT)
));
rules.add(new GatewayFlowRule("hello-service")
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
.setCount(1).setIntervalSec(1)
.setParamItem(new GatewayParamFlowItem()
.setFieldName("name")
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM)
.setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_EXACT)
));
rules.add(new GatewayFlowRule("test-service")
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
.setCount(1).setIntervalSec(1)
.setParamItem(new GatewayParamFlowItem()
.setFieldName("age")
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM)
.setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_EXACT)
));
GatewayRuleManager.loadRules(rules);
}
}
************
后端服务
application.properties
# 应用名称
spring.application.name=demo
# 应用服务 WEB 访问端口
server.port=8081
HelloController
@RestController
@RequestMapping("/service")
public class HelloController {
@RequestMapping("/hello")
public String hello(){
System.out.println("hello");
return "hello";
}
@RequestMapping("/hello2")
public String hello2(String name){
System.out.println("hello2 "+name);
return "hello2 "+name;
}
@RequestMapping("/test")
public String test(){
return "test";
}
@RequestMapping("/test2")
public String test2(Integer age){
return "test2 "+age;
}
}
************
本地限流配置
META-INF/services/com.alibaba.csp.sentinel.initFunc
com.example.demo.config.CustomGatewayFlowRule
com.example.demo.config.CustomApiDefinitionConfig
jmeter 测试
localhost:8080/service/hello
localhost:8080/service/hello2?name=gtlx
************
控制台限流配置
请求链路
api管理
流控规则
jmeter测试
localhost:8080/service/test
localhost:8080/service/test2?age=20