赞
踩
本篇博客基于:https://blog.csdn.net/BiandanLoveyou/article/details/117359078
限流就是限制流量,通过限流,可以很好的控制系统的 QPS(Query Per Second每秒查询率),从而达到保护系统的目的。
常见的限流算法:
1、计数器算法
2、漏桶算法(Leaky Bucket)
3、令牌桶算法(Token Bucket)
计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流 QPS 为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能全部把请求都拒绝,我们把这种现象称为“突刺现象”。如果有恶意的用户,某一时段发送大量的请求,正常的用户的请求就会被拒绝,造成服务器资源的浪费。
漏桶算法为了消除”突刺现象”,可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置QPS为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。
Gateway 提供了 RequestRateLimiterGatewayFilterFactory 这个工厂类,使用 Redis 和 Lua 脚本实现令牌桶的方式限流。我们实际开发中用得最多的就是令牌桶算法的限流方式。
官网学习地址:https://docs.spring.io/spring-cloud-gateway/docs/2.2.8.RELEASE/reference/html/
从官网文档可以看出,如果超过了限流,会报429错误,同时使用的是 Spel 表达式加载 bean 对象。
我们一般使用 Redis 的方式实现限流。为了演示 Gateway 的限流实例,我们需要 Redis 环境。可以参考之前我的博客,在 docker 里安装 Redis 镜像:https://blog.csdn.net/BiandanLoveyou/article/details/116422555
这是我本机 Redis 的基本信息:
最新版的 SpringBoot 的连接池默认使用 LettuceConnectionFactory 做连接池,因为 JedisPoolConfig 做连接池有线程不安全的隐患。Lettuce 是一个高性能的redis客户端,底层基于netty框架来管理连接,是非阻塞和线程安全的。比起 Jedis 需要为每个实例创建物理连接来保证线程安全,Lettuce 会更好点。
如果想了解 Jedis 做连接池,可以查看博客:https://blog.csdn.net/BiandanLoveyou/article/details/83269140
OK,我们修改 Gateway 服务的 pom.xml 配置文件,增加 Redis 的依赖:spring-boot-starter-data-redis-reactive 以及 commons-pool2 的依赖,完整配置如下:
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <parent>
- <artifactId>study</artifactId>
- <groupId>com.study</groupId>
- <version>1.0-SNAPSHOT</version>
- </parent>
- <modelVersion>4.0.0</modelVersion>
-
- <artifactId>gateway-server</artifactId>
-
- <dependencies>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-gateway</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
- </dependency>
- <!-- 需要 commons-pool2 的依赖 -->
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-pool2</artifactId>
- </dependency>
- </dependencies>
-
- </project>
application.yml 配置如下:
server: port: 9999 eureka: instance: hostname: 127.0.0.1 client: serviceUrl: defaultZone: http://${eureka.instance.hostname}:8080/eureka/ spring: application: name: gateway-server cloud: gateway: routes: # 路由的 ID,没有固定规则但要求唯一,建议配合服务名 # 产品微服务路由 - id: product-server # 根据服务名称从注册中心获取服务地址 uri: lb://product-server # 断言 predicates: - Path=/product/** # 配置过滤器 filters: - name: RequestRateLimiter args: # 令牌桶每秒填充速度,目前设置为1,为了演示效果 redis-rate-limiter.replenishRate: 1 # 令牌桶总容量,目前设置2,为了演示效果 redis-rate-limiter.burstCapacity: 2 # 使用 SpEl 表达式按名称引用 bean,注意对应的是 Bean 对象的方法名 key-resolver: "#{@pathKeyResolver}" # Redis 相关配置 redis: host: 192.168.0.105 port: 6379 password: 123456 database: 0 timeout: 10s lettuce: pool: max-active: 50 #最大连接数,默认是8 max-wait: 1000ms #最大连接阻塞数,单位毫秒,默认 max-idle: 100 #最大空闲连接,默认是8 min-idle: 5 #最小空闲连接,默认是0
创建 config 类:KeyResolverConfig,代码如下:
- package com.study.config;
-
- import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import reactor.core.publisher.Mono;
-
- /**
- * @author biandan
- * @description
- * @signature 让天下没有难写的代码
- * @create 2021-05-29 下午 5:56
- */
- @Configuration
- public class KeyResolverConfig {
-
- /**
- * 根据 URI 限流控制
- * @return
- */
- @Bean
- public KeyResolver pathKeyResolver(){
- //JDK 1.8 lambda 写法
- return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
- }
- }
注意:config 类的 pathKeyResolver 要对应 yml 配置文件的 SpEl 表达式获取 bean 的方法名。这个 Bean 使用的是 URI 限流策略。令牌桶我们设置的填充速度是1,总容量是2,为了方便测试。
重启 Gateway 服务,浏览器地址输入:http://127.0.0.1:9999/product/1?token=123 (因为我们之前的代码里增加了全局过滤器,需要判断 token=123 才会被执行,可以注释掉。)
1秒内点击2次以内,就会出现正常的情况:
1秒内超过2次的点击,就会出现 429 的错误。
说明我们的限流起到了效果。
这时候我们立即前往 Redis 查看,会看到有个限流而生成的 key(需要快速查看,否则容易过期)
在我们之前的配置类 KeyResolverConfig 增加下面的 Bean 注解。
- /**
- * 根据参数限流
- * @return
- */
- @Bean
- public KeyResolver parameterKeyResolver(){
- return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("token"));
- }
然后,把之前的根据 URI 的 Bean 注解注释掉,因为在配置类里只允许有一个 KeyResolver Bean。
如果 KeyResolver 有两个 Bean 注解,会报错:
然后,在 application.yml 配置文件里把 key-resolver 的值改成参数限流的函数名:
OK,重启 Gateway 服务,浏览器访问:http://127.0.0.1:9999/product/1?token=123
一秒内多次请求,也会出现 429 的错误,同时 Redis 里出现限流的信息:
做法跟上面一样,在配置类增加:
- /**
- * 根据IP限流
- * @return
- */
- @Bean
- public KeyResolver ipKeyResolver(){
- return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
- }
然后,在 application.yml 配置文件里把 key-resolver 的值改成 IP 限流的函数名:
重启 Gateway 服务,测试:http://127.0.0.1:9999/product/1?token=123
OK,Gateway 实现限流讲解到这。
部分理论内容参考链接:https://www.fangzhipeng.com/springcloud/2018/12/22/sc-f-gatway4.html
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。