Sentinel如何根据请求来源进行限流
在使用Sentinel做流控的时候,有时候我们会希望根据上级微服务或者请求来源进行限流,这时我们可以使用控制台中的“针对来源”进行限流,如图所示:当设置为default时,表示会对所有来源进行限流,可以根据自身的需求进行相应的配置。具体可以参考官网相关描述。然而,根据官网的描述编写代码后,我发现针对来源的限流并不生效。经过搜索和研究源码之后发现了两种方法,亲测在2.2.6.RELEASE版本上可以
前言
在使用Sentinel做流控的时候,有时候我们会希望根据上级微服务或者请求来源进行限流,这时我们可以使用控制台中的“针对来源”进行限流,如图所示:
当设置为default时,表示会对所有来源进行限流,可以根据自身的需求进行相应的配置。具体可以参考官网相关描述基于调用关系的流量控制。
然而,根据官网的描述编写代码后,我发现针对来源的限流并不生效。经过搜索和研究源码之后发现了两种方法,亲测在2.2.6.RELEASE版本上可以达到效果。
需要注意的是,链路限流并不适合此处的场景,因为链路限流的调用链是指在一个微服务内部,如controller调用service的场景,而本文主要讨论跨微服务之间的调用。
方法一:使用ContextUtil.enter()
这也是上面连接中提供的方法,也是我最早尝试的方法,但很遗憾并没有生效。原来,在AbstractSentinelInterceptor的preHandler方法中,已经使用过ContextUtil.enter()方法创建了context对象:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
try {
// 判断是否已经创建过context
String resourceName = getResourceName(request);
if (StringUtil.isEmpty(resourceName)) {
return true;
}
if (increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
return true;
}
// 此处解析出资源名和origin,并调用ContextUtil.enter()方法进行设置
String origin = parseOrigin(request);
String contextName = getContextName(request);
ContextUtil.enter(contextName, origin);
Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry);
return true;
} catch (BlockException e) {
try {
handleBlockException(request, response, e);
} finally {
ContextUtil.exit();
}
return false;
}
}
ContextUtil.enter会判断当前线程是否创建过context对象,如果没有才会新建,并存储在ThreadLocal中。因此,后续再调用该方法并不能修改或新建context对象。其代码如下:
protected static Context trueEnter(String name, String origin) {
// contextHolder是一个ThreadLocal,用来存储当前线程的context对象
Context context = contextHolder.get();
// 只有获取不到时才会进行创建,否则直接返回从ThreadLocal中获取到的context
if (context == null) {
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// Add entrance node.
Constants.ROOT.addChild(node);
Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
context = new Context(node, name);
context.setOrigin(origin);
contextHolder.set(context);
}
return context;
}
那么,为什么AbstractSentinelInterceptor的preHandler方法中没有解析到origin呢?因为我们需要自己提供一个实现RequestOriginParser接口的对象,该接口只有一个parseOrigin方法,用来从请求中解析来源。我们提供了该接口的实现类并交给spring管理后,AbstractSentinelInterceptor就可以从请求中解析出origin并设置到context中。例如,我们希望根据请求头中的S-user(一个自定义的属性)进行限流,进行如下配置就可以解析出origin,然后在控制台上进行配置即可:
@Configuration
public class OriginParserConfig {
@Bean
public RequestOriginParser requestOriginParser() {
return new RequestOriginParser() {
@Override
public String parseOrigin(HttpServletRequest request) {
return request.getHeader("S-user");
}
};
}
}
方法二:使用Sentinel Web Filter
该方法在官方文档的FAQ中有提到,使用前需要额外引入一个新的依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-web-servlet</artifactId>
<version>x.y.z</version>
</dependency>
然后将该包为我们提供的CommonFilter注册进Web容器,并使用WebCallbackManager.setRequestOriginParser()方法来提供RequestOriginParser的实现类。实例代码如下:
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
registration.setFilter(new CommonFilter());
// Set the matching URL pattern for the filter.
registration.addUrlPatterns("/*");
registration.setName("sentinelCommonFilter");
registration.setOrder(1);
// Set whether to support the specified HTTP method prefix for the filter.
registration.addInitParameter(CommonFilter.HTTP_METHOD_SPECIFY, "false");
WebCallbackManager.setRequestOriginParser(request -> request.getHeader("S-user"));
return registration;
}
}
CommonFilter中也有类似前一个方法中的代码:
// Parse the request origin using registered origin parser.
String origin = parseOrigin(sRequest);
String contextName = webContextUnify ? WebServletConfig.WEB_SERVLET_CONTEXT_NAME : target;
ContextUtil.enter(contextName, origin);
但是,使用该方法有一个问题:例如想要每秒通过的QPS为1,那么在控制台中需要将QPS设置为2。因此,更推荐使用第一种方法实现根据来源限流的功能
更多推荐
所有评论(0)