架构思维:限流技术深度解析

文章目录


Pre

架构思维:熔断机制深度解析

Java - 深入四大限流算法:原理、实现与应用

SpringBoot - 优雅的实现【流控】

Nginx - 请求\上传下载速率_流控小妙招


业务场景

在秒杀活动中,总计有 100 个特价商品,且每个商品的价格都非常低,活动计划于 10 月 10 日晚上 10 点 10 分 0 秒开启。

服务器架构图如下,所有客户端的 API 请求先进入 1 个 Nginx 层,再由 Nginx 层转发至网关层(Java,使用 Spring Gateway),最后转发至后台服务1(Java).

预测到秒杀开始那一瞬间会有海量用户涌入,致使系统无法处理所有用户请求,为保障服务器承受住大流量,我们只能通过限流的方式将部分流量放入后台服务中。

那什么是限流呢?一说到限流,有些人总喜欢把它与熔断混在一起谈,其实它们是有区别的。

--

熔断 VS 限流

  • 熔断一般发生在服务调用方,比如服务 A 需要调用服务 B,调用几次后发现服务 B 出现了问题且无法再调用,此时服务 A 必须立马触发熔断,在一段时间内不再调用服务 B。

  • 限流一般发生在服务被调用方,且主要在网关层做限流操作。比如一个网站 1 秒钟内后台服务只能处理 1 万个请求,这时突然涌入了 10 万个请求,我们该怎么办?此时,我们可以把 90% 的请求全部抛弃且不做处理,然后专心处理 10% 的请求,以此保证至少 1 万人能正常操作。

再回到业务场景中,这次我们的诉求是在某个层级通过限流的方式将秒杀活动的交易 TPS 控制在 100 笔/秒(因为秒杀活动总计 100 个库存,也就是说最终的交易只有 100 笔,而我们希望 100 笔交易在 1 秒内完成),此时应该怎么做呢?这就需要使用到限流的一些常用算法了。


4大限流算法

关于限流的算法总共分为固定时间窗口计数、滑动时间窗口计数、漏桶、令牌桶这 4 种

固定时间窗口计数

假设我们的诉求是每 5 秒钟,后台服务处理 500 个请求(以 5 秒为单位方便举例),那么每 5 秒钟我们就需要一个时间窗口来统计请求。

此时固定时间窗口计算算法看起来可以满足我们的诉求了,不过它会出现一个问题,我们细细分析下。

假设 1 秒至4 秒有 200 个请求过来,5 秒时有 300 个请求过来,6 秒至 9 秒有 499 个请求过来,10 秒时有 1 个请求过来,通过计算得知:1 秒至 5 秒总计 500 个请求,6 秒至 10 秒也是总计 500 个请求。

通过这种算法统计后,我们发现流量确实没有超出阈值。再仔细看看 5 秒~9 秒这个区间的请求数,它已经达到了 300+499=799 个,也就是说 5 秒~9 秒的请求数超标了 299 个,服务器明显扛不住了。

因此,固定时间窗口计数算法在现实中并不实用


滑动时间窗口计数

假设我们的诉求是在 1 秒内后台服务处理 100 个请求,滑动时间窗口计数算法是每 100 毫秒设置一个时间区间,每个时间区间统计该区间内的请求数量,然后每 10 个时间区间合并计算请求总数,请求数超出最大数量时就把多余的请求数据抛弃。当时间节点进入到下一个区间(比如第 11 个区间),我们便不再统计第 1 个区间的请求数量,而是将第 2 个区间~第 11 个区间的请求数量进行合并计算出一个总数,并以此类推,如下图所示。

虽然滑动时间窗口计数算法并不能保证每秒的统计请求数 100% 精准,但是可以大大减少单位时间内请求数超出阈值且检测不出来的概率。比如请求都堆积在前 100ms 的尾端与后 100ms 的首端,有可能出现请求数超出最大数量且不被发现的情况。

当然,我们可以将这个区间分得更细,比如设置 10 毫秒为一个区间。因为区间分得越细,计算数据就越精密,不过资源损耗也越多。

这个算法目前看来似乎已经能满足我们的需求了。不过,我们的场景是这样的:库存中只有 100 个商品,如果我们想把 TPS 控制在每秒 100 笔,将滑动时间窗口设置为 1 秒就行,即被分成 10 个区间,每个区间 100 毫秒,此时就会出现在第 1 个 100ms 请求已经超出了 100 个的情况,也就是说商品已经被秒光。

这时问题就来了,什么人能在 100ms 内完成点击购买、下单、提交订单整个流程?我只能说机器人可以做到,也就是说秒杀商品基本是给机器人准备的,这并不是我们想要的结果


漏桶

从上图中,我们可以看到漏桶算法的实现思路分为 3 个步骤:

  • 任意请求进来后直接进入漏桶排队;

  • 以特定的速率处理漏桶队列里面的请求;

  • 超出漏桶负载范围的新请求直接抛弃掉,无法进入排队队列。

结合上方在 1 秒内控制 100 个请求的例子,我们可以把输出速率设置为 100r/s(即每 10ms 处理一次请求),再把桶的大小设置为 100 。

因为漏桶算法是按照先进先出的原则处理请求,所以会出现最终被处理的请求还是前面那 100 个,这就与滑动时间窗口计数方法遇到的问题一样了

那如果我们把桶的大小设置为 1,不就可以达到我们的目的了。不过还有其他考虑,之后会进一步说明。


令牌桶

令牌桶算法的实现思路是这样的:

  • 按照特定的速率产生 tokens 并存放在令牌桶中,如果令牌桶满了,新的令牌不再产生;

  • 新进来的请求如果需要处理,则消耗桶中的一个令牌;

  • 如果桶中有令牌,直接消耗一个;

  • 如果桶中没有令牌,进入一个队伍中等待新的令牌;

  • 如果等待令牌的队伍满了,新请求就会直接被抛弃掉。

再结合上方在 1 秒内控制 100 个请求的例子,如果使用令牌桶算法,我们需要先把令牌的产生速率设置为 100/s,等待令牌的排队队列设为 0,这样就能够满足我们的秒杀限流的诉求了。

那令牌桶数量到底设置为多少呢?如果设置为 100,假设令牌在秒杀前已经产生,那么秒杀开始时请求数已经是 100 了,前 100 个请求就会被放行,也就是说机器人又抢到了所有商品

此时,我们可以设置令牌桶数量为 10,这样可以保证顶多 10 个机器人抢到商品


方案实现

使用令牌桶还是漏桶模式?

刚刚提到令牌桶算法与漏桶算法都可以满足我们的诉求,但是做限流时,我们希望这个算法不仅可以用于秒杀功能,还可以用于其他限流场景。

而使用漏桶算法存在一个缺陷:比如服务器空闲时,理论上服务器可以直接处理一次洪峰,但是漏桶的机制是请求处理速率恒定,因此,前期服务器资源只能根据恒定的漏水速率逐步处理请求,无法用于其他限流场景。

要是使用令牌桶算法就不存在这个问题了,因为我们可以把令牌桶一下子装满。因此,针对这个问题,我们最终使用的是令牌桶。

在 Nginx 中实现限流还是在网关层中实现限流?

在上述业务场景中,最终我们决定在网关层实现限流的原因有两点。

  • Nginx 中有一个限流插件,它可以对单个用户的请求数做限制,不过它基于漏桶算法;

  • 如果希望可以动态调整限流的相关配置,可以借助 Nginx+Lua ,实现配置管理


使用分布式限流还是单机限流?

如果使用单机限流的方式,我们需要提前算好服务器的数量,然后把 100/s 的 TPS 平分到各个服务器上进行一层换算。

如果使用分布式限流的方式,比如我们把令牌桶的数据存放在 Redis 中,即每次请求都需要访问 Redis,因秒杀开始时,下单的请求数往往很大,Redis 未必能承受住如此大的 QPS。

两害相权取其轻,最终我们决定使用单机限流的方式(根据自己的实际业务场景)。


使用哪个开源技术?

最终,我们使用开源库 Google-Guava 中的 RateLimiter 的相关类来实现限流,它是基于令牌桶算法的实现库。

在使用开源技术之前,我们需要先定义一个 filter,再使用 Guava 的 RateLimiter 对提交订单的 API 请求进行过滤。

在使用 RateLimiter 的过程中,我们需要设置如下 3 个配置项。

  • permitsPerSecond:每秒允许的请求数。

  • warmupPeriod:令牌桶多久满。

  • tryAcquire 的超时时间:当令牌桶为空时,可以等待新的令牌多久。

针对以上配置项,配置如下:

  • permitsPerSecond 设置为 100/10,100 代表想达到的 TPS,10 是代表网关节点 10 台;

  • warmupPeriod 设置为 100ms;

  • tryAcquire 的超时时间设置为 0,即拿不到令牌的请求直接抛弃掉,它无须等待。

permitsPerSecond 为 10,说明 1 秒可以产生 10 个令牌。warmupPeriod=100ms,代表从开始到令牌桶塞满需要 100ms,即令牌桶的大小是 1,如果我们有 10 台网关服务器,那么总令牌桶的大小就是 10。(前面我们提到过,为防止抢到物品的都是机器人,我们需要把令牌桶设置为 10。)


限流方案的注意事项

限流返回给客户端的错误代码

为了给用户带来好的体验,用户界面上尽量别出现错误,因此限流后被抛弃的请求应该返回一个特制的 HTTP CODE,供客户端进行特殊处理。

实时监控

在实际工作中,最好对限流日志随时做好记录并实时统计,这样有助于我们实时监控限流情况,一旦出现意外,可以及时处理。

实时配置

因为限流功能还需要应用到秒杀以外的场景,所以最好在配置中心就可以实现对令牌桶的动态管理+实时设置,这样也方便我们管理其他的限流场景。

其他场景的限流配置

在这次秒杀活动中,我们可以简单换算出控制在 100 的 TPS,而在平时的限流场景中,TPS 或 QPS(其他场景可能不使用 TPS)需要根据实际的压力测试结果来计算限流的正确配置。

相关推荐
TimeFine1 小时前
Android AI解放生产力(六)实战:解放页面开发前的繁琐工作
android·架构
语落心生2 小时前
边缘AI推理计算 - StarryOS RK3588 边缘AI系统架构深度解析(二):AArch64裸机启动与内存管理
架构
元气满满-樱2 小时前
LNMP架构实验部署
架构
BuffaloBit2 小时前
5G 核心网架构入门
网络协议·5g·架构
pengkai火火火3 小时前
基于springmvc拓展机制的高性能日志审计框架的设计与实现
spring boot·安全·微服务·架构
想用offer打牌4 小时前
数据库大事务有什么危害(面试版)
数据库·后端·架构
踏浪无痕4 小时前
别再只会用 Feign!手写一个 Mini RPC 框架搞懂 Spring Cloud 底层原理
后端·面试·架构
guslegend5 小时前
第2节:项目性能优化(中)
架构
Xの哲學5 小时前
Linux链路聚合深度解析: 从概念到内核实现
linux·服务器·算法·架构·边缘计算