网关限流与动态路由
一、限流
为什么要限流
互联网项目与传统项目不同的互联网项目是暴露在互联网中,面向的是全体网民,可能会出现
1)大量的请求使服务器过载
2)恶意用户高频访问导致服务器宕机
3)网页爬虫 ,对于这些情况我们需要对用户的访问进行限流访问
如何限流
下面列举三种常见的限流算法
计数器算法
使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。在周期范围类(1s)如果第一时间(10ms)就已经达到了阈值,则剩余的时间(990ms)会把所有请求拒绝掉。(周期范围不一定是1s,这里仅举例说明)
漏桶算法
当请求进入服务器时,先将其放入当容器中,然后服务器以一定的速率取处理容器中的请求,当容器中请求满了过后(出发限流策略),则会将后面的请求给丢弃掉。


图片出自百度百科
令牌桶算法
服务器会以一定的速率将令牌放入桶中,请求进入,先获取令牌,获取到令牌则请求通过,否则拒绝请求。
图片出自百度百科
漏桶算法流入的请求量是不固定的,无法处理短时间的突发流量;
令牌桶算法,令牌的流入固定,可以存放在桶中,可以处理短时间的突发流量
Gateway 实现令牌桶算法
Spring Cloud Gateway官方提供了RequestRateLimiterGatewayFilterFactory这个类,适用Redis和lua脚本实现了令牌桶的方式。
本文仅测试redis单机情况下。
引入依赖
<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>
复制代码
定义KeyResolver
//注入bean
@Bean
public KeyResolver ipKeyResolver(){
return (ServerWebExchange exchange) -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
复制代码
application.yml
server:
port: 9000 # 指定网关服务的端口
spring:
application:
name: api-gateway
cloud:
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: cookie_route
uri: http://localhost:8087
predicates:
- Path=/tool/**
filters:
- name: RequestRateLimiter
args:
# 用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
key-resolver: '#{@ipKeyResolver}'
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 3 # 令牌桶总容量
复制代码
启动项目,然后使用jmeter测试
可以看见在一秒请求一次的情况下,三个线程只有在桶中装满三个令牌的时候才能全部请求成功,后续请求因为每秒装一个令牌进桶中,所以后续都只会有一个请求成功。
动态路由
作为微服务的统一入口,不能每次有路由配置修改就重启网关服务,动态路由就是在服务运行时可以添加路由配置,且不用重启服务就能生效。下面实例中使用redis将新增的路由信息持久化。
Springcloud 还提供了 RouteDefinitionWriter 可以直接使用的新增/删除路由的接口,直接调用就可以了,但是所有新增的路由都在内存中,并没有持久化,服务重启过后就没有了。所以我们可以自己实现 RouteDefinitionRepository
官方默认提供了这些接口进行网关的管理
在源码中可以看到接口都是result风格开发,所以调用哪个接口通过请求方式(POST/GET/DELETE)区分,调用格式为 http://ip:port/actuator/gateway/routes/{param}
首先引入依赖
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
复制代码
在配置文件中暴露所有端点
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
复制代码
实现RouteDefinitionRepository接口
/**
* 自定义路由持久化
*/
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
private final static String GATETORY_ROUTER_KEY = "gateway_dynamic_route";
// 本地启动redis服务
@Autowired
private RedisTemplate redisTemplate;
// 获取路由信息
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
List<RouteDefinition> routeDefinitions = new ArrayList<RouteDefinition>();
// 从 redis 中获取路由信息
redisTemplate.opsForHash().values(GATETORY_ROUTER_KEY).stream().forEach(route -> {
routeDefinitions.add(JSON.parseObject(route.toString(),RouteDefinition.class));
});
return Flux.fromIterable(routeDefinitions);
}
// 保存路由信息
@Override
public Mono<Void> save(Mono<RouteDefinition> route) {
return route.flatMap(routeDefinition -> {
// 将路由转为 json 字符串存储到 redis
redisTemplate.opsForHash()
.put(GATETORY_ROUTER_KEY,routeDefinition.getId(),JSON.toJSONString(routeDefinition));
return Mono.empty();
});
}
// 删除路由信息
@Override
public Mono<Void> delete(Mono<String> routeId) {
return routeId.flatMap(id -> {
// 如果包含路由 id
if(redisTemplate.opsForHash().hasKey(GATETORY_ROUTER_KEY,id)){
redisTemplate.opsForHash().delete(GATETORY_ROUTER_KEY,id);
return Mono.empty();
}
// 没有找到要删除的路由 id 抛出异常
return Mono.defer(()->Mono.error(new Exception("routeDefinition not found:"+id)));
});
}
}
复制代码
需要注意
不管是新增接口还是删除接口调用完过后都需要调用刷新路由的接口
POST请求 http://localhost:8080/actuator/refresh
POST请求 调用新增接口
localhost:8080/actuator/gateway/routes/jd_router
json格式的参数
{
"id":"jd_router",
"uri":"https://www.jd.com/",
"order":"0",
"filters":[
{
"name":"StripPrefix",
"args":{
"_genkey_0":"1"
}
}
],
"predicates":[
{
"name":"Path",
"args":{
"_genkey_0":"/jd/**"
}
}
]
}
复制代码
刷新过后可以看到如下信息
DELETE 请求删除接口
localhost:8080/actuator/gateway/routes/jd_router
刷新过后可以看到路由信息已经为空了
完!
下面是所有的依赖信息,需要的可以复制下
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>2020.0.4</spring-cloud.version>
</properties>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
复制代码