6 种限流实现方案!(纯干货)优雅又不失强悍

2022-12-25 0 895

为了上班方便,去年我把自己在南郊的房子买下来了,搬到了南郊,这种离我上班的地方设点了,它为我节省了许多的时间成本,我能用它来做许多有意义的事,起码不会即使交通堵塞而郁闷了,安全感直角上升。

但即使这种,日常生活也有其他的苦恼。南郊的居住密度比较大,因而停放就成了恶心的事,我租的是路两边的非固定停放位,每次只要上班回来,一定是没有停放位停了,因而我根本无法和别人的车中间CT200H,但这种增添的问题是,我每天早上都要被挪车的电话给惊醒,心情自然就别说了。

但后来几天,我就慢慢变精明了,我A43EI267SD晚上停放的这时候,会找隔天尾号的车中间CT200H,这种我隔天就不用发酒疯了,这真是尾号给我增添的“非常大增量”啊。

而车辆尾号是一种日常生活中很常见的开闭策略,他除了给我增添了以上的益处之外,送给他们幸福的日常生活环境增添了一丝改善,并且稳步增长的的士已经给他们的交通增添了非常大的“负担”,如果再不尾号,可能所有的车都要被堵在路上,这是开闭给他们的日常生活增添的非常大益处。

从日常生活回到程序中,假定一个掌控系统根本无法为 10W 人提供更多服务,突然有一天即使某一热点事件,造成了掌控系统短时期内的用户数量迅速增加到了 50W,那么导致的直接结果是掌控系统崩盘,其他人都无法用掌控系统了,显然只有少人数能用相比而言其他人都无法用更符合他们的预期,因而这个这时候他们要使用「开闭」了。

开闭进行分类

开闭的同时实现计划有许多种,磊哥这里稍稍理了一下,开闭的进行分类如下右图:

正当性校正开闭:比如说校正码、IP 白名单等,这些方式能有效的防止蓄意攻击和食腐收集;罐子开闭:比如说 Tomcat、Nginx 等开闭方式,其中 Tomcat 能设置最小缓存数(maxThreads),当mammalian超过最小缓存数会排队等候等待执行;而 Nginx 提供更多了三种开闭方式:一是掌控速度,二是掌控mammalian通话量;服务端开闭:比如说他们在服务端通过开闭演算法同时实现开闭,该项也是他们责任编辑介绍的重点项目。

正当性校正开闭为最常规性的业务代码,是普通的接收者和 IP 白名单掌控系统,责任编辑就不做过多的描述了,他们重点项目来看b0d3fb三种开闭的同时实现计划:罐子开闭和服务端开闭。

罐子开闭

Tomcat 开闭

Tomcat 8.5 版的最小缓存数在 conf/server.xml 配置中,如下右图:

其中 maxThreads 是 Tomcat 的最小缓存数,当请求的mammalian大于此值(maxThreads)时,请求就会排队等候执行,这种就完成了开闭的目的。

小贴士:maxThreads 的值能适当的调大一些,此值默认为 150(Tomcat 版 8.5.42),但这个值也不是越大越好,要看具体的硬件配置,需要注意的是每开启一个缓存需要耗用 1MB 的 JVM 内存空间用于作为缓存栈之用,并且缓存越多 GC 的负担也越重。最后需要注意一下,操作掌控系统对于进程中的缓存数有一定的限制,Windows 每个进程中的缓存数不允许超过 2000,Linux 每个进程中的缓存数不允许超过 1000。

Nginx 开闭

Nginx 提供更多了三种开闭方式:一是掌控速度,二是掌控mammalian通话量。

掌控速度

他们需要使用 limit_req_zone 用来限制单位时间内的请求数,即速度限制,示例配置如下:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit; } }

以上配置表示,限制每个 IP 访问的速度为 2r/s,即使 Nginx 的开闭统计是基于毫秒的,他们设置的速度是 2r/s,转换一下是 500ms 内单个 IP 只允许通过 1 个请求,从 501ms 开始才允许通过第 2 个请求。

他们使用单 IP 在 10ms 内发mammalian送了 6 个请求的执行结果如下:

6 种限流实现方案!(纯干货)优雅又不失强悍

从以上结果能看出他的执行符合他们的预期,只有 1 个执行成功了,其他的 5 个被拒绝了(第 2 个在 501ms 才会被正常执行)。

速度限制升级版

上面的速度掌控虽然很精准但是应用于真实环境未免太苛刻了,真实情况下他们应该掌控一个 IP 单位总时间内的总访问次数,而不是像上面那么精确但毫秒,他们能使用 burst 关键字开启此设置,示例配置如下:

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s; server { location / { limit_req zone=mylimit burst=4; } }

burst=4 表示每个 IP 最多允许4个突发请求,如果单个 IP 在 10ms 内发送 6 次请求的结果如下:

6 种限流实现方案!(纯干货)优雅又不失强悍

从以上结果能看出,有 1 个请求被立即处理了,4 个请求被放到 burst 队列里排队等候执行了,另外 1 个请求被拒绝了。

掌控mammalian数

利用 limit_conn_zone 和 limit_conn 两个指令即可掌控mammalian数,示例配置如下:

limit_conn_zone $binary_remote_addr zone=perip:10m; limit_conn_zone $server_name zone=perserver:10m; server { … limit_conn perip 10; limit_conn perserver 100; }

其中 limit_conn perip 10 表示限制单个 IP 同时最多能持有 10 个连接;limit_conn perserver 100 表示 server 同时能处理mammalian连接的总数为 100 个。

小贴士:只有当 request header 被后端处理后,这个连接才进行计数。

服务端开闭

服务端开闭需要配合开闭的演算法来执行,而演算法相当于执行开闭的“大脑”,用于指导限制计划的同时实现。

有人看到「演算法」两个字可能就晕了,觉得很深奥,其实并不是。演算法就相当于操作某一事务的具体同时实现步骤汇总,其实并不难懂,不要被它的表象给吓到哦~

开闭的常见演算法有以下三种:

时间窗口演算法漏桶演算法令牌演算法

接下来他们分别看来。

1.时间窗口演算法

所谓的滑动时间演算法指的是以当前时间为截止时间,往前取一定的时间,比如说往前取 60s 的时间,在这 60s 之内运行最小的访问数为 100,此时演算法的执行逻辑为,先清除 60s 之前的所有请求记录,再计算当前集合内请求数量是否大于设定的最小请求数 100,如果大于则执行开闭拒绝策略,否则插入本次请求记录并返回能正常执行的标识给客户端。

滑动时间窗口如下图右图:

6 种限流实现方案!(纯干货)优雅又不失强悍

其中每一小个表示 10s,被红色虚线包围的时间段则为需要判断的时间间隔,比如说 60s 秒允许 100 次请求,那么红色虚线部分则为 60s。

他们能借助 Redis 的有序集合 ZSet 来同时实现时间窗口演算法开闭,实现的过程是先使用 ZSet 的 key 存储开闭的 ID,score 用来存储请求的时间,每次有请求访问来了之后,先清空之前时间窗口的用户数量,统计现在时间窗口的个数和最小允许用户数量对比,如果大于等于最大用户数量则返回 false 执行开闭操作,负责允许执行业务逻辑,并且在 ZSet 中添加一条有效的访问记录,具体同时实现代码如下。

他们借助 Jedis 包来操作 Redis,同时实现在 pom.xml 添加 Jedis 框架的引用,配置如下:

<!– https://mvnrepository.com/artifact/redis.clients/jedis

–> redis.clients jedis 3.3.0

具体的 Java 同时实现代码如下:

import redis.clients.jedis.Jedis; public class RedisLimit { // Redis 操作客户端 static Jedis jedis = new Jedis(“127.0.0.1”, 6379); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 15; i++) { boolean res = isPeriodLimiting(“java”, 3, 10); if (res) { System.out.println(“正常执行请求:” + i); } else { System.out.println(“被开闭:” + i); } } // 休眠 4s Thread.sleep(4000); // 超过最小执行时间之后,再从发起请求 boolean res = isPeriodLimiting(“java”, 3, 10); if (res) { System.out.println(“休眠后,正常执行请求”); } else { System.out.println(“休眠后,被开闭”); } } /** * 开闭方法(滑动时间演算法) * @param key 限流标识 * @param period 开闭时间范围(单位:秒) * @param maxCount 最小运行访问次数 * @return */ private static boolean isPeriodLimiting(String key, int period, int maxCount) { long nowTs = System.currentTimeMillis(); // 当前时间戳 // 删除非时间段内的请求数据(清除老访问数据,比如说 period=60 时,标识清除 60s 以前的请求记录) jedis.zremrangeByScore(key, 0, nowTs – period * 1000); long currCount = jedis.zcard(key); // 当前请求次数 if (currCount >= maxCount) { // 超过最小请求次数,执行开闭 return false; } // 未达到最小请求数,正常执行业务 jedis.zadd(key, nowTs, “” + nowTs); // 请求记录 +1 return true; } }

以上程序的执行结果为:

正常执行请求:0

正常执行请求:1

正常执行请求:2

正常执行请求:3

正常执行请求:4

正常执行请求:5

正常执行请求:6

正常执行请求:7

正常执行请求:8

正常执行请求:9

被开闭:10

被开闭:11

被开闭:12

被开闭:13

被开闭:14

休眠后,正常执行请求

此同时实现方式存在的缺点有两个:

使用 ZSet 存储有每次的访问记录,如果数据量比较大时会占用大量的空间,比如说 60s 允许 100W 访问时;此代码的执行非原子操作,先判断后增加,中间空隙可穿插其他业务逻辑的执行,最终导致结果不准确。

2.漏桶演算法

漏桶演算法的灵感源于漏斗,如下图右图:

6 种限流实现方案!(纯干货)优雅又不失强悍

滑动时间演算法有一个问题是在一定范围内,比如说 60s 内根本无法有 10 个请求,当第一秒时就到达了 10 个请求,那么剩下的 59s 根本无法把所有的请求都给拒绝掉,而漏桶演算法能解决这个问题。

漏桶演算法类似于日常生活中的漏斗,无论上面的水流倒入漏斗有多大,也就是无论请求有多少,它都是以均匀的速度慢慢流出的。当上面的水流速度大于下面的流出速度时,漏斗会慢慢变满,当漏斗满了之后就会丢弃新来的请求;当上面的水流速度小于下面流出的速度的话,漏斗永远不会被装满,并且能一直流出。

演算法。

上面他们演示 Nginx 的掌控速度其实使用的是漏桶演算法,当然他们也能借助 Redis 很方便的同时实现漏桶演算法。

他们能使用 Redis 4.0 版中提供更多的 Redis-Cell 模块,该模块使用的是漏斗演算法,并且提供更多了原子的开闭指令,而且依靠 Redis 这个天生的分布式程序就能同时实现比较完美的开闭了。

Redis-Cell 同时实现开闭的方法也很简单,只需要使用一条指令 cl.throttle 即可,使用示例如下:

nteger)14 # 漏斗剩余容量 4)(integer)-1 # 被拒绝之后,多长时间之后再试(单位:秒)-1 表示无需重试 5)(integer)2 # 多久之后漏斗完全空出来

其中 15 为漏斗的容量,30 / 60s 为漏斗的速度。

3.令牌演算法

,如下图右图:

6 种限流实现方案!(纯干货)优雅又不失强悍

他们能使用 Google 开源的 guava 包,很方便的同时实现令牌桶演算法,首先在 pom.xml 添加 guava 引用,配置如下:

<!– https://mvnrepository.com/artifact/com.google.guava/guava

–> com.google.guava guava 28.2-jre

具体同时实现代码如下:

import com.google.common.util.concurrent.RateLimiter; import java.time.Instant; /** * Guava 同时实现开闭 */ public class RateLimiterExample { public static void main(String[] args) { // 每秒产生 10 个令牌(每 100 ms 产生一个) RateLimiter rt = Rastem.out.println(“正常执行方法,ts:” + Instant.now()); }).start(); } } }

以上程序的执行结果为:

正常执行方法,ts:2020-05-15T14:46:37.175Z

正常执行方法,ts:2020-05-15T14:46:37.237Z

正常执行方法,ts:2020-05-15T14:46:37.339Z

正常执行方法,ts:2020-05-15T14:46:37.442Z

正常执行方法,ts:2020-05-15T14:46:37.542Z

正常执行方法,ts:2020-05-15T14:46:37.640Z

正常执行方法,ts:2020-05-15T14:46:37.741Z

正常执行方法,ts:2020-05-15T14:46:37.840Z

正常执行方法,ts:2020-05-15T14:46:37.942Z

正常执行方法,ts:2020-05-15T14:46:38.042Z

正常执行方法,ts:2020-05-15T14:46:38.142Z

注意:使用 guava 同时实现的令牌演算法属于程序级别的单机开闭计划,而上面使用 Redis-Cell 的是分布式的开闭计划。

总结

责任编辑提供更多了 6 种具体的同时实现开闭的方式,他们分别是:Tomcat 使用 maxThreads 来同时实现开闭;Nginx 提供更多了三种开闭方式,一是通过 limit_req_zone 和 burst 来同时实现速度开闭,二是通过 limit_conn_zone 和 limit_conn 两个指令掌控mammalian连接的总数。最后他们讲了时间窗口演算法借助 Redis 的有序集合能同时实现,还有漏桶演算法能使用 Redis-Cell 来同时实现,以及令牌演算法能解决 Google 的 guava 包来同时实现。

需要注意的是借助 Redis 同时实现的开闭计划可用于分布式掌控系统,而 guava 同时实现的开闭根本无法应用于单机环境。如果你嫌弃服务端开闭麻烦,甚至能在不改代码的情况下直接使用罐子开闭(Nginx 或 Tomcat),但前提是能满足你的业务需求。

好了,文章到这里就结束了,期待他们下期再会~

最后的话

原创不易,如果觉得责任编辑对你有用,请随手点击一个「赞」,这是对作者最小的支持与鼓励,谢谢你!

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务