计数器是最简单的限流算法,思路是维护一个单位时间内的计数器 Counter,如判断单位时间已经过去,则将计数器归零。
我们假设有个需求对于某个接口 /query 每分钟最多允许访问 200 次。
滑动窗口(Sliding window)(https://en.wikipedia.org/wiki/Sliding_window_protocol) 是一种流量控制技术,这个词出现在 TCP 协议中。我们来看看在限流中它是怎样表现的:
上图中我们用红色的虚线代表一个时间窗口(一分钟),每个时间窗口有 6 个格子,每个格子是 10 秒钟。每过 10 秒钟时间窗口向右移动一格,可以看红色箭头的方向。我们为每个格子都设置一个独立的计数器 Counter,假如一个请求在 0:45 访问了那么我们将第五个格子的计数器 +1(也是就是 0:40~0:50),在判断限流的时候需要把所有格子的计数加起来和设定的频次进行比较即可。
我再来回顾一下刚才的计数器算法,我们可以发现,计数器算法其实就是滑动窗口算法。只是它没有对时间窗口做进一步地划分,所以只有1格。
由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
从图中我们可以看到,整个算法其实十分简单。首先,我们有一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。
我们将算法中的水换成实际应用中的请求,我们可以看到漏桶算法天生就限制了请求的速度。当使用了漏桶算法,我们可以保证接口会以一个常速速率来处理请求。
漏桶算法有以下特点:
漏桶限制的是常量流出速率(即流出速率是一个固定常量值),所以最大的速率就是出水的速率,不能出现突发流量。
令牌桶算法和漏桶算法的方向刚好是相反的,我们有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的,token以 一个固定的速率r往桶里填充,直到达到桶的容量,多余的令牌将会被丢弃。每当一个请求过来时,就会尝试从桶里移除一个令牌,如果没有令牌的话,请求无法通过。
令牌桶有以下特点:
令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量。
计数器和滑动窗口比较
计数器算法实现起来最简单,可以看成是滑动窗口的低精度实现。滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。也就是说,如果滑动窗口的精度越高,需要的存储空间就越大。
漏桶算法和令牌桶算法比较
|
漏桶 |
令牌桶 |
---|---|---|
何时拒绝请求 | 流入请求速率任意,以固定的速率流出请求,流入请求数超过漏桶容量,拒绝请求 | 以固定速率往桶中添加令牌,桶中无令牌则拒绝请求 |
速率限制 | 限制常量流出速率,从而平滑突发流入速率 | 限制平均流入速率,允许一定程度的突发请求(允许一次拿多个令牌) |
开启zuul服务限流的组件,包含五种内置的限流方式:
限流方式 |
说明 |
|
|
---|---|---|---|
Authenticated User | 使用经过身份验证的用户名或“anonymous匿名” |
|
|
Request Origin | 使用用户原始请求(通过客户端IP地址区分) |
|
|
URL | 使用服务的请求路径 | ||
ROLE | 使用经过身份验证的用户角色 | ||
Request method | 使用HTTP请求方法 | ||
Global configuration per service | 这个不验证请求Origin,Authenticated User或URI,要使用这个,请不要设置type |
只需向列表中添加多个值,就可以将经过身份验证的用户、请求源、URL、角色和请求方法组合在一起
添加ratelimit依赖
com.marcosbarbero.cloud
spring-cloud-zuul-ratelimit
LATEST
使用数据存储不同,则需引入不同依赖
Redis:
org.springframework.boot
spring-boot-starter-data-redis
Consul:
org.springframework.cloud
spring-cloud-starter-consul
Spring Data JPA:
org.springframework.boot
spring-boot-starter-data-jpa
Bucket4j JCache:
com.github.vladimir-bukhtoyarov
bucket4j-core
com.github.vladimir-bukhtoyarov
bucket4j-jcache
javax.cache
cache-api
Bucket4j Hazelcast (depends on Bucket4j JCache):
com.github.vladimir-bukhtoyarov
bucket4j-hazelcast
com.hazelcast
hazelcast
Bucket4j Infinispan (depends on Bucket4j JCache):
com.github.vladimir-bukhtoyarov
bucket4j-infinispan
org.infinispan
infinispan-core
Bucket4j Ignite (depends on Bucket4j JCache):
com.github.vladimir-bukhtoyarov
bucket4j-ignite
org.apache.ignite
ignite-core
配置示例:
zuul:
ratelimit:
key-prefix: your-prefix #限流key前缀
enabled: true #是否启用限流
repository: REDIS #使用何种方式存储数据
behind-proxy: true
add-response-headers: true
default-policy-list: #optional - will apply unless specific policy exists 默认策略 (60s内超过10次或请求时间累积超过1000s触发限流)
- limit: 10 #optional - request number limit per refresh interval window 单位时间内请求次数限制
quota: 1000 #optional - request time limit per refresh interval window (in seconds) 单位时间内累计请求时间限制(秒)
refresh-interval: 60 #default value (in seconds) 限流时间窗口,默认60s
type: #optional 限流方式
- user
- origin
- url
- httpmethod
policy-list: # 自定义策略
myServiceId: #本例配置:60s内超过10次,请求时间累积超过1000s触发限流
- limit: 10 #optional - request number limit per refresh interval window 单位时间内请求次数限制
quota: 1000 #optional - request time limit per refresh interval window (in seconds) 单位时间内累计请求时间限制(秒)
refresh-interval: 60 #default value (in seconds) 限流时间窗口,默认60s
type: #optional 限流方式
- user
- origin
- url
- type: #optional value for each type
- user=anonymous
- origin=somemachine.com
- url=/api #url prefix
- role=user
- httpmethod=get #case insensitive
实现 |
说明 |
---|---|
|
基于本地内存,默认,使用currentHashMap存储key值 |
redis | 基于redis,使用时必须引入redis相关依赖 |
JPA | 基于SpringDataJPA,需要用到数据库 |
consul | 基于consul |
BUKET4J | 使用一个Java编写的基于令牌桶算法的限流库 |
1.7.1.RELEASE
2.2.6.RELEASE
zuul.ratelimit. :配置项
配置项 |
可选项 |
说明 |
|
|
---|---|---|---|---|
enabled | Boolean | 是否启用限流 | ||
behind-proxy | true/false | 默认false,是否是代理之后的请求,type=origin,影响ip取值 |
|
|
add-response-headers | true/false | 默认true,是否添加响应头 | X-RateLimit-Limit: 60//每秒60次请求 X-RateLimit-Remaining: 23//当前还剩下多少次 X-RateLimit-Reset: 1540650789//限制重置时间 |
|
key-prefix | String | 限流key前缀(默认${spring.application.name:rate-limit-application}) | ||
repository | CONSUL(K/V存储), REDIS, JPA, BUCKET4J_JCACHE, BUCKET4J_HAZELCAST, BUCKET4J_INFINISPAN, BUCKET4J_IGNITE, IN_MEMORY | 限流数据的存储方式,默认是:IN_MEMORY(内存) | BUCKET4J 基于令牌算法 | |
default-policy | list-of-policy | 默认策略 | ||
policy-list | Map of Lists of Policy | 自定义策略 | ||
postFilterOrder | int | postFilter(后置)过滤顺序 | FilterConstants.SEND_RESPONSE_FILTER_ORDER - 10 | |
preFilterOrder | int | preFilter(前置)过滤顺序 | FilterConstants.FORM_BODY_WRAPPER_FILTER_ORDER |
policy(策略配置项):
配置项 |
说明 |
默认值 |
---|---|---|
limit | 刷新窗口期 限流调用次数阈值 | |
quota | 刷新窗口期 所有的请求的总时间 限制(秒) | |
refresh-interval |
刷新窗口期 | 60s |
type | 限流方式:ORIGIN, USER, URL,ROLE,HTTP_METHOD |
@RequiredArgsConstructor
public class CustomRateLimitKeyGenerator implements RateLimitKeyGenerator {
private final RateLimitProperties properties;
private final RateLimitUtils rateLimitUtils;
@Override
public String key(final HttpServletRequest request, final Route route, final Policy policy) {
final List types = policy.getType().stream().map(MatchType::getType).collect(Collectors.toList());
final StringJoiner joiner = new StringJoiner(":");
joiner.add(properties.getKeyPrefix());
if (route != null) {
joiner.add(route.getId());
}
if (!types.isEmpty()) {
if (types.contains(Type.URL) && route != null) {
joiner.add(route.getPath());
}
if (types.contains(Type.ORIGIN)) {
joiner.add(rateLimitUtils.getRemoteAddress(request));
}
if (types.contains(Type.USER)) {
joiner.add(rateLimitUtils.getUser(request));
}
}
return joiner.toString();
}
}
RateLimitProperties加载前缀:zuul.ratelimit的配置
@Data
@Validated
@RefreshScope
@NoArgsConstructor
@ConfigurationProperties(RateLimitProperties.PREFIX)
public class RateLimitProperties {
public static final String PREFIX = "zuul.ratelimit";
redis\jpa\consul 用的是计数器的方式实现限流,不同的限流策略生成一个限流key,计算剩余的限流数据等存入rate,key–rate 一一对应,请求进来后由key查询是否已有rate,没有则生成rate保存,后置filter更新剩余请求次数、请求耗时
public class Rate {
@Id
@Column(name = "rate_key")
private String key;
/**
* 剩余的请求数
*
*/
private Long remaining;
/**
* 剩余的请求耗时
*
*/
private Long remainingQuota;
/**
*
*
*/
private Long reset;
/**
* 过期时间
*
*/
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy HH:mm:ss")
private Date expiration;
}
取得配置的Route信息,策略集合,遍历集合,生成限流key,创建rate(保存或更新),比较limit调用次数,请求时间是否超过阈值
计算请求耗时,更新key值对应的 限流rate的请求时间阈值