限流的背景:
在微服务中每个服务都是独立的,当系统频繁的请求时,比如秒杀业务,如果请求量大于系统承载量的时,如果系统不做任何的限量处理,就有可能在某段时间节点涌入大量的请求从而导致系统被压垮,根据上述情况我们就可以在网关内做限流,因为所有请求都需要先经过网关,通过网关把请求路由到具体的微服务中。
常见的限流算法
1. 计数器算法
以QPS(每秒查询率Queries-per-second)为100举例。从第一个请求开始计时。每次请求都会使计数器加一。当到达100以后,其他的请求都拒绝。如果1秒钟内前200ms请求数量已经到达了100,后面800ms中500次请求都被拒绝了,这种情况称为“突刺现象”
计数器算法是用来记录每秒访问量的,计数器算法不能限制单个客户端的并发请求。这样可能可能会被恶意攻击切计数器算法很难保证极限情况,这种算法的缺点是:无法解决对短时间节点内的突发流量。
2. 漏桶算法
漏桶算法可以解决突刺现象。
和生活中漏桶一样,有一个水桶,下面有一个”漏眼”往出漏水,不管桶里有多少水,漏水的速率都是一样的。但是既然是一个桶,桶里装的水都是有上限的。当到达了上限新进来的水就装不了(主要出现在突然倒进来大量水的情况)。在算法上实现可以准备一个队列来保存请求,另外通过一个线程池从队列中获取请求并执行,可以一次性获取多个请求并执。
3. 令牌桶算法
令牌桶算法可以说是对漏桶算法的一种改进。令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶用来存放固定数量的令牌,算法中存在一种机制,根据一定的速率往令牌桶中存放令牌,每请求时需要先获取令牌,只有拿到了令牌才有机会继续执行,否则选择等待可用的令牌,或者直接拒绝。
放令牌是一直持续进行的,如果令牌桶中的令牌达到了上限,就会丢弃这个令牌,例如桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,如果QPS为100,那么限流器初始化完成一秒后桶中就已经有了100个令牌了。当服务启动完成对外提供服务时,该限流器就可以抵挡瞬时的100个请求。只有桶中没有令牌时,请求才会进行等待,最后相当于一定的速率执行。
使用Gateway令牌桶算法实现限流
在SpringCloud项目中我们可以使用Gateway中的RequestRateLimiterGatewayFilterFactory实现限流,可以帮我们进行客户端的访问流量限制,他主要限制的不是上限信号量而是每个客户端时间节点内可访问的次数。例如时间节点为1秒。限制的是每个客户断在一秒内访问的次数,避免一个客户端在某段时间节点内大量请求。
RequestRateLimiterGatewayFilterFactory
RequestRateLimiterGatewayFilterFactory 算法工厂由代码提供,令牌桶由Redis提供,底层逻辑由lua脚本计算。RequestRateLimiterGatewayFilterFactory 中找到内部类:Config中有两大属性
KeyResolver:
keyResolver是实现KeyResolver接口的bean。在配置中,使用SpEL通过名称引用bean。#{@myKeyResolver}是引用名称为myKeyResolver的bean的SpEL表达式。
RateLimiter:
redis实现基于Stripe所做的工作。它需要使用spring-boot-starter-data-redis-reactive Spring Boot起动器。使用的算法是令牌桶算法。
redis-rate-limiter.replenishRate是您希望用户每秒允许多少个请求,而没有任何丢弃的请求。这是令牌桶被填充的速率
redis-rate-limiter.burstCapacity是允许用户在一秒钟内执行的最大请求数。这是令牌桶可以容纳的令牌数。将此值设置为零将阻止所有请求。
进入redisRateLimiter中查看,这两个信息可配置到yml文件中
Gateway项目搭建
搭建过程可参考:Gateway搭建过程
修改pom.xml
<?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>springcloud210915</artifactId>
<groupId>com.dp.springcould</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-gateway-gateway9527</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- gateway -->
<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</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.dp.springcould</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- json start -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
创建yml文件
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh1 #路由ID,没有固定规则但要求唯一,简易配合服务名
#uri: http://localhost:8001 #匹配后提供的路由地址
uri: lb://SPRINGCLOUD-PAYMENT-SERVICE # 注册中心服务应用名
predicates:
- Path=/payment/** #断言,路径相匹配的进行路由
- id: payment_routh #路由ID,没有固定规则但要求唯一,简易配合服务名
#uri: http://localhost:8001 #匹配后提供的路由地址
uri: lb://SPRINGCLOUD-PAYMENT-SERVICE # 注册中心服务应用名
predicates:
- Path=/payment/getPaymentInfo/** #断言,路径相匹配的进行路由
- id: payment_routh2
#uri: http://localhost:8001
uri: lb://SPRINGCLOUD-PAYMENT-SERVICE # 注册中心服务应用名
predicates:
- Path=/payment/lb/**
- Method=GET
# - Host=**.somehost.org,**.anotherhost.org
# - Header=X-RequesalipayCashOutOrBank0rWechatt-Id, \d+ #请求头要有X-Request-Id属性并且值为正数的正则表达式
# - Cookie=username,zzyy #键值对
# - After=2021-11-26T11:19:11.142+08:00[Asia/Shanghai] #意思: 在2021-11-26 11:19:11 之后才能正常访问
# - Before=2021-11-26T11:19:11.142+08:00[Asia/Shanghai]
# - Between=2021-11-26T11:19:11.142+08:00[Asia/Shanghai],2021-11-28T11:19:11.142+08:00[Asia/Shanghai]
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
#logging:
# level:
# root: debug
创建启动类
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class,args);
}
}
Gateway服务中实现限流
1.引入依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>20.1.0</version>
</dependency>
2.修改yml文件中的filters配置
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh1 #路由ID,没有固定规则但要求唯一,简易配合服务名
#uri: http://localhost:8001 #匹配后提供的路由地址
uri: lb://SPRINGCLOUD-PAYMENT-SERVICE # 注册中心服务应用名
predicates:
- Path=/payment/** #断言,路径相匹配的进行路由
filters:
#- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率 生产令牌速度,每秒多少个令牌
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
keyResolver: '#{@myKeyResolver}' # 使用SpringEL表达式,从Spring容器中找对象,并赋值。 '#{@beanName}'
- id: payment_routh #路由ID,没有固定规则但要求唯一,简易配合服务名
#uri: http://localhost:8001 #匹配后提供的路由地址
uri: lb://SPRINGCLOUD-PAYMENT-SERVICE # 注册中心服务应用名
predicates:
- Path=/payment1/getPaymentInfo/** #断言,路径相匹配的进行路由
- id: payment_routh2
#uri: http://localhost:8001
uri: lb://SPRINGCLOUD-PAYMENT-SERVICE # 注册中心服务应用名
predicates:
- Path=/payment1/lb/**
- Method=GET
# - Host=**.somehost.org,**.anotherhost.org
# - Header=X-RequesalipayCashOutOrBank0rWechatt-Id, \d+ #请求头要有X-Request-Id属性并且值为正数的正则表达式
# - Cookie=username,zzyy #键值对
# - After=2021-11-26T11:19:11.142+08:00[Asia/Shanghai] #意思: 在2021-11-26 11:19:11 之后才能正常访问
# - Before=2021-11-26T11:19:11.142+08:00[Asia/Shanghai]
# - Between=2021-11-26T11:19:11.142+08:00[Asia/Shanghai],2021-11-28T11:19:11.142+08:00[Asia/Shanghai]
redis:
host: 39.105.74.127
port: 7000
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
#logging:
# level:
# root: debug
创建MyKeyResolver类实现KeyResolver接口
package com.julang.springcloud.first;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class MyKeyResolver implements KeyResolver {
/**
* 1.返回值Mono<String> 泛型中的String 就代表令牌分给谁
* @param exchange
* @return
*/
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
String path = exchange.getRequest().getURI().getPath();
//根据ip限流
String hostAddress = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
return Mono.just(hostAddress);
}
}
参考文献:https://www.springcloud.cc/spring-cloud-greenwich.html