流控作用
一般的做后台服务的,都会接触到流控,一般的场景就是在流量异常,比如遭受攻击的时候,保障服务不过载,在可支持的范围内提供稳定的服务。比如我们的服务支持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依赖:
com.google.guava guava18.0
集群限流
下面介绍几种服务集群的限流方案:
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次
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值。
@Nullable Long increment(K key, long delta);
分布式滑动窗口限流
Kong 官方提供了一种分布式滑动窗口算法的设计, 目前支持在 Kong 上作集群限流配置。它经过集中存储每一个滑动窗口和 consumer 的计数,从而支持集群场景。这里推荐一个 Go 版本的实现: slidingwindow
其余
另外业界在分布式场景下,也有 经过 Nginx+Lua 和 Redis+Lua 等方式来实现限流
总结
本文主要在学习和调研高并发场景下的限流方案的总结。目前业界流行的限流算法包括计数器、漏桶、令牌桶和滑动窗口, 每种算法都有本身的优点,实际应用中能够根据本身业务场景作选择,而分布式场景下的限流方案,也基本经过以上限流算法来实现。在高并发下流量控制的一个原则是:先让请求先到队列,并作流量控制,不让流量直接打到系统上。