流控作用
一般的做后台服务的,都会接触到流控,一般的场景就是在流量异常,比如遭受攻击的时候,保障服务不过载,在可支持的范围内提供稳定的服务。比如我们的服务支持100QPS,当一下子来了1000个请求的时候,我们在可服务的范围内,每秒处理100个请求,这样在牺牲一些响应时效性的时候,可以保证服务不会crash。
单机限流(guava的RateLimiter)
示例
Guava给我们提供了好用的流控工具,简单使用场景如下
private static RateLimiter rateLimiter = RateLimiter.create(5);
public static void main(String[] args) throws InterruptedException {
while (true) {
get(1);
}
}
private static void get(int permits) {
rateLimiter.acquire(permits);
System.out.println(System.currentTimeMillis());
}
运行这个简单的代码片段,从打印的时间戳可以看出来,每200ms打印一次,即正好控制QPS为5,同时保证稳定的速率。简单来说,就是当有大量请求进来的时候,限制请求的频率,维持其在一个稳定的区间。而其具体的方法,简单来说就是,根据上次处理的时间戳和允许的每秒允许的请求,来决定下次可以执行的时间。
原理
RateLimiter主要是利用了一个令牌桶的算法,系统以恒定的速率产生令牌(permit),当来一个请求的时候,会请求一个或者多个令牌,当且仅当系统有这么多个令牌的时候,请求才被允许执行,否则就一直等待令牌的生成。也就是以固定的频率向桶中放入令牌,例如一秒钟10枚令牌,实际业务在每次响应请求之前都从桶中获取令牌,只有取到令牌的请求才会被成功响应,获取的方式有两种:阻塞等待令牌或者取不到立即返回失败。
令牌同简单示意图
核心方法
方法说明
public static RateLimiter create(double permitsPerSecond)
该方法会创建一个RateLimiter实例,其每秒产生permitsPerSecond个令牌
public double acquire(int permits)
该方法是用于获取N个令牌的方法,如果系统内令牌不够,则一直等待直到有足够令牌可用
public boolean tryAcquire(int permits, Duration timeout)
该方法用户获取另外,如果在timeout时间内可以获取到足够的令牌,则等待,否则直接返回false
方法原理
- 保持分发的速率,以一定速率分发令牌,比如我们设置
permitsPerSecond
为500的话,则每2毫秒产生一个令牌 - 令牌会存储,若一定时间没有请求,可用令牌会存储下来,当然会有一个上限值,当下次来请求的时候,优先使用现有的存储的令牌
- 会有一个
nextFreeTicketMicros
来记录下次有可用令牌的时间戳,在这个时间之前,所有的请求均不能通过
guava依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
集群限流
下面介绍几种服务集群的限流方案:
Nginx 限流
Nginx 官方提供的限速模块使用的是 漏桶算法,保证请求的实时处理速度不会超过预设的阈值,主要有两个设置:
- limit_req_zone: 限制 IP 在单位时间内的请求数
- limit_req_conn: 限制同一时间链接数
Redis 限流
分布式服务接口限流,通常会结合Redis来做,根据 Redis 提供的 incr 命令,在规定的时间窗口,容许经过的最大请求数。例如如果要设置每1s只能通过的请求数,通常会使用redis incr再设置过期时间,例如使用的键值对业务标识:秒级时间戳,并使用incr命令,每来一次请求,就增加1,然后与规则进行对比,并为键设置过期时间,例如1分钟。
Incr 命令介绍:
Redis Incr 命令将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。且将key的有效时间设置为长期有效 。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。因为Redis没有专用的整数类型,所以在内存中是以字符串的形式存储的。
限流:利用redis的incr命令可以实现一般的限流操作。如限制某接口每秒请求次数上限1000次
/**
* 1秒内最大1000次
* @param key 可以设计为业务标识及接口标识+秒级时间戳组合。
* @param expireMillis 过期时间60s
* @return
*/
public Boolean limiter(String key, Long expireMillis) {
Long count = redisTemplate.opsForValue().increment(key, INCREMENT_STEP);
if (1 == count) {
redisTemplate.expire(key, expireMillis, TimeUnit.SECONDS);
}
if (count > 1000) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
increment函数解释:在key下的value整型数基础上加delta值。
/**
* Increment an integer value stored as string value under {@code key} by {@code delta}.
*
* @param key must not be {@literal null}.
* @param delta
* @return {@literal null} when used in pipeline / transaction.
* @see <a href="https://redis.io/commands/incrby">Redis Documentation: INCRBY</a>
*/
@Nullable
Long increment(K key, long delta);
分布式滑动窗口限流
Kong 官方提供了一种分布式滑动窗口算法的设计, 目前支持在 Kong 上作集群限流配置。它经过集中存储每一个滑动窗口和 consumer 的计数,从而支持集群场景。这里推荐一个 Go 版本的实现: slidingwindow
其余
另外业界在分布式场景下,也有 经过 Nginx+Lua 和 Redis+Lua 等方式来实现限流
总结
本文主要在学习和调研高并发场景下的限流方案的总结。目前业界流行的限流算法包括计数器、漏桶、令牌桶和滑动窗口, 每种算法都有本身的优点,实际应用中能够根据本身业务场景作选择,而分布式场景下的限流方案,也基本经过以上限流算法来实现。在高并发下流量控制的一个原则是:先让请求先到队列,并作流量控制,不让流量直接打到系统上。