目录
一. 限流中的基础问题
1. 为什么限流及常见限流方案
- 例如用户增长过快,当前服务配置情况下压力过大,热点事件,恶意请求等等,简单解释一下不做限流的危害
- 常见限流方案:
- Hystrix 线程池隔离,或信号量方式超过线程池的负载,走熔断的逻辑。在一般应用服务器中,比如tomcat容器也是通过限制它的线程数来控制并发的;
- 通过时间窗口的平均速度来控制流量。常见的限流纬度有比如通过Ip来限流、通过uri来限流、通过用户访问频次来限流。
- 一般限流都是在网关这一层做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;也可以在应用层通过Aop这种方式去做限流。
- 通常情况下增加限流后会设置请求进行熔断降级处理
- 什么是熔断: 类似于保险丝,当某服务出现不可用或响应超时的情况,为了防止整个系统出现雪崩,暂停对服务的调用,上层设置请求直接返回
- 什么是降级: 从整个服务负荷情况出发和考虑,对某些负荷较高的情况,提供预备方案,当超过负载时执行预备方案或者直接走兜底的方法,从而释放服务器资源以保证核心业务,
- 实际都是为了保证整个系统可以正常对外提供服务,不同点是服务降级一般是从整体的负荷考虑,主动降级
- 通过熔断降级再次解释限流的必要性:
假设创建订单成功调用短信服务发送短信,假设创建订单服务在调用短信服务时,短信服务异常,可能会造成创建订单一直拿不到响应,请求堆积,最终造成后续订单也无法创建的问题(实际生产上这种发短信的业务可能通过mq异步执行,此处只是举个例子)
2. 常见限流算法
常用限流算法: 计数器(滑动窗口算法计数器),令牌桶算法(RateLimiter),漏桶算法(短时间内不能承受过大的流量),(代码层面实现限流)
计数器限流算法
- 计数器限流算法有两种: 传统计数器,与滑动窗口计数器
- 传统计数器方式存在的临界问题:
假设: 一秒钟内某个后端运行请求不超过10次,使用原子类针对于某个服务设置一个计数器,每请求一次计数器+1,当请求超过10此时,并且与第一次请求的时间间隔不超过一秒钟,说明请求过多,服务熔断,执行服务降级,给用户提示信息,如果与第一次请求间隔超过一秒钟,说明还在范围内重置计数器(思考问题: 传统的计数器方式,假设在第59到第61临界时突然发生了大量的请求,由于都在判断的阀值以内,可能会达不到限流的效果,使用滑动窗口算法实现计数器解决)
- 滑动窗口算法计数器, 可以解决传统计数器临界问题:
假设每分钟允许向后台请求60次,可以将这60次请求分为6份,每秒钟允许请求10此,通过滑动进行判断,解决临界问题,(是不是可以看为,当到达一分钟时滑动一个,在滑动式也会计算每一份的效率,将60次请求分为6个窗口,每个窗口为10秒10次,当到达1分钟时滑动包含超过1分钟的另外一份,如果在一份中内判断超过60次请求进行熔断,降级,如果在滑动包含时每个窗口的效率每10秒超过10次也熔断,降级,滑动以后,如果滑动以后没有到达熔断的条件第一个窗口初始化为开始值)
令牌桶算法
- 令牌桶有两个动作:
- 固定速率,向桶中添加令牌
- 客户端如果发送请求向后台,首先要获取桶中的令牌
- 令牌桶中存放有固定容量的令牌,会开启独立线程按照固定的速率向桶中添加令牌,假设每秒向桶中添加2个令牌,客户端请求服务端时需要先在令牌桶中获取令牌,例如每秒有10获取令牌的请求,但是每秒只生成2个令牌,其它8获取不到令牌的请求就会被服务降级,假设每秒只发送一个请求,此时消费令牌的速度过慢,就需要通过令牌桶的容量进行限制,当令牌桶存满时,则不会再去添加令牌(令牌桶优点可以接受突然的高并发)
漏桶算法
- 漏桶算法内部有一个容器,当请求进来时,相当入将请求通过容器匀速的流出,这样不管上层接收多少请求,下面流出的速度始终保持不变,因为处理的速度是固定的,请求进来的速度是未知的,没来得及处理的请求就先放在桶里,桶的容量是有限的,如果满了,新进来的请求走降级策略
- 漏桶算法存在一个缺陷,无法应对短时间的突发流量
3. 几种基础版限流实现方案
基于redis实现限流
- 基础版, 定时生成token存储redis,并对token个数进行限制,请求执行前先通过redis获取令牌,获取不到则执行降级策略
- 进阶:redis实现滑动窗口限流, 使用 zset 记录 IP 访问次数,每个 IP 通过 key 保存下来,score 保存当前时间戳,value 唯一用时间戳或者UUID来实现,当接收请求时,时间分数作为窗口限制,获取该窗口记录行数,判断是否超过限制,若超过则执行降级
public class RedisLimiterTest {
private Jedis jedis;
public RedisLimiterTest(Jedis jedis) {
this.jedis = jedis;
}
/**
* @param ipAddress Ip地址
* @param period 特定的时间内,单位秒
* @param maxCount 最大允许的次数
* @return
*/
public boolean isIpLimit(String ipAddress, int period, int maxCount) {
String key = String.format("ip:%s", ipAddress);
// 毫秒时间戳
long currentTimeMillis = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
// redis事务,保证原子性
pipe.multi();
// 存放数据,value 和 score 都使用毫秒时间戳
pipe.zadd(key, currentTimeMillis, "" + UUID.randomUUID());
// 移除窗口区间所有的元素
pipe.zremrangeByScore(key, 0, currentTimeMillis - period * 1000);
// 获取时间窗口内的行为数量
Response<Long> count = pipe.zcard(key);
// 设置 zset 过期时间,避免冷用户持续占用内存,这里宽限1s
pipe.expire(key, period + 1);
// 提交事务
pipe.exec();
pipe.close();
// 比较数量是否超标
return count.get() > maxCount;
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
RedisLimiterTest limiter = new RedisLimiterTest(jedis);
for (int i = 1; i <= 20; i++) {
// 验证IP 10秒钟之内只能访问5次
boolean isLimit = limiter.isIpLimit("222.73.55.22", 10, 5);
System.out.println("访问第" + i + "次, 结果:" + (isLimit ? "限制访问" : "允许访问"));
}
}
}
基于 Guava RateLimiter 实现令牌算法
- 项目中引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
- 实现流程
- 创建操作限流对象,并设置向令牌桶中添加令牌的速率
- 指定等待时间内获取锁,
- 如果指定时间内获取不到锁,服务降级给用户提示信息
- 如果指定时间内获取到锁调用生成订单的逻辑方法
- 此处有一个问题(没有设置令牌桶存满的个数)
- 重点逻辑
- 使用RateLimiter 实现令牌桶算法, 创建RateLimiter对象,设置限流规则
- 通过RateLimiter 调用tryAcquire()尝试获取令牌,并设置等待时间
- tryAcquire()空参方法获取令牌,如果令牌获取不到则线程会一直等待,tryAcquire(指定等待时间,单位)方法设置指定等待时间,指定时间内返回是否获取到令牌的布尔值,如果获取不到令牌线程也不会一直等待
- 代码示例
import com.google.common.util.concurrent.RateLimiter;
import com.shsxt.orderService.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class IndexController {
@Autowired
private OrderService orderService;
// 解释:1.0 表示 每秒中生成1个令牌存放在桶中(以秒为单位的固定速率值)
// 如果方法中传入的为"10.0"则表示每秒中向桶中存放10个令牌
RateLimiter rateLimiter = RateLimiter.create(1.0);//此方法是一个独立线程所开启的,不受其他线程影响
// 下单请求
@RequestMapping("/order")
public String order() {
// 1.限流判断
// 当前请求线程在令牌桶中拿到令牌的等待时间
double b = rateLimiter.acquire();
//2.判断请求在指定时间内是否获取到令牌,假设此时每秒会有10个客户端发起抢购,
// 请求该接口,由于令牌桶每秒只能生成1个,其他9个获取不到令牌的请求会等待,
// 通过tryAcquire方法设置获取令牌的等待时间为500毫秒,假设500毫秒内获取不到令牌
// 则请求不会继续等待,返回false,走if语句内,相当于服务降级,返回给用户
// 提示信息,提示用户当前系统繁忙请稍后再试
boolean acquire = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
//rateLimiter.tryAcquire();//该方式获取令牌,如果获取不到令牌,线程会一直等待
if (!acquire) {
return "当前系统繁忙请稍后再试";//获取令牌失败服务降级,给出用户提示信息,
}
// 3.如果没有达到限流的要求或者500毫秒内获取到了令牌,直接调用订单接口
// 获取订单是否生成成功,判断抢购是否成功,给用户提示信息
boolean isOrderAdd = orderService.addOrder();
if (isOrderAdd) {
return "恭喜您,抢购成功!";
}
return "抢购失败!";//此处是虽然获取到了令牌,但是生成订单时由于某种原因失败而定义的
}
}
二. Gateway Redis 令牌桶实现限流案例
- 在Spring Cloud Gateway官方提供了一个类RequestRateLimiterGatewayFilterFactory,适用Redis和lua脚本实现了令牌桶的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:
- 实现步骤
- 创建项目引入依赖,包括Gateway,与redis
- 项目中增加限流标识,也就是进行限流判断时,是否限流的判断依据
- 配置限流速率
<!--基于Redis实现限流-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.2.10.RELEASE</version>
</dependency>
- 增加限流标识,限流通常要根据某一个参数值作为依据来限流,比如每个IP每秒钟只能访问2次,此时是以IP为参考依据,我们创建根据IP限流的对象,该对象需要实现接口KeyResolver:
public class IpKeyResolver implements KeyResolver {
/***
* 根据IP限流
* @param exchange
* @return
*/
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
- 将限流标识对象注入到Spring容器
@Bean(name = "ipKeyResolver")
public KeyResolver userIpKeyResolver() {
return new IpKeyResolver();
}
- yaml文件中配置限流速率等等
spring:
cloud:
gateway:
routes:
#商品服务
- id: goods_route
uri: lb://mall-goods
predicates:
- Path=/mall/brand/**
filters:
- StripPrefix=1
# 指定过滤器
- name: RequestRateLimiter
args:
# 指定限流标识
key-resolver: '#{@ipKeyResolver}' #对应上面注入到容器中的限流标识对象名,用于限流的键的解析器的Bean对象的名字.会使用 SpEL 表达式根据#{@beanName}从Spring容器中获取Bean对象
# 速率限流
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
# 能容纳的并发流量总数
redis-rate-limiter.burstCapacity: 2 #令牌桶总容量
三. Gateway整合Sentinel实现网关限流
- Sentinel 支持对 SpringCloudGateway、Zuul 等主流的 API 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 分组维度进行限流
- 网关流控实现原理
- 通过 GatewayRuleManager 加载网关流控规则GatewayFlowRule时,Sentinel 底层会将网关流控规则转化为热点参数规则(ParamFlowRule).存储在 GatewayRuleManager 中,与正常的热点参数规则相隔离。转换时 Sentinel 会根据请求属性配置,为网关流控规则设置参数索引(idx),并同步到生成的热点参数规则中
- 外部请求进入 API Gateway 时会经过 Sentinel 实现的 filter,会依次进行: 路由/API 分组匹配、请求属性解析和参数组装。
2.1 Sentinel 会根据配置的网关流控规则来解析请求属性,并依照参数索引顺序组装参数数组,最终传入 SphU.entry(res, args) 中
2.2 Sentinel API Gateway Adapter Common 模块向 Slot Chain 中添加了一个 GatewayFlowSlot,专门用来做网关规则的检查。GatewayFlowSlot 会从 GatewayRuleManager 中提取生成的热点参数规则,根据传入的参数依次进行规则检查。若某条规则不针对请求属性,则会在参数最后一个位置置入预设的常量,达到普通流控的效果
基础使用示例
- 项目中已入依赖(注意不同版本Gateway与Sentinel的引入依赖可能不同)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
- 配置
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1 # nacos注册地址
config:
server-addr: 127.0.0.1 # 远程配置地址
sentinel:
transport:
dashboard: localhost:8080 # sentinel注册地址
# 直接建立心跳
eager: true
scg:
# 限流后的响应配置
fallback:
content-type: application/json
# 模式 response、redirect
mode: response
# 响应状态码
response-status: 429
# 响应信息
response-body: 对不起,已经被限流了!!!
gateway:
# spring cloud gateway 路由配置方式
discovery:
locator:
enabled: true # 表明gateway开启服务注册和发现的功能
lower-case-service-id: true # 将请求路径上的服务名配置为小写
route-id-prefix: route-id- # 配置routeId的前缀,最终为 route-id-注册服务名
自定义 GatewayFilter, 自定义API分组维度实现限流, 以及异常处理
- 再次解释 基于Sentinel的Gateway限流是通过其提供的Filter来完成的,使用时可以自定义注入对应的SentinelGatewayFilter实例以及SentinelGatewayBlockExceptionHandler实例
package cn.jack.config;
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.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>> 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(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* 配置初始化的限流參數
*/
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<GatewayFlowRule>();
rules.add(
new GatewayFlowRule("product_route") // 资源名称,对应路由id
.setCount(1) // 限流阈值
.setIntervalSec(5) // 统计时间窗口(熔断时间),单位:秒。默认是1秒
);
GatewayRuleManager.loadRules(rules);
}
/**
* 自定义限流异常页面,限流返回的信息
*/
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
- 自定义API分组维度实现限流, 是一种更细粒度的限流规则定义。它可以对匹配到的接口进行限流
package cn.jack.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.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.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 GatewayConfiguration2 {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration2(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(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* 配置初始化的限流參數
*/
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<GatewayFlowRule>();
rules.add(
new GatewayFlowRule("product_api_001") // 资源名称,可以是sentinel的API定义的分组
.setCount(1) // 限流阈值
.setIntervalSec(5) // 统计时间窗口(即时间段内达到阈值实行限流),单位:秒。默认是1秒
);
rules.add(
new GatewayFlowRule("product_api_002") // 资源名称,可以是sentinel的API定义的分组
.setCount(1) // 限流阈值
.setIntervalSec(5) // 统计时间窗口(即时间段内达到阈值实行限流),单位:秒。默认是1秒
);
GatewayRuleManager.loadRules(rules);
}
// 自定义API分组
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
Set predicateItemSet1 = new HashSet();
predicateItemSet1.add(new ApiPathPredicateItem()
.setPattern("/product-serv/product/api001/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); // 前缀匹配
ApiDefinition api1 = new ApiDefinition("product_api_001")
.setPredicateItems(predicateItemSet1);
Set predicateItemSet2 = new HashSet();
predicateItemSet2.add(new ApiPathPredicateItem()
.setPattern("/product-serv/product/api002/demo1")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_EXACT)); // 精确匹配
ApiDefinition api2 = new ApiDefinition("product_api_002")
.setPredicateItems(predicateItemSet2);
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
/**
* 自定义限流异常页面,限流返回的信息
*/
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}