微服务系列:服务网关 Spring Cloud Gateway 集成 Sentinel 限流

1,642 阅读5分钟

Sentinel 是阿里开源的一款面向分布式服务架构的流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统自适应保护等多个维度来保障微服务的稳定性。

除了Spring Cloud Gateway官方提供的RequestRateLimiterGatewayFilterFactory过滤器工厂来实现网关限流之外,我们还可以选择集成 Sentinel 来实现限流。

本文将主要学习 Spring Cloud Gateway 集成 Sentinel 实现限流,对 Sentinel 的基础知识不做过多的讲解,大家可以直接参考官方文档:Sentinel 官方文档 (sentinelguard.io)

话不多说,开始今天的学习。

Sentinel 限流

从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:

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

1、添加依赖

<!-- SpringCloud Alibaba Sentinel -->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
		
<!-- SpringCloud Alibaba Sentinel Gateway -->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>

2、自定义限流异常处理类

/**
 * 自定义限流异常处理
 *
 * @author ezhang
 */
public class SentinelFallbackHandler implements WebExceptionHandler {

    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":429, "msg":"请求超过最大数,请稍后再试"}".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);
        }
        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);
    }
}

该类实现了 Spring WebFlux 中的异常处理接口 WebExceptionHandler

这个类除了自定义之外,也可以直接使用 Sentinel 自带的 SentinelGatewayBlockExceptionHandler 类。

public class SentinelGatewayBlockExceptionHandler implements WebExceptionHandler {
    private List<ViewResolver> viewResolvers;
    private List<HttpMessageWriter<?>> messageWriters;
    private final Supplier<Context> contextSupplier = () -> {
        return new Context() {
            public List<HttpMessageWriter<?>> messageWriters() {
                return SentinelGatewayBlockExceptionHandler.this.messageWriters;
            }

            public List<ViewResolver> viewResolvers() {
                return SentinelGatewayBlockExceptionHandler.this.viewResolvers;
            }
        };
    };

    public SentinelGatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolvers;
        this.messageWriters = serverCodecConfigurer.getWriters();
    }

    private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {
        return response.writeTo(exchange, (Context)this.contextSupplier.get());
    }

    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        if (exchange.getResponse().isCommitted()) {
            return Mono.error(ex);
        } else {
            return !BlockException.isBlockException(ex) ? Mono.error(ex) : this.handleBlockedRequest(exchange, ex).flatMap((response) -> {
                return this.writeResponse(response, exchange);
            });
        }
    }

    private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) {
        return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
    }
}

SentinelGatewayBlockExceptionHandler 类同样是实现了 WebExceptionHandler 接口。

3、限流规则配置类

/**
 * 限流规则配置类
 *
 * @author ezhang
 */

@Configuration
public class GatewayConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelFallbackHandler sentinelGatewayExceptionHandler() {
        return new SentinelFallbackHandler();
    }

    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter() {

        return new SentinelGatewayFilter();
    }

    @PostConstruct
    public void doInit() {
        // 加载网关限流规则
        initGatewayRules();
    }

    /**
     * 网关限流规则
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        rules.add(new GatewayFlowRule("cloud-gateway")
                .setCount(3)    // 限流阈值
                .setIntervalSec(60));   // 统计时间窗口,单位是秒,默认是 1 秒
        // 加载网关限流规则
        GatewayRuleManager.loadRules(rules);
    }
}

4、application.yml 配置

server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: cloud-gateway
          uri: http://192.168.1.211:8088
          predicates:
            - Path=/ytb/**
          filters:
            - StripPrefix=1

5、启动测试

启动项目后我们还是访问这个接口 http://localhost:8080/ytb/file/getFileList

image.png

此时返回是正常的,当我们一分钟之内访问第四次的时候就会返回异常了,此时说明限流成功。

image.png

同时我们还可以在 cmd 下执行这个命令来查看实时的统计信息

curl http://localhost:8719/cnode?id=cloud-gateway

输出内容格式如下:

image.png

其中:

  • thread: 代表当前处理该资源的线程数;
  • pass: 代表一秒内到来到的请求;
  • blocked: 代表一秒内被流量控制的请求数量;
  • success: 代表一秒内成功处理完的请求;
  • total: 代表到一秒内到来的请求以及被阻止的请求总和;
  • RT: 代表一秒内该资源的平均响应时间;
  • 1m-pass: 则是一分钟内到来的请求;
  • 1m-block: 则是一分钟内被阻止的请求;
  • 1m-all: 则是一分钟内到来的请求和被阻止的请求的总和;
  • exception: 则是一秒内业务本身异常的总和。

除此之外,无论触发了限流、熔断降级还是系统保护,它们的秒级拦截详情日志都在 ${user_home}/logs/csp/sentinel-block.log里。如果没有发生拦截,则该日志不会出现。日志格式如下:

2021-12-24 15:46:02|1|cloud-gateway,ParamFlowException,$D,|2,0
2021-12-24 15:46:03|1|cloud-gateway,ParamFlowException,$D,|1,0

日志含义:

index例子说明
12021-12-24 15:46:03时间戳
21该秒发生的第一个资源
3cloud-gateway资源名称
4XXXException拦截的原因, 通常 FlowException 代表是被限流规则拦截,DegradeException 则表示被降级,SystemBlockException 则表示被系统保护拦截
52,02 被拦截的数量,0无意义可忽略

Sentinel分组限流

cloud-systemcloud-ytb分组限流配置

1、application.yml配置文件

server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: cloud-ytb
          uri: http://192.168.1.211:8088
          predicates:
          - Path=/ytb/**
          filters:
          - StripPrefix=1
        - id: cloud-system
          uri: http://192.168.1.211:8088
          predicates:
          - Path=/system/**
          filters:
          - StripPrefix=1

2、限流规则配置类

/**
 * 限流规则配置类
 *
 * @author ezhang
 */

@Configuration
public class GatewayConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelFallbackHandler sentinelGatewayExceptionHandler() {
        return new SentinelFallbackHandler();
    }

    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter() {

        return new SentinelGatewayFilter();
    }

    @PostConstruct
    public void doInit() {
        // 分组
        initCustomizedApis();
        // 加载网关限流规则
        initGatewayRules();
    }

    /**
     * 网关限流规则
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        rules.add(new GatewayFlowRule("system-api")
                .setCount(3)    // 限流阈值
                .setIntervalSec(60));   // 统计时间窗口,单位是秒,默认是 1 秒
        rules.add(new GatewayFlowRule("ytb-api")
                .setCount(6)    // 限流阈值
                .setIntervalSec(60));
        // 加载网关限流规则
        GatewayRuleManager.loadRules(rules);
    }

    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        // cloud-system 组
        ApiDefinition api1 = new ApiDefinition("system-api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {
                    {
                        // 匹配 /file 以及其子路径的所有请求
                        add(new ApiPathPredicateItem().setPattern("/system/file/**")
                                .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                    }
                });
        // cloud-ytb 组
        ApiDefinition api2 = new ApiDefinition("ytb-api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {
                    {
                        // 只匹配 /file/getFileList
                        add(new ApiPathPredicateItem().setPattern("/ytb/file/getFileList"));
                    }
                });
        definitions.add(api1);
        definitions.add(api2);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }

}

访问:http://localhost:8080/system/file/getFileList (触发限流)
访问:http://localhost:8080/system/user/list (不会触发限流)
访问:http://localhost:8080/ytb/file/getFileList (触发限流)
访问:http://localhost:8080/ytb/file/updateFileInfo (不会触发限流)

Sentinel自定义异常

Sentinel支持自定义异常处理。

方案一:yml配置

# Spring
spring: 
  cloud:
    sentinel:
      scg:
        fallback:
          mode: response
          response-body: '{"code":403,"msg":"请求超过最大数,请稍后再试"}'

启动测试触发限流之后:

image.png

方案二:就是上面自定义的 SentinelFallbackHandler 注入到 GatewayConfig

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelFallbackHandler sentinelGatewayExceptionHandler() {
    return new SentinelFallbackHandler();
}

自定义异常可以使触发限流后返回信息更规整,如果我们直接使用 Sentinel 自带的 SentinelGatewayBlockExceptionHandler 类注入到 GatewayConfig 中那么触发限流之后返回的是这样的异常信息

image.png

这里只是简单的学习一下 Spring Cloud Gateway 集成 Sentinel 怎样配置实现限流。没有深入、系统的学习 Sentinel。后面针对 Sentinel 熔断降级等会专门去学习下。

Spring Cloud Gateway 系列的学习目前就告一段落了,接下来准备开始 Nacos 的学习了。