高可用架构如何实现限流?
# 高可用架构如何实现限流?
# 一、简介
限流就是限制流量,监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性
# 1.1 限流的场景
可以考虑如下几种常见的场景:
- 限制某个接口一分钟内最多请求 100 次
- 限制某个用户的下载速度最多 100KB/S
- 限制某个用户同时只能对某个接口发起 5 路请求
- 限制某个 IP 来源禁止访问任何请求
其中最常见的两个限流场景是请求频率和并发量,他们对应的限流被称为 请求频率限流(Request rate limiting)和 并发量限流(Concurrent requests limiting)
# 1.2 限流的处理方式
拒绝服务 直接抛出异常
返回错误信息(比如返回 HTTP 状态码 429 Too Many Requests (opens new window)),或者给前端返回 302 重定向到一个错误页面,提示用户资源没有了或稍后再试
排队等待 将请求放到一个消息队列中
但是对于一些比较重要的接口不能直接拒绝,比如秒杀、下单等接口,我们既不希望用户请求太快,也不希望请求失败,这种情况一般会将请求放到一个消息队列中排队等待,消息队列可以起到削峰和限流的作用
服务降级 当触发限流条件时,直接返回兜底数据
比如查询商品库存的接口,可以默认返回有货。
# 二、常用的限流算法
# 2.1 固定窗口算法(Fixed Window)
规定了我们单位时间处理的请求数量。它将时间划分为多个窗口,在每个窗口内每有一次请求就将计数器加一,如果计数器超过了限制数量,则本窗口内所有的请求都被丢弃当时间到达下一个窗口时,计数器重置。
但是存在严重的临界问题
假如限制 1 秒内最多通过 5 个请求,在第一个窗口的最后半秒内通过了 5 个请求,第二个窗口的前半秒内又通过了 5 个请求。这样看来就是在 1 秒内通过了 10 个请求。
# 2.2 滑动窗口算法(Sliding Window)
为了解决固定窗口算法的临界问题,可以将时间窗口划分成更小的时间窗口,然后随着时间的滑动删除相应的小窗口,而不是直接滑过一个大窗口,这就是滑动窗口算法
例如我们的接口限流每分钟处理 60 个请求,我们可以把 1 分钟分为 60 个窗口。每隔 1 秒移动一次,每个窗口一秒只能处理 不大于
60(请求数)/60(窗口数)
的请求, 如果当前窗口的请求计数总和超过了限制的数量的话就不再处理其他请求。
很显然, 当滑动窗口的格子划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。
# 2.3 漏桶算法(Leaky Bucket)
除了计数器算法,另一个很自然的限流思路是将所有的请求缓存到一个队列中,然后按某个固定的速度慢慢处理
漏桶算法可以通过一个队列来实现,如下图所示:
当请求到达时,不直接处理请求,而是将其放入一个队列,然后另一个线程以固定的速率从队列中读取请求并处理,从而达到限流的目的。
注意的是这个队列可以有不同的实现方式,比如设置请求的存活时间,或将队列改造成 PriorityQueue,根据请求的优先级排序而不是先进先出。当然队列也有满的时候,如果队列已经满了,那么请求只能被丢弃了。
漏桶算法有一个缺陷,在处理突发流量时效率很低,于是人们又想出了下面的令牌桶算法。
# 2.4 令牌桶算法(Token Bucket)
是目前应用最广泛的一种限流算法,它的基本思想由两部分组成:生成令牌 和 消费令牌。
生成令牌:我们根据限流大小,按照一定的速率往桶里添加令牌。如果桶装满了,就不能继续往里面继续添加令牌了
假设有一个装令牌的桶,最多能装 M 个,然后按某个固定的速度(每秒 r 个)往桶中放入令牌,桶满时不再放入;
消费令牌:我们的每次请求都需要从桶中拿一个令牌才能放行,当桶中没有令牌时即触发限流,这时可以将请求放入一个缓冲队列中排队等待,或者直接拒绝;
在上面的图中,我们将请求放在一个缓冲队列中,可以看出这一部分的逻辑和漏桶算法几乎一模一样,只不过在处理请求上,一个是以固定速率处理,一个是从桶中获取令牌后才处理。
仔细思考就会发现,令牌桶算法有一个很关键的问题,就是桶大小的设置,正是这个参数可以让令牌桶算法具备处理突发流量的能力。譬如将桶大小设置为 100,生成令牌的速度设置为每秒 10 个,那么在系统空闲一段时间的之后(桶中令牌一直没有消费,慢慢的会被装满),突然来了 50 个请求,这时系统可以直接按每秒 50 个的速度处理,随着桶中的令牌很快用完,处理速度又会慢慢降下来,和生成令牌速度趋于一致。这是令牌桶算法和漏桶算法最大的区别,漏桶算法无论来了多少请求,只会一直以每秒 10 个的速度进行处理。当然,处理突发流量虽然提高了系统性能,但也给系统带来了一定的压力,如果桶大小设置不合理,突发的大流量可能会直接压垮系统。
# 三、单机限流
# 3.1 Google Guava
单机限流可以直接使用 Google Guava (opens new window) 自带的限流工具类 RateLimiter
。 RateLimiter
基于令牌桶算法,可以应对突发流量。
除了最基本的令牌桶算法(平滑突发限流)实现之外,Guava 的RateLimiter
还提供了 平滑预热限流 的算法实现。
平滑突发限流就是按照指定的速率放令牌到桶里,而平滑预热限流会有一段预热时间,预热时间之内,速率会逐渐提升到配置的速率。
我们下面通过两个简单的小例子来详细了解吧!
我们直接在项目中引入 Guava 相关的依赖即可使用。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
2
3
4
5
下面是一个简单的 Guava 平滑突发限流的 Demo。
import com.google.common.util.concurrent.RateLimiter;
public class RateLimiterDemo {
public static void main(String[] args) {
// 1s 放 5 个令牌到桶里也就是 0.2s 放 1个令牌到桶里
RateLimiter rateLimiter = RateLimiter.create(5);
for (int i = 0; i < 10; i++) {
double sleepingTime = rateLimiter.acquire(1);
System.out.printf("get 1 tokens: %ss%n", sleepingTime);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
输出:
get 1 tokens: 0.0s
get 1 tokens: 0.188413s
get 1 tokens: 0.197811s
get 1 tokens: 0.198316s
get 1 tokens: 0.19864s
get 1 tokens: 0.199363s
get 1 tokens: 0.193997s
get 1 tokens: 0.199623s
get 1 tokens: 0.199357s
get 1 tokens: 0.195676s
2
3
4
5
6
7
8
9
10
# 3.2 Bucket4j
Bucket4j (opens new window) 是一个非常不错的基于令牌/漏桶算法的限流库。Bucket4j 提供的限流功能相对于Guava 来说更加全面。不仅支持单机限流和分布式限流,还可以集成监控,搭配 Prometheus 和 Grafana 使用。
不过,毕竟 Guava 也只是一个功能全面的工具类库,其提供的开箱即用的限流功能在很多单机场景下还是比较实用的。
Spring Cloud Gateway 中自带的单机限流的早期版本就是基于 Bucket4j 实现的。后来,替换成了 Resilience4j。
# 3.3 Resilience4j
Resilience4j 是一个轻量级的容错组件,不仅提供限流,还提供了熔断、负载保护、自动重试等保障系统高可用开箱即用的功能。并且,Resilience4j 的生态也更好,很多网关都使用 Resilience4j 来做限流熔断的。
其灵感来自于 Hystrix。自Netflix 宣布不再积极开发 Hystrix (opens new window) 之后,Spring 官方和 Netflix 都更推荐使用 Resilience4j 来做限流熔断。
一般情况下,为了保证系统的高可用,项目的限流和熔断都是要一起做的。
因此,在绝大部分场景下 Resilience4j 或许会是更好的选择。如果是一些比较简单的限流场景的话,Guava 或者 Bucket4j 也是不错的选择。
# 四、分布式限流
分布式限流常见的方案:
使用中间件限流 :使用 Sentinel 或 Redis 来自己实现对应的限流逻辑。
网关层限流 :比较常用的一种方案,直接在网关层进行限流。
不过,通常网关层限流通常也需要借助到中间件/框架。就比如 Spring Cloud Gateway 的分布式限流实现
RedisRateLimiter
就是基于 Redis+Lua 来实现的,再比如 Spring Cloud Gateway 还可以整合 Sentinel 来做限流。
参考:何为限流?限流算法有哪些? (opens new window)、实战 Spring Cloud Gateway 之限流篇 (opens new window)