欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

程序员文章站 2022-07-28 19:22:24
Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战 在阅读本文前,建议先阅读 "《Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵基础实战》" 。 1. Sentinel整合Feign和RestTemplate Sen ......

spring cloud alibaba | sentinel:分布式系统的流量防卫兵进阶实战

在阅读本文前,建议先阅读《spring cloud alibaba | sentinel:分布式系统的流量防卫兵基础实战》

1. sentinel整合feign和resttemplate

sentinel目前已经同时支持feign和resttemplate,需要我们引入对应的依赖,在使用feign的时候需要在配置文件中打开sentinel对feign的支持:feign.sentinel.enabled=true,同时需要加入openfeign starter依赖使sentinel starter中的自动化配置类生效。在使用resttemplate的时候需要在构造resttemplate的bean的时候加上@sentinelresttemplate注解,开启sentinel对resttemplate的支持。

1.1 创建父工程sentinel-springcloud-high:

父工程pom.xml如下:

代码清单:alibaba/sentinel-springcloud-high/pom.xml
***

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-actuator</artifactid>
</dependency>
<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-web</artifactid>
</dependency>
<dependency>
    <groupid>com.alibaba.cloud</groupid>
    <artifactid>spring-cloud-starter-alibaba-sentinel</artifactid>
</dependency>
<dependency>
    <groupid>com.alibaba.cloud</groupid>
    <artifactid>spring-cloud-starter-alibaba-nacos-discovery</artifactid>
</dependency>

公共组件中引入sentinel做流量控制,引入nacos做服务中心。

1.2 创建子工程provider_server:

配置文件application.yml如下:

代码清单:alibaba/sentinel-springcloud-high/provider_server/pom.xml
***

server:
  port: 8000
spring:
  application:
    name: spring-cloud-provider-server
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.44.129:8848
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8720
management:
  endpoints:
    web:
      cors:
        allowed-methods: '*'

接口测试类hellocontroller.java如下:

代码清单:alibaba/sentinel-springcloud-high/provider_server/src/main/java/com/springcloud/provider_server/controller/hellocontroller.java
***

@restcontroller
public class hellocontroller {
    @getmapping("/hello")
    public string hello(httpservletrequest request) {
        return "hello, port is: " + request.getlocalport();
    }
}

1.3 创建子工程consumer_server:

子工程依赖pom.xml如下:

代码清单:alibaba/sentinel-springcloud-high/consumer_server/pom.xml
***

<dependency>
    <groupid>org.springframework.cloud</groupid>
    <artifactid>spring-cloud-starter-openfeign</artifactid>
</dependency>

配置文件application.yml如下:

代码清单:alibaba/sentinel-springcloud-high/consumer_server/src/main/resources/application.yml
***

server:
  port: 9000
spring:
  application:
    name: spring-cloud-consumer-server
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.44.129:8848
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
management:
  endpoints:
    web:
      cors:
        allowed-methods: '*'
feign:
  sentinel:
    enabled: true

这里使用feign.sentinel.enabled=true开启sentinel对feign的支持。

接口测试类hellocontroller.java

代码清单:alibaba/sentinel-springcloud-high/consumer_server/src/main/java/com/springcloud/consumer_server/controller/hellocontroller.java
***

@restcontroller
public class hellocontroller {
    @autowired
    helloremote helloremote;

    @autowired
    resttemplate resttemplate;

    @getmapping("/hellobyfeign")
    public string hellobyfeign() {
        return helloremote.hello();
    }

    @getmapping("/hellobyresttemplate")
    public string hellobyresttemplate() {
        return resttemplate.getforobject("http://spring-cloud-provider-server/hello/", string.class);
    }
}

sentinel已经对做了整合,我们使用feign的地方无需额外的注解。同时,@feignclient注解中的所有属性,sentinel都做了兼容。

启动主类ch122consumerserverapplication.java如下:

代码清单:alibaba/sentinel-springcloud-high/consumer_server/src/main/java/com/springcloud/consumer_server/consumerserverapplication.java
***

@springbootapplication
@enablediscoveryclient
@enablefeignclients
public class ch122consumerserverapplication {

    public static void main(string[] args) {
        springapplication.run(ch122consumerserverapplication.class, args);
    }

    @bean
    @loadbalanced
    @sentinelresttemplate
    public resttemplate resttemplate() {
        return new resttemplate();
    }

}

在使用resttemplate的时候需要增加@sentinelresttemplate来开启sentinel对resttemplate的支持。

1.4 测试

启动工程provider_server和consumer_server,provider_server修改启动配置,启动两个实例,打开浏览器访问:http://localhost:9000/hellobyfeign 和 http://localhost:9000/hellobyresttemplate ,刷新几次,可以看到页面交替显示hello, port is: 8000hello, port is: 8001,说明目前负载均衡正常,现在查看sentinel控制台,如图:

Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

1.5 流量控制测试

这时选择左侧的簇点流控,点击流控,如图:
Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

这里我们配置一个最简单的规则,配置qps限制为1,点击新增,如图:

Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

这里解释一下什么是qps,简单来说qps是一个每秒访问数,这里我们测试时需要重复快速刷新http://localhost:9000/hellobyfeign 和 http://localhost:9000/hellobyresttemplate ,在刷新的过程中,我们可以看到页面会显示错误信息,如:blocked by sentinel (flow limiting),说明我们配置sentinel已经限流成功,这时我们再看一下sentinel的控制台,可以看到我们刚才访问的成功和限流的数量,如图:

Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

2. 服务降级

在上一小结,我们介绍了feign和resttemplate整合sentinel使用,并且在sentinel控制台上做了qps限流,并且限流成功,限流成功后,默认情况下,sentinel对控制资源的限流处理是直接抛出异常。在没有合理的业务承接或者前端对接情况下可以这样,但是正常情况为了更好的用户业务,都会实现一些被限流之后的特殊处理,我们不希望展示一个生硬的报错。这一小节,我们介绍一下服务降级处理。

2.1 创建子工程consumer_fallback

feign服务降级类helloremotefallback.java如下:

代码清单:alibaba/sentinel-springcloud-high/consumer_fallback/src/main/java/com/springcloud/consumer_fallback/fallback/helloremotefallback.java
***

@component
public class helloremotefallback implements helloremote {
    @override
    public string hello() {
        return "feign fallback msg";
    }
}

相对应的,这里需要在helloremote.java上做一部分配置,使得限流后,触发服务降级执行我们的服务降级类,代码如下:

代码清单:ch12_2/ch12_2_consumer_fallback/src/main/java/com/springcloud/book/ch12_2_consumer_fallback/remote/helloremote.java
***

@feignclient(name = "spring-cloud-provider-server", fallback = helloremotefallback.class)
public interface helloremote {
    @getmapping("/hello")
    string hello();
}

fallback = helloremotefallback.class指定服务降级的处理类为helloremotefallback.class

resttemplate服务降级工具类exceptionutil.java如下:

代码清单:alibaba/sentinel-springcloud-high/consumer_fallback/src/main/java/com/springcloud/consumer_fallback/remote/helloremote.java
***

public class exceptionutil {

    private final static logger logger = loggerfactory.getlogger(exceptionutil.class);

    public static sentinelclienthttpresponse handleexception(httprequest request, byte[] body, clienthttprequestexecution execution, blockexception ex) {
        logger.error(ex.getmessage(), ex);
        return new sentinelclienthttpresponse("resttemplate fallback msg");
    }
}

这里同样需要修改resttemplate注册成为bean的地方,使得resttemplate触发服务降级以后代码执行我们为它写的处理类,ch122consumerfallbackapplication.java代码如下:

代码清单:alibaba/sentinel-springcloud-high/consumer_fallback/src/main/java/com/springcloud/consumer_fallback/consumerfallbackapplication.java
***

@bean
@loadbalanced
@sentinelresttemplate(blockhandler = "handleexception", blockhandlerclass = exceptionutil.class)
public resttemplate resttemplate() {
    return new resttemplate();
}

这里需要注意,@sentinelresttemplate注解的属性支持限流(blockhandler, blockhandlerclass)和降级(fallback, fallbackclass)的处理。

其中blockhandlerfallback属性对应的方法必须是对应blockhandlerclassfallbackclass属性中的静态方法。

@sentinelresttemplate注解的限流(blockhandler, blockhandlerclass)和降级(fallback, fallbackclass)属性不强制填写。

当使用resttemplate调用被sentinel熔断后,会返回resttemplate request block by sentinel信息,或者也可以编写对应的方法自行处理返回信息。这里提供了 sentinelclienthttpresponse用于构造返回信息。

2.2 测试

顺次启动provider_server和consumer_fallback两个子工程。先在浏览器中交替访问http://localhost:9090/hellobyfeign 和 http://localhost:9090/hellobyresttemplate ,而后打开sentinel控制台,在这两个接口上增加限流信息,注意,这里要将限流信息加在资源上,具体如图:

Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

在浏览器中刷新两个链接,两个限流信息都可以正常浏览器中显示,测试成功,再次查看sentinel控制台,也可以看到被拒接的流量统计,如图:

Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

3. sentinel整合服务网关限流

sentinel目前支持spring cloud gateway、zuul 等主流的 api gateway 进行限流。看一下官方的结构图,如图:

Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

从这张官方的图中,可以看到,sentinel对zuul的限流主要是通过3个filter来完成的,对spring cloud gateway则是通过一个sentinlegatewayfilter和一个blockrequesthandler来完成的。

sentinel 1.6.0 引入了 sentinel api gateway adapter common 模块,此模块中包含网关限流的规则和自定义 api 的实体和管理逻辑:

  • gatewayflowrule:网关限流规则,针对 api gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 api 分组进行限流,支持针对请求中的参数、header、来源 ip 等进行定制化的限流。
  • apidefinition:用户自定义的 api 定义分组,可以看做是一些 url 匹配的组合。比如我们可以定义一个 api 叫 my_api,请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 api 分组下面。限流的时候可以针对这个自定义的 api 分组维度进行限流。

3.1 zuul 1.x

sentinel 提供了 zuul 1.x 的适配模块,可以为 zuul gateway 提供两种资源维度的限流:

  • route 维度:即在 spring 配置文件中配置的路由条目,资源名为对应的 route id(对应 requestcontext 中的 proxy 字段)
  • 自定义 api 维度:用户可以利用 sentinel 提供的 api 来自定义一些 api 分组

3.1.1 创建子工程zuul_server

工程依赖pom.xml如下:

代码清单:alibaba/sentinel-springcloud-high/zuul_server/pom.xml
***

<dependency>
    <groupid>org.springframework.cloud</groupid>
    <artifactid>spring-cloud-starter-netflix-zuul</artifactid>
</dependency>
<dependency>
    <groupid>com.alibaba.csp</groupid>
    <artifactid>sentinel-zuul-adapter</artifactid>
</dependency>

这里因为sentinel-zuul-adapter未包含在spring-cloud-starter-alibaba-sentinel,需要手动单独引入。

3.1.2 配置文件application.yml如下:

代码清单:alibaba/sentinel-springcloud-high/zuul_server/src/main/resources/application.yml
***

server:
  port: 18080
spring:
  application:
    name: spring-cloud-zuul-server
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.44.129:8848
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8720
zuul:
  routes:
    consumer-route:
      path: /consumer/**
      serviceid: spring-cloud-consumer-fallback

3.1.3 定义降级处理类zuulfallbackprovider.java如下:

代码清单:alibaba/sentinel-springcloud-high/zuul_server/src/main/java/com/springcloud/zuul_server/fallback/zuulfallbackprovider.java
***

public class zuulfallbackprovider implements zuulblockfallbackprovider {
    @override
    public string getroute() {
        return "*";
    }

    @override
    public blockresponse fallbackresponse(string route, throwable cause) {
        recordlog.info(string.format("[sentinel defaultblockfallbackprovider] run fallback route: %s", route));
        if (cause instanceof blockexception) {
            return new blockresponse(429, "sentinel block exception", route);
        } else {
            return new blockresponse(500, "system error", route);
        }
    }
}

3.1.4 同时,我们需要将3个sentinel的filter注入spring,配置类如下:

代码清单:alibaba/sentinel-springcloud-high/zuul_server/src/main/java/com/springcloud/zuul_server/config/zuulconfig.java
***

@configuration
public class zuulconfig {
    @bean
    public zuulfilter sentinelzuulprefilter() {
        // we can also provider the filter order in the constructor.
        return new sentinelzuulprefilter();
    }

    @bean
    public zuulfilter sentinelzuulpostfilter() {
        return new sentinelzuulpostfilter();
    }

    @bean
    public zuulfilter sentinelzuulerrorfilter() {
        return new sentinelzuulerrorfilter();
    }

    /**
     * 注册 zuulfallbackprovider
     */
    @postconstruct
    public void doinit() {
        zuulblockfallbackmanager.registerprovider(new zuulfallbackprovider());
    }

}

最终,启动前需要配置jvm启动参数,增加-dcsp.sentinel.app.type=1,来告诉sentinel控制台我们启动的服务是为 api gateway 类型。

3.1.5 测试

顺次启动子工程provider_server、consumer_fallback、zuul_server,打开浏览器访问:http://localhost:18080/consumer/hellobyfeign ,然后我们打开sentinel控制台,查看zuul_server服务,如图:

Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

我们定制限流策略,依旧是qps为1,我们再次刷新http://localhost:18080/consumer/hellobyfeign 页面,这时,页面上已经可以正产限流了,限流后显示的内容为:

{"code":429, "message":"sentinel block exception", "route":"consumer-route"}

这里注意,定义限流的是资源,千万不要定义错地方,限流定义如图:

Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

3.2 spring cloud gateway

从 1.6.0 版本开始,sentinel 提供了 spring cloud gateway 的适配模块,可以提供两种资源维度的限流:

  • route 维度:即在 spring 配置文件中配置的路由条目,资源名为对应的 routeid
  • 自定义 api 维度:用户可以利用 sentinel 提供的 api 来自定义一些 api 分组

3.2.1 创建子工程gateway_server

工程依赖pom.xml如下:

代码清单:alibaba/sentinel-springcloud-high/gateway_server/pom.xml
***

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-webflux</artifactid>
</dependency>
<dependency>
    <groupid>org.springframework.cloud</groupid>
    <artifactid>spring-cloud-starter-gateway</artifactid>
</dependency>
<dependency>
    <groupid>com.alibaba.csp</groupid>
    <artifactid>sentinel-spring-cloud-gateway-adapter</artifactid>
</dependency>

3.2.2 配置文件application.yml如下:

代码清单:alibaba/sentinel-springcloud-high/gateway_server/src/main/resources/application.yml
***

server:
  port: 28080
spring:
  application:
    name: spring-cloud-gateway-server
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.44.129:8848
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8720
    gateway:
      enabled: true
      discovery:
        locator:
          lower-case-service-id: true
      routes:
        - id: consumer_server
          uri: lb://spring-cloud-consumer-fallback
          predicates:
            - method=get

3.2.3 全局配置类gatewayconfig.java如下:

同上一小节介绍的zuul,这里我们同样需要将两个sentinel有关spring cloud gateway的filter注入spring:sentinelgatewayfiltersentinelgatewayblockexceptionhandler,这里因为在sentinel v1.6.0版本才加入spring cloud gateway的支持,很多地方还不是很完善,异常处理sentinelgatewayblockexceptionhandler目前只能返回一个异常信息,在我们的系统中无法和上下游很好的结合,这里笔者自己重新实现了sentinelgatewayblockexceptionhandler,并命名为jsonsentinelgatewayblockexceptionhandler,返回参数定义成为json,这里不再注入sentinel提供的sentinelgatewayblockexceptionhandler,而是改为笔者自己实现的jsonsentinelgatewayblockexceptionhandler

代码清单:alibaba/sentinel-springcloud-high/gateway_server/src/main/java/com/springcloud/gateway_server/config/gatewayconfig.java
***

@configuration
public class gatewayconfig {
    private final list<viewresolver> viewresolvers;
    private final servercodecconfigurer servercodecconfigurer;

    public gatewayconfig(objectprovider<list<viewresolver>> viewresolversprovider, servercodecconfigurer servercodecconfigurer) {
        this.viewresolvers = viewresolversprovider.getifavailable(collections::emptylist);
        this.servercodecconfigurer = servercodecconfigurer;
    }

    @bean
    @order(ordered.highest_precedence)
    public jsonsentinelgatewayblockexceptionhandler jsonsentinelgatewayblockexceptionhandler() {
        // register the block exception handler for spring cloud gateway.
        return new jsonsentinelgatewayblockexceptionhandler(viewresolvers, servercodecconfigurer);
    }

    @bean
    @order(-1)
    public globalfilter sentinelgatewayfilter() {
        return new sentinelgatewayfilter();
    }
}

3.2.4 降级处理类jsonsentinelgatewayblockexceptionhandler.java如下:

代码清单:alibaba/sentinel-springcloud-high/gateway_server/src/main/java/com/springcloud/gateway_server/exception/jsonsentinelgatewayblockexceptionhandler.java
***

public class jsonsentinelgatewayblockexceptionhandler implements webexceptionhandler {
    private list<viewresolver> viewresolvers;
    private list<httpmessagewriter<?>> messagewriters;

    public jsonsentinelgatewayblockexceptionhandler(list<viewresolver> viewresolvers, servercodecconfigurer servercodecconfigurer) {
        this.viewresolvers = viewresolvers;
        this.messagewriters = servercodecconfigurer.getwriters();
    }

    private mono<void> writeresponse(serverresponse response, serverwebexchange exchange) {
        serverhttpresponse serverhttpresponse = exchange.getresponse();
        serverhttpresponse.getheaders().add("content-type", "application/json;charset=utf-8");
        byte[] datas = "{\"code\":403,\"msg\":\"sentinel block exception\"}".getbytes(standardcharsets.utf_8);
        databuffer buffer = serverhttpresponse.bufferfactory().wrap(datas);
        return serverhttpresponse.writewith(mono.just(buffer));
    }

    @override
    public mono<void> handle(serverwebexchange exchange, throwable ex) {
        if (exchange.getresponse().iscommitted()) {
            return mono.error(ex);
        }
        // this exception handler only handles rejection by sentinel.
        if (!blockexception.isblockexception(ex)) {
            return mono.error(ex);
        }
        return handleblockedrequest(exchange, ex)
                .flatmap(response -> writeresponse(response, exchange));
    }

    private mono<serverresponse> handleblockedrequest(serverwebexchange exchange, throwable throwable) {
        return gatewaycallbackmanager.getblockhandler().handlerequest(exchange, throwable);
    }

    private final supplier<serverresponse.context> contextsupplier = () -> new serverresponse.context() {
        @override
        public list<httpmessagewriter<?>> messagewriters() {
            return jsonsentinelgatewayblockexceptionhandler.this.messagewriters;
        }

        @override
        public list<viewresolver> viewresolvers() {
            return jsonsentinelgatewayblockexceptionhandler.this.viewresolvers;
        }
    };
}

笔者这里仅重写了writeresponse()方法,讲返回信息简单的更改成了json格式,各位读者有需要可以根据自己的需求进行修改。

3.2.5 测试

顺次启动provider_server、consumer_server和gateway_server,配置gateway_server jvm启动参数-dcsp.sentinel.app.type=1,如图:

Spring Cloud Alibaba | Sentinel:分布式系统的流量防卫兵进阶实战

打开浏览器访问:http://localhost:28080/hellobyfeign ,刷新几次,页面正常返回hello, port is: 8000,打开sentinel控制台,配置限流策略,qps限制为1,再刷新浏览器页面,这时,我们可以看到浏览器返回限流信息:

{"code":403,"msg":"sentinel block exception"}

测试成功。