在分布式系统中,网关作为流量的入口,因此会有大量的请求进入网关,向其他服务发起调用,其他服务不可避免的会出现调用失败(超时、异常),失败时不能让请求堆积在网关上,需要快速失败并返回给客户端,想要实现这个要求,就必须在网关上做熔断、降级操作。
为什么在网关上请求失败需要快速返回给客户端?
因为当一个客户端请求发生故障的时候,这个请求会一直堆积在网关上,当然只有一个这种请求,网关肯定没有问题(如果一个请求就能造成整个系统瘫痪,那这个系统可以下架了),但是网关上堆积多了就会给网关乃至整个服务都造成巨大的压力,甚至整个服务宕掉。因此要对一些服务和页面进行有策略的降级,以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的响应,所以需要网关上请求失败需要快速返回给客户端。
Spring Cloud Gateway 集成熔断、限流
集成 Hystrix 熔断降级,引用hystrix依赖,在filters下加入熔断降级配置,设置降级后返回的路由,同时配置默认使用信号量隔离、3秒主动超时。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
server:
port: 8082
spring:
application:
name: gateway
redis:
host: localhost
port: 6379
password: 123456
cloud:
gateway:
routes:
- id: rateLimit_route
uri: http://localhost:8000
order: 0
predicates:
- Path=/test/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: defaultfallback
fallbackUri: forward:/defaultfallback
# hystrix 信号量隔离,1.5秒后自动超时
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE
thread:
timeoutInMilliseconds: 1500
# hystrix.shareSecurityContext 属性为true。这样做会自动配置一个Hystrix 并发策略插件钩子,
# 然后会将SecurityContext从你当前主线程传输到一个使用Hystrix Command注解的地方。
shareSecurityContext: true
这里的配置,使用了两个过滤器:
1)过滤器StripPrefix,作用是去掉请求路径的最前面n个部分截取掉。
StripPrefix=1就代表截取路径的个数为1,比如前端过来请求/test/good/1/view,匹配成功后,路由到后端的请求路径就会变成http://localhost:8888/good/1/view。
2)过滤器Hystrix,作用是通过Hystrix进行熔断降级
当上游的请求,进入了Hystrix熔断降级机制时,就会调用fallbackUri配置的降级地址。需要注意的是,还需要单独设置Hystrix的commandKey的超时时间
fallbackUri配置的网关降级地址的代码如下:
@RestController
public class FallbackController {
@GetMapping("/defaultfallback")
public Response defaultfallback() {
Response response = new Response();
response.setCode("100");
response.setMessage("服务暂时不可用");
return response;
}
}
漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(Traffic Policing),漏桶算法的描述如下:
一个固定容量的漏桶,按照常量固定速率流出水滴;
如果桶是空的,则不需流出水滴;
可以以任意速率流入水滴到漏桶;
如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
令牌桶算法
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
SpringCloudGateway限流方案
在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用在Redis内的通过执行Lua脚本实现了令牌桶的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:
Spring Cloud Gateway 默认实现 Redis限流,如果扩展只需要实现Ratelimter接口即可,同时也可以通过自定义KeyResolver来指定限流的Key,比如我们需要根据用户、IP、URI来做限流等等,通过exchange对象可以获取到请求信息。
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!--springboot2.X默认使用lettuce连接池,需要引入commons-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
application.yml下添加限流配置:
server:
port: 8081
spring:
cloud:
gateway:
routes:
- id: order-center
uri: lb://order-center
predicates:
- Path=/order/**
filters:
- name: RequestRateLimiter
args:
# 使用SpEL名称引用Bean,与上面新建的RateLimiterConfig类中的bean的name相同
key-resolver: '#{@userKeyResolver}'
# 每秒最大访问次数
redis-rate-limiter.replenishRate: 200
# 令牌桶最大容量
redis-rate-limiter.burstCapacity: 200
application:
name: cloud-gateway
redis:
host: 127.0.0.1
port: 6379
lettuce:
pool:
max-active: 200 #连接池最大连接数(使用负值表示没有限制)
max-idle: 20 # 连接池中的最大空闲连接
min-idle: 5 #连接池中的最小空闲连接
max-wait: 1000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
filter名称必须是RequestRateLimiter
redis-rate-limiter.replenishRate:允许用户每秒处理多少个请求
redis-rate-limiter.burstCapacity:令牌桶的容量,允许在一秒钟内完成的最大请求数
key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
限流配置类
@Configuration
public class RequestRateLimiterConfig {
/**
* 按URL限流,即以每秒内请求数按URL分组统计,超出限流的url请求都将返回429状态
*/
@Bean
@Primary
KeyResolver apiKeyResolver() {
//按URL限流
return exchange -> Mono.just(exchange.getRequest().getPath().toString());
}
/**
* 这里根据用户ID限流,请求路径中必须携带userId参数
*/
@Bean
KeyResolver userKeyResolver() {
//按用户限流
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
/**
* 通过exchange对象可以获取到请求信息,这边用了HostName
*/
@Bean
KeyResolver ipKeyResolver() {
//按IP来限流
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
健康检查配置actuator健康检查配置,为之后的功能提供支持,此部分比较简单,不再赘述,加入以下maven依赖和配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# actuator 监控配置
management:
#actuator端口 如果不配置做默认使用上面8080端口
server:
port: 8080
endpoints:
web:
exposure:
#默认值访问health,info端点 用*可以包含全部端点
include: "*"
#修改访问路径 2.0之前默认是/; 2.0默认是/actuator可以通过这个属性值修改
base-path: /actuator
endpoint:
shutdown:
enabled: true #打开shutdown端点
health:
show-details: always #获得健康检查中所有指标的详细信息
统一配置跨域请求
现在的请求通过经过gateWay网关时,需要在网关统一配置跨域请求,需求所有请求通过
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "*"
allowed-headers: "*"
allow-credentials: true
allowed-methods:
- GET
- POST
- DELETE
- PUT
- OPTION
整合Sentinel完成流控和降级
使用Sentinel作为gateWay的限流、降级、系统保护工具
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 分组维度进行限流。
其中网关限流规则 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 和 matchStrategy:为后续参数匹配特性预留,目前未实现。
用户可以通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,或通过 GatewayRuleManager.register2Property(property) 注册动态规则源动态推送(推荐方式)。
maven依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- sentinel适配支持Spring Cloud Gateway-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
配置文件
客户端配置:在配置文件中增加下列配置,dashboard就可以轻松管理客户端了,还有一种方式是在启动时加入
server:
port: 8003
spring:
application:
name: gateway-center
cloud:
sentinel:
transport:
## VM
##-Djava.net.preferIPv4Stack=true -Dcsp.sentinel.dashboard.server=localhost:8080 -Dcsp.sentinel.api.port=8666 -Dproject.name=gateway -Dcsp.sentinel.app.type=1
dashboard: localhost:8880
port: 8880
gateway:
routes:
- id: path_route
uri: lb://user-center
predicates:
- Path=/user/**
配置gateway sentinel配置
@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;
}
/**
* 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
* 自定义异常提示:当发生限流、熔断异常时,会返回定义的提示信息。
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public JsonSentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new JsonSentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
/**
* 由于sentinel的工作原理其实借助于全局的filter进行请求拦截并计算出是否进行限流、熔断等操作的,增加SentinelGateWayFilter配置
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* sentinel 不仅支持通过硬代码方式进行资源的申明,还能通过注解方式进行声明,
* 为了让注解生效,还需要配置切面类SentinelResourceAspect
* @return
*/
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
@PostConstruct
public void doInit() {
initGatewayRules();
}
/**
* 配置限流规则
*/
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
//order-center 路由ID,和配置文件中的一致就行
rules.add(new GatewayFlowRule("order-center")
.setCount(1) // 限流阈值
.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
}
指定参数限流
上面的配置是针对整个路由来限流的,如果我们只想对某个路由的参数做限流,那么可以使用参数限流方式:
rules.add(new GatewayFlowRule("path_route")
.setCount(1)
.setIntervalSec(1)
.setParamItem(new GatewayParamFlowItem()
.setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM).setFieldName("vipType")
)
);
通过指定PARAM_PARSE_STRATEGY_URL_PARAM表示从url中获取参数,setFieldName指定参数名称
自定义API分组
假设我有下面两个路由,我想让这两个路由共用一个限流规则,那么我们可以自定义进行组合:
- id: product-center
uri: lb://product-center
predicates:
- Path=/product/**
- id: order-center
uri: lb://order-center
predicates:
- Path=/order/**
自定义分组代码:
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("customized_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// product完全匹配
add(new ApiPathPredicateItem().setPattern("/product"));
// order/开头的
add(new ApiPathPredicateItem().setPattern("/order/**")
.setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api1);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
然后我们需要给customized_api这个资源进行配置:
rules.add(new GatewayFlowRule("customized_api")
.setCount(1)
.setIntervalSec(1)
);
自定义异常提示
当触发限流后页面显示的是Blocked by Sentinel: FlowException,正常情况下,就算给出提示也要跟后端服务的数据格式一样,如果你后端都是JSON格式的数据,那么异常的提示也要是JSON的格式,所以问题来了,我们怎么去自定义异常的输出?
前面我们有配置SentinelGatewayBlockExceptionHandler,需要重写SentinelGatewayBlockExceptionHandler中的输出方法
/**
* @Author: huangyibo
* @Date: 2021/8/22 18:02
* @Description: 配置 限流后异常处理 JsonSentinelGatewayBlockExceptionHandler
* 重写 SentinelGatewayBlockExceptionHandler
*/
@Slf4j
@Component
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) {
//return response.writeTo(exchange, contextSupplier.get());
ServerHttpResponse serverHttpResponse = exchange.getResponse();
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] datas = "{\"code\":403,\"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);
}
// 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;
}
};
}
/**
* 配置限流的异常处理器:JsonSentinelGatewayBlockExceptionHandler
* 自定义异常提示:当发生限流、熔断异常时,会返回定义的提示信息。
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public JsonSentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new JsonSentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}