Sentinel全系列之三——Http限流和dub


1. 前言

  目前在企业中使用最多的莫不过是http接口和dubbo接口的限流,本篇文章将会向大家完整介绍一下http接口限流和dubbo接口限流的原理,以及会对他们的源码进行一个详细的分析,sentinel是如何对此进行了高效的的且不侵入代码的完美适配的。


2. sentinel-adapter

image.png

  如图我们可以看到,sentinel非常良心地向我们提供了大量的适配器,那么他们的作用是什么呢?

2.1 适配器的作用

  看过前面文章的小伙伴肯定都知道,我们调用sentinel来实现限流的关键入口其实是“SphU.entry()”,这个大家应该都了解,限流的核心其实就是调用SphU的核心方法,来判定本次请求是否能够通过这段链路,如下图所示:

image.png

  我们在有的时候也会使用注解的方式进行指定接口的限流,就像下面这种形式

@SentinelResource(value = "资源名", blockHandler = "限流回调方法名")

  但是这些方式是对代码有侵入性的,因此sentinel提供了适配器,适配器能够让用户方在无感知的情况下,只要引入对应的jar包,就能对http或者dubbo接口进行限流,是不是很香。


3. http接口限流

3.1 http限流使用过程

  1. 首先使用http限流,项目需要引入sentinel的sentinel-web-servlet的jar包。同时,因为我们演示时接入了dashboard,所以我还需要引入sentinel-transport-simple-http的jar包,依赖如下
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-web-servlet</artifactId>
    <version>${sentinel.version}</version>
</dependency>

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-apache-dubbo-adapter</artifactId>
    <version>${sentinel.version}</version>
</dependency>

复制代码

  当然,如果是spring boot的项目可以直接引入spring-cloud-starter-alibaba-sentinel。版本可自选

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>0.2.0.RELEASE</version>
</dependency>
复制代码
  1. 编写接口,启动dashboard设置规则,注意资源名必须为http接口的路径名(原因会在后续源码分析时说明),点击新增后,这个规则就会生效了,是不是相当轻松。(dashboard的接入在前面文章已经有所介绍,可以自行查看)

image.png

image.png

3.2 http限流、降级逻辑注册

  接下来我们就是编写限流回调逻辑了,适配器为我们提供了一套默认的限流回调,如下图所示。

image.png

  当然我们也可以编写一套自己的逻辑,建议在spring boot 启动的main方法里写上(其实哪里写都无所谓,这是个静态的回调方法,只要启动后能加载到就可以),代码如下:

WebCallbackManager.setUrlBlockHandler((request, response, ex) ->{
    System.out.println("限流处理的业务逻辑");
});
复制代码

3.3 Http限流源码解析

3.3.1 项目结构

image.png

3.3.2 核心源码解析

  其实通过上方的项目结构,我们一下子就能发现,其实http适配器主要的核心思路,就是进行了一个过滤器的操作来进行请求的拦截,过滤。

  对于过滤器,此处不过多介绍,感兴趣的可以自行查阅资料。他的作用就是所有的http请求都会经过过滤器,因此我们可以在过滤器一端进行限流操作。

  过滤器会过滤到http请求,接着就是从请求中读出请求过来的路径或者方法的后缀,作为资源名传入“SphU.entry()”,进行对该资源的一个流量监控等,因此这就是为什么前面在创建规则需要输入正确的http接口路径名的原因,因为一旦如果资源名不匹配,就会造成因为读取不到对应规则,而造成限流失效的问题(此处sentinel核心限流源码部分省略,会在后续进行完整讲解)。关键源码如下:进行了http请求的拦截,并获取路径,设定调用链的入口。开启限流判定等。

image.png


4. dubbo接口限流

4.1 dubbo限流使用过程

  1. 使用dubbo限流前,同样道理项目需要引入sentinel的sentinel-apache-dubbo-adapter(dubbo高版本适配器,低版本的引用另一个)的jar包。
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>${sentinel.version}</version>
</dependency>

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-apache-dubbo-adapter</artifactId>
    <version>${sentinel.version}</version>
</dependency>

复制代码

  同样的如果是spring boot的项目可以直接引入spring-cloud-starter-alibaba-sentinel。版本可自选

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>0.2.0.RELEASE</version>
</dependency>
复制代码
  1. 和http一样,启动dashboard设置规则,这里的资源名,如果是对某个dubbo的服务接口限流,资源名必须设置为接口的完全限定名,如果是接口的某个方法,则设置为接口全限定名:方法签名,例如: buguniao.UserService:getUserById(java.lang.Long)点击新增后,这个规则就会生效了,这里就不放截图了,除了资源名需要注意外,其余和http基本相同,轻松完爆。

4.2 dubbo限流、降级逻辑注册

  接下来我们就是编写限流回调逻辑了,dubbo适配器没有默认的限流回调,如下图所示。所以我们需要编写一套自己的逻辑,同样需要在启动的时候能加载到就可以。

  • 有个问题,前面没提,sentinel只能注册全局的回调的逻辑,因此需要在回调方法里区分 限流逻辑、降级逻辑
  • 由于sentinel dubbo 限流熔断是基于dubbo filter实现的,因此在最终的返回结果需要返回一个 RpcResult,请参考以下模板,修改限流逻辑部分、降级逻辑部分

代码如下:

DubboFallbackRegistry.setConsumerFallback(new DubboFallback() {
       @Override
       public Result handle(Invoker<?> invoker,Invocation invocation,BlockException ex) {
                RpcResult rs = new RpcResult();
                String r = null;
                try{
                    if(ex instanceof FlowException){
                        r="限流逻辑"; //需要自定义,最终返回业务返回值
                    }else if(ex instanceof DegradeException){
                        r="降级逻辑"; //需要自定义,最终返回业务返回值
                    }

                }catch (Exception e){
                    r="其他错误";
                    rs.setException(e); //业务异常注入,以便在consumer方抛出异常
                }
                rs.setValue(r); //设置业务返回值

                return rs;
            }
        });
复制代码

4.3 dubbo限流源码解析

4.3.1 项目结构

image.png

4.3.2 核心源码解析

  dubbo接口的限流实现,其实是基于dubbo filter实现的,dubbo filter如何配置的我们在此处不过多介绍,我们只聊限流方面的。

  首先,该适配器会在dubbo消费者端进行一层拦截,主要是获取url中消费者的应用名,并添加到dubbo的上下文中,这样provider端才能对来源应用进行一个限流。代码如下:

@Activate(group = CONSUMER)
public class DubboAppContextFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String application = invoker.getUrl().getParameter(CommonConstants.APPLICATION_KEY);
        if (application != null) {
            RpcContext.getContext().setAttachment(DubboUtils.SENTINEL_DUBBO_APPLICATION_KEY, application);
        }
        return invoker.invoke(invocation);
    }
复制代码

  consummer端有一层EntryType类型设置为OUT的限流判定,此处暂时省略介绍,会在核心源码解析时具体介绍,部分源码如下:

try {
    interfaceEntry = SphU.entry(interfaceResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT);
    methodEntry = SphU.entry(methodResourceName, ResourceTypeConstants.COMMON_RPC, EntryType.OUT,
        invocation.getArguments());
    Result result = invoker.invoke(invocation);
    if (result.hasException()) {
        Tracer.traceEntry(result.getException(), interfaceEntry);
        Tracer.traceEntry(result.getException(), methodEntry);
    }
    return result;
} catch{
//...
}
复制代码

  provider端的过滤器的主要作用便是进行EntryType类型设置为IN的限流判定,会进行一个有效的流量判定,确定是否需要限流,前面讲到的dubbo的消费者应用名的作用是为了对指定来源应用进行限流使用的,dashboard的界面如下:

image.png

  源码如下:

@Activate(group = "provider")
public class SentinelDubboProviderFilter extends AbstractDubboFilter implements Filter {

    public SentinelDubboProviderFilter() {
        RecordLog.info("Sentinel Dubbo provider filter initialized");
    }

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // Get origin caller.
        //获取来源应用的名字,从上下文中
        String application = DubboUtils.getApplication(invocation, "");

        Entry interfaceEntry = null;
        Entry methodEntry = null;
        try {
            //获取资源名(接口名)
            String resourceName = getResourceName(invoker, invocation);
            //获取接口的指定方法名
            String interfaceName = invoker.getInterface().getName();
            //一个调用链的入口,参数为资源名和来源应用名
            ContextUtil.enter(resourceName, application);
            //接口限流判定开启
            interfaceEntry = SphU.entry(interfaceName, EntryType.IN);
            //方法限流判定开启
            methodEntry = SphU.entry(resourceName, EntryType.IN, 1, invocation.getArguments());
            //返回的RpcResult
            Result result = invoker.invoke(invocation);
            if (result.hasException()) {
                Tracer.trace(result.getException());
            }
            return result;
        } catch (BlockException e) {
            return DubboFallbackRegistry.getProviderFallback().handle(invoker, invocation, e);
        } catch (RpcException e) {
            Tracer.trace(e);
            throw e;
        } finally {
            if (methodEntry != null) {
                methodEntry.exit(1, invocation.getArguments());
            }
            if (interfaceEntry != null) {
                interfaceEntry.exit();
            }
            ContextUtil.exit();
        }
    }
}
复制代码

注:由于大部分业务场景下,服务提供端是需要限流的,则消费端和一般不会接入sentinel,但是这会是一个小bug,如果dubbo消费端没有接入sentinel的dubbo适配器,则无法拦截到dubboName丢进上下文中,因此,不管dashboard如何设置限流来源均会失效,解决方案是进行dubboName的透传,使用
RpcContext.getContext().setAttachment()方法在消费者端处往协请求头里加一个参数。


5.总结

  http和dubbo接口的限流,说白了就是拦截到请求,开始并进行限流判定,判断是否继续执行,因此理解起来不会太难,后续文章会详细介绍sentinel是怎么进行限流的监控和判定的!