微服务实战(八)集成Sentinel网关服务限流功能 SpringCloud GateWay + Sentinel + Nacos

本章主要内容

在SpringCloud GateWay服务网关中集成Sentinel来实现服务的限流功能。

首先,我们谈一下什么是服务限流。

在如今互联网的大环境下,我们后端的接口调用频次(QPS/TPS)动辄上百万,甚至达到千万级别,这对于服务端的承受能力是一个巨大的考验。那么限流在这个问题中起到什么作用呢?其实就是对于流量进行有策略的管理和限制。 比如说在一个系统中,有订单查询服务,商品查询服务,积分查询等,如果一个系统能支持的QPS(每秒查询次数)是100,那么这100该如何如何分配就是限流功能所考虑的事情。

根据这些服务的重要性,我们可能会把商品查询服务的QPS设置的比较高,因为这类用户查询到商品是有购买意向的,而订单查询、积分查询啥的管他呢!那么在一段时间内用户访问量过大时,我们就能尽量保证有购买意向的用户正常使用系统。这就是限流的作用。

什么是Sentinel

Sentinel 由阿里巴巴研发,主要以流量为切入点,从流量控制熔断降级系统负载保护等多个维度保护服务的稳定性。

文档:https://github.com/alibaba/Sentinel/wiki/主页

源码:https://github.com/alibaba/Sentinel

其实文档还是非常全面的,我也就不做搬运工了,主要是有目的性地讲述一下与SpringCloud GateWay的集成以及初步使用吧!

和SpringCloud GateWay 集成

添加依赖

先在我们之前搭建上的网关工程的pom.xml 中添加sentinel的网关依赖包

<dependency>
	<groupId>com.alibaba.csp</groupId>
	<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>

The managed version is 1.6.3 The artifact is managed in com.alibaba.cloud:spring-cloud-alibaba-dependencies:2.1.0.RELEASE

由于我们已经引入了 spring-cloud-alibaba-dependencies ,所以在上面就不用特意添加版本号了,默认就使用了1.6.3,与整体的SpringCloud版本对应。

注入Sentinel配置

我们写一个 @Configuration 配置类(放在Boot的类路径同级或者子级),里面的代码主要作用是设置需要限流的API (根据路径),以及限流的规则



import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;


@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    @PostConstruct
    public void doInit() {
        initCustomizedApis();
        initGatewayRules();
    }

    private void initCustomizedApis() {
/*
ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。
比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 API 分组下面。
限流的时候可以针对这个自定义的 API 分组维度进行限流。
*/
        Set<ApiDefinition> definitions = new HashSet<>();
        ApiDefinition api1 = new ApiDefinition("combat_gateway_api")
            .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                add(new ApiPathPredicateItem().setPattern("/nacos-provider/loadBanlance/print"));
            }});
        
        definitions.add(api1);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }

    private void initGatewayRules() {
/*
GatewayFlowRule:网关限流规则,
针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,
支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
*/
        Set<GatewayFlowRule> rules = new HashSet<>();
       
/*设置限流规则 
count: QPS即每秒钟允许的调用次数
intervalSec: 每隔多少时间统计一次汇总数据,统计时间窗口,单位是秒,默认是 1 秒。
*/
        rules.add(new GatewayFlowRule("combat_gateway_api")
            .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
            .setCount(5)
            .setIntervalSec(1)
            
        );
        GatewayRuleManager.loadRules(rules);
    }
}

其中网关限流规则 GatewayFlowRule 的字段解释如下:

  • resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。
  • resourceMode:规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是 route。
  • grade:限流指标维度,同限流规则的 grade 字段。
  • count:限流阈值
  • intervalSec:统计时间窗口,单位是秒,默认是 1 秒。
  • controlBehavior:流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
  • burst:应对突发请求时额外允许的请求数目。
  • maxQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。
  • paramItem:参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段:
    • parseStrategy:从请求中提取参数的策略,目前支持提取来源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数(PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。
    • fieldName:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称。
    • pattern:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流控;若为空则统计该请求属性的所有值。(1.6.2 版本开始支持)
    • matchStrategy:参数值的匹配策略,目前支持精确匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)和正则匹配(PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本开始支持)

用户可以通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,或通过 GatewayRuleManager.register2Property(property) 注册动态规则源动态推送(推荐方式)。

限流效果测试

把我们之前章节中搭建的那些个玩意儿都运行起来吧。

我这里运行了 nacos、combat-gateway、combat-provider

然后通过网关,调用我们之前写好的API接口。

http://127.0.0.1:9000/nacos-provider/loadBanlance/print

(注意调用的接口需要在 GatewayConfiguration  中设置好限流的路径)

在浏览器或者PostMan中调用接口,连续刷新。

然后我们查看Sentinel的日志(启动时会输出日志地址)

下面就是限流的统计数据

passQps blockQps successQps exceptionQps rt occupiedPassQps concurrency classification
5 36 9 0 1275 0 0 3
5 49 5 0 130 0 0 3

其中 passQps  代表通过的请求, blockQps 代表被阻止的请求, successQps  代表成功执行完成的请求个数, exceptionQps  代表用户自定义的异常, rt 代表平均响应时长。

由于之前设置的限流是 QPS =5 ,可以看到上面的限流日志中,通过的请求数是 <=5/s的。

rules.add(new GatewayFlowRule("combat_gateway_api")
            .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
            .setCount(5)
            .setIntervalSec(1)

自定义限流的返回值

通过刚才的测试发现,被限流后的返回编码是429。

Server returned HTTP response code: 429 for URL: http://127.0.0.1:9000/nacos-provider/loadBanlance/print

如何让它按照我们业务系统的规范返回定制的错误信息呢?这样才便于我们针对网关限流后的处理。

在之前的 GatewayConfiguration 中注册一个限流处理器

 GatewayCallbackManager.setBlockHandler(new MyBlockRequestHandler());

实现BlockRequestHandler

package com.zjf.combat.sentinel;

import static org.springframework.web.reactive.function.BodyInserters.fromObject;

import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;

import reactor.core.publisher.Mono;

public class MyBlockRequestHandler implements BlockRequestHandler {

	private static final String DEFAULT_BLOCK_MSG_PREFIX = "Blocked by Sentinel: ";

	@Override
	public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable ex) {
		
		// 返回http状态码为200
		return ServerResponse.status(200).contentType(MediaType.APPLICATION_JSON_UTF8)
				.body(fromObject(buildErrorResult(ex)));
	}

	

	private ErrorResult buildErrorResult(Throwable ex) {
		return new ErrorResult(200,
				DEFAULT_BLOCK_MSG_PREFIX + ex.getClass().getSimpleName());
	}

	

	private static class ErrorResult {
		private final int code;
		private final String message;

		ErrorResult(int code, String message) {
			this.code = code;
			this.message = message;
		}

		public int getCode() {
			return code;
		}

		public String getMessage() {
			return message;
		}
	}

}

被限流后的请求返回

发布了30 篇原创文章 · 获赞 123 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/u011177064/article/details/104270027