概述:在SpringCloud中我们之前在使用Hystrix 以及 sentinel 中都可以对服务调用进行限流控制,在SpringCloud体系中通过网关也可以对整个系统架构进行限流处理。本章将介绍一些通过微服务网关实现限流的一些常见算法以及其在SpringCloud Gateway 网关下的实现方式。
一、常见的限流算法
1,计数器算法
计数器算法为最简答的限流算法,其实现原理是为维护一个单位时间内的计数器。在单位时间内,开始计数器为0,每次通过一个请求计数器+1。如果单位时间内 计数器的数量大于了预先设定的阈值,则在此刻到单位时间的最后一刻范围内的请求都将被拒绝。单位时间结束计数器归零,重新开始计数。
2,漏桶算法
漏桶算法实际为一个容器请求队列,关键要素为 桶大小(队列大小),流出速率(出队速率)。即无论请求并发多高,如果桶内的队列满了,多余进来的请求都将被舍弃。由于桶的流出速率固定,所以可以保证限流后的请求并发数可以固定在一个范围内。
3,令牌桶算法
令牌桶算法为漏桶算法的一种改进。漏桶算法能够控制调用服务的速率,而令牌桶算法不仅能控制调用服务的速率,还能在短时间内允许一个超并发的调用。其实现原理为,存在一个令牌桶,并且有一个持续不断地产生令牌的机制,比如每秒产生100个令牌。桶存在一个固定大小,比如300。当桶中的令牌满了的时候,多余的令牌将被舍弃。
当请求过来时必须先从桶中获取一个令牌,桶内令牌数减一,获取到令牌的请求将被放行。桶中令牌被用光时,没有获取到令牌的请求将进行等待或者拒绝。这样在短期的时间内该算法将允许大于100,小于等于300的并发。如果持续有大于100的并发请求经过网关,在消耗完桶内令牌后,则最大通过网关的qps为产生令牌的速率,及 qps=100。
二、限流算法的简单实现方式
1,使用Filter 实现令牌桶算法(RequestRateLimiter局部过滤器,需要借助redis+lua脚本实现)
- 修改pom 添加redis依赖
- 添加reids key 的解析器即key-resolver 解析类
- 调整配置文件
pom.xml 需要添加redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
解析类src/main/java/com/xiaohui/gateway/config/KeyResolverConfiguration.java
package com.xiaohui.gateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class KeyResolverConfiguration {
@Bean
public KeyResolver pathKeyResolver(){
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getPath().toString());
}
};
}
}
application.properties 中主要部分
spring:
application:
name: api-gateway-server #服务名称
redis:
host: 127.0.0.1
pool: 6379
database: 0
cloud:
gateway:
routes:
#配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service #保持唯一
#uri: http://127.0.0.1:8001 #目标为服务地址
uri: lb://cloud-payment-service # lb:// 根据服务名称从注册中心获取请求地址路径
predicates:
#- Path=/payment/** #路由条件 path 路由匹配条件
- Path=/product-service/** #给服务名称前加上一个固定的应用分类路径 将该路径转发到 http://127.0.0.1:8001/payment/get/1
filters: #配置路由过滤器 http://127.0.0.1:8080/product-service/payment/get/1 -> http://127.0.0.1:8001/payment/get/1
- name: RequestRateLimiter
args:
#使用SpEL从容器中获取对象
key-resolver: '#{@pathKeyResolver}'
#桶令牌每秒产生平均速率
redis-rate-limiter.replenishRate: 1
#令牌桶的上限
redis-rate-limiter.burstCapacity: 2
- RewritePath=/product-service/(?<segment>.*),/$\{segment} #路径重写的过滤器,在yml中$写为 $\
如果需要进行根据判断参数(userId)进行设置限流则其解析类可以调整为如下示例(修改配置文件中key-resolver 项为 paramKeyResolver):
package com.xiaohui.gateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class KeyResolverConfiguration {
@Bean
public KeyResolver paramKeyResolver(){
return exchange -> Mono.just(
exchange.getRequest().getQueryParams().getFirst("userId")
);
}
}
效果:每秒钟一次请求http://127.0.0.1:8080/product-service/payment/get/1?userId=11 没问题,如果连续每秒钟多次请求改地址,则部分请求会出现返回429错误码。
2,使用Sentinel 实现限流
在Gateway + Sentinel的实现限流方案中,有两种限流方式:
- 基于 route-id 的ruote维度:即在application.yml 文件中的spring.cloud.gateway.routes[0].id配置项
- 基于自定义API维度:通过 ApiDefinition 定义不同的分组,进行分组限流控制
使用Gateway+Sentinel的形式 都需要添加Sentinel的依赖,依赖坐标如下:
<!-- 使用sentinel对gateway进行限流实现 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.6.3</version>
</dependency>
实现方式一:基于route-id的形式
该种配置方式将对配置的路由id中的微服务中全部接口进行限流控制。无法进行单独的限流控制。
package com.xiaohui.gateway.config;
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.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GatewayFilter;
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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolverProvider,
ServerCodecConfigurer serverCodecConfigurer){
this.viewResolvers = viewResolverProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 配置限流的异常处理器
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler(){
return new SentinelGatewayBlockExceptionHandler(this.viewResolvers,this.serverCodecConfigurer);
}
/**
* 配置限流过滤器
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGlobalFilter(){
return new SentinelGatewayFilter();
}
/**
* product-service: rules-id配置项值
* setIntervalSec : 单位时间
* setCount : 调用总数
*/
@PostConstruct
public void initGatewayRules(){
Set<GatewayFlowRule> rules = new HashSet<>();
// 按照 rules-id配置项值 整体设置限流
rules.add(new GatewayFlowRule("product-service").setIntervalSec(1).setCount(1));
GatewayRuleManager.loadRules(rules);
}
/**
* 自定义限流处理器
*/
@PostConstruct
public void initBlockHanlers(){
BlockRequestHandler blockhandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap();
map.put("code","001");
map.put("msg","网关限流拦截返回...");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockhandler);
}
}
对应的application.yml 如下:
server:
port: 8080
spring:
application:
name: api-gateway-server #服务名称
redis:
host: 127.0.0.1
pool: 6379
database: 0
cloud:
gateway:
routes:
#配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service #保持唯一
#uri: http://127.0.0.1:8001 #目标为服务地址
uri: lb://cloud-payment-service # lb:// 根据服务名称从注册中心获取请求地址路径
predicates:
#- Path=/payment/** #路由条件 path 路由匹配条件
- Path=/product-service/** #给服务名称前加上一个固定的应用分类路径 将该路径转发到 http://127.0.0.1:8001/payment/get/1
filters: #配置路由过滤器 http://127.0.0.1:8080/product-service/payment/get/1 -> http://127.0.0.1:8001/payment/get/1
- RewritePath=/product-service/(?<segment>.*),/$\{segment} #路径重写的过滤器,在yml中$写为 $\
# 配置自动根据微服务名称进行路由转发 http://127.0.0.1:8080/cloud-payment-service/payment/get/1
discovery:
locator:
enabled: true #开启根据服务名称自动转发
lower-case-service-id: true #微服务名称已小写形式呈现
#eureka 注册中心
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka1.com:9000/eureka/
instance:
prefer-ip-address: true #使用ip进行注册
该配置类中在initGatewayRules 方法中定义了需要限流的服务id product-service 对应的微服务 uri: lb://cloud-payment-service. initBlockHanlers方法定义了限流时对限流接口的返回信息封装。
实现方式二:基于自定义API维度
配置文件同实现方式一的,配置类如下:
package com.xiaohui.gateway.config;
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.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GatewayFilter;
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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.*;
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolverProvider,
ServerCodecConfigurer serverCodecConfigurer){
this.viewResolvers = viewResolverProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 配置限流的异常处理器
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler(){
return new SentinelGatewayBlockExceptionHandler(this.viewResolvers,this.serverCodecConfigurer);
}
/**
* 配置限流过滤器
* @return
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGlobalFilter(){
return new SentinelGatewayFilter();
}
/**
* product-service: rules-id配置项值
* setIntervalSec : 单位时间
* setCount : 调用总数
*/
@PostConstruct
public void initGatewayRules(){
Set<GatewayFlowRule> rules = new HashSet<>();
// 按照 rules-id配置项值 整体设置限流
// rules.add(new GatewayFlowRule("product-service").setIntervalSec(1).setCount(1));
//分组限流
rules.add(new GatewayFlowRule("product_api").setIntervalSec(1).setCount(1));
rules.add(new GatewayFlowRule("order_api").setIntervalSec(3).setCount(1));
GatewayRuleManager.loadRules(rules);
}
/**
* 自定义限流处理器
*/
@PostConstruct
public void initBlockHanlers(){
BlockRequestHandler blockhandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap();
map.put("code","001");
map.put("msg","网关限流拦截返回...");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockhandler);
}
/**
* 自定义限流分组
* 1,定义分组
* 2,对个组设置匹配路径 如:/product-service/**
*/
@PostConstruct
private void initCustomizedApis(){
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product_api")
.setPredicateItems(new HashSet<ApiPredicateItem>(){{
add(new ApiPathPredicateItem().setPattern("/product-service/payment/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
add(new ApiPathPredicateItem().setPattern("/product-service/payment2/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("order_api")
.setPredicateItems(new HashSet<ApiPredicateItem>(){{
add(new ApiPathPredicateItem().setPattern("/product-service/order/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}
在 initCustomizedApis 方法中,定义了两个分组 "product_api"、"order_api",并分别配置了不同的匹配路径。
在initGatewayRules 方法中对不同的分组设定了不同的限流规则。单位时间和 阈值的指定。