前言

在使用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。因此,更推荐使用第一种方法实现根据来源限流的功能

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐