头图

大家好,我是小菜。
一个希望能够成为 吹着牛X谈架构 的男人!如果你也想成为我想成为的人,不然点个关注做个伴,让小菜不再孤单!

本文主要介绍 服务限流

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

天气微凉,适合火锅?走起!小菜来到了海上捞火锅店,意料之中的人满为患,想走?嘴巴却不同意。那只能拿号排队了!看着每家店都是人满为患的样子,心里不禁意想了起来,如果我是某家店的老板那还用的敲代码吗?

瞎想总会成为可能,我不禁洋洋为我将来的餐厅考虑起来了,一家饭店,受场地规模和工作人员的因素能够承载的客流总是被限制的,因此很多受欢迎的餐厅在高峰期的时候都需要排队,餐厅为满载之后的客人排号,只有当顾客用餐完毕之后,才能让有号码牌且对应号码顺序的顾客进入餐厅就餐。那为什么要这么设计呢?其实这就是一种限流措施,严格控制客流量使其稳定在餐厅的运营能力之内,不会因客流量骤增而导致餐厅无法正常营业。这种限流方式保证在餐厅内就餐的顾客总数(并发量)是一致的。只有走了一个客人,才能允许一位客人进入,井条有序,合理运行!

服务限流 应当是每个并发程序都应该考虑的~!限流的目的不仅是为了控制访问的总并发量,而且还要尽量让访问的流量来的更均衡,这样才不会让系统的负载大起大落,因此又称为"流量整形"

当然在微服务盛行的时代,我们考虑到的 服务限流 不再单单应对 单体服务 ,而是更要清楚分布式场景下如何进行 服限流

一丶单体限流

image-20210730215659302

以上三种是我们在 单体服务 中常见的限流算法,我们接下来分别认识一下!

1、计数器限流

计数器限流 是属于一种比较简单粗暴的方式!

设计思路如下:

我们会限制一秒钟内能够通过的请求数(比如 50),从第一个请求进来开始计数,在接下去的1s 内,每进来一个请求,我们就会把计数值加 1,如果累加的数字达到了 50,那么后续的请求就会被全部拒绝,等到 1s 过去之后,把计数恢复成 0,并重新开始计数

使用计数器可以用来限制一定时间内的总并发数,但说到底这是一种简单粗暴的限流方式,而不是平均速率限流,在某些场景下可以使用。但是遇到某些特殊的情况下,如果系统的负载量只有 50,在第59秒瞬间请求 50 次,并且在第 1:00 也请求了 50 次,那么这个程序在 1 秒内被请求了 100次,瞬间超过总负载,很有可能直接击垮我们的应用程序!

当然,事情都没有绝对的,我们可以使用 滑动窗口 的方式解决问题。说到 滑动窗口 有些小伙伴并不陌生,因为 TCP 协议 就有采用 滑动窗口来控制流量,不清楚的小伙伴往下看!

滑动窗口算法指的是以当前时间为截止时间,往前取一定的时间,比如取 60 秒时间,在这 60 秒之内运行的最大访问数为 50,此时算法的执行逻辑为:先清除 60 秒之前的所有请求记录,再计算当前集合内请求数量是否大于设定的最大请求数 50?如果大于则执行限流拒绝策略,否则插入本次请求记录并执行正常流程。

我们在上图可以看出,一个被红色线段圈起来的就可以认为是一个时间窗口(1分钟) ,然后我们将时间窗口进行划分为 5 小格子,也就相当于 1 个小格是 12 s,每超过 12 s ,时间窗口就会往前步移一格,每一格都有自己独立的计数器,假设第 35 秒的时候来了一个请求,那么 0:25~0:36 这个范围格子的计数值就会加 1 。

我们通过上图回顾下 计数器 限流会遇到的问题,当 0:59 来了 50 个请求时会落在上图紫色区域 中,如果 1:00 又来了 50 个请求,会落在上图的 粉色区域 中,因为时间窗口的移动,总共 100 个请求落在了同一个时间窗口中,就会被检测出从而触发限流。而这就是 滑动窗口 的思想,接下来我们可以借助 Redis 来简单演示下:

执行结果:

Thread-0    正常执行
Thread-2    正常执行
Thread-3    正常执行
Thread-6    正常执行
Thread-7    正常执行
Thread-10    正常执行
Thread-11    正常执行
Thread-14    正常执行
Thread-1    正常执行
Thread-4    正常执行
Thread-5    正常执行
Thread-8    超出最大的系统负载量, 执行限流
Thread-8    正常执行
Thread-9    超出最大的系统负载量, 执行限流
Thread-9    正常执行
Thread-12    超出最大的系统负载量, 执行限流
Thread-12    正常执行
Thread-13    超出最大的系统负载量, 执行限流
Thread-13    正常执行

这段简易的代码当然有许多漏洞,但是仅仅给你提供一个实现的思路!

2、漏桶算法

我们开头说到的 餐厅排号 其实就是一种类漏桶的实现方式,餐厅的容量就相当于是一个 桶容量,桶的容量是固定的,桶底的水会不断的流出(用餐结束的顾客),桶顶的水(待用餐的顾客)不断流入。如果流入的水量(请求量)超出了流出的桶流量(最大并发量),桶满后新流入的水会直接溢出,这就是限流应用中常用的漏桶算法。

其实 Java 就已经自带了一个很好实现漏桶算法的工具,那就是 Semaphore,它可以有效的控制服务的最大并发总数,防止服务过载。下面是 Semaphore 的典型用法:

通过上述例子我们不难发现,漏桶算法主要关注的是当前的并发总量(信号总量),只有某个资源被释放的信号发出(release操作),等待进入的请求才能获得“通行证”,有出才有进,我们通过这种方式同样可以保证系统的负载可控。

3、令牌桶算法

限流的另一种常用算法是令牌桶算法,它的实现原理为系统以恒定的速度往桶中放入令牌,请求需要从桶中获取令牌才能被处理,一旦桶中无令牌可取,则拒绝服务。

我们可以借助第三方工具实现该算法,如 Google Guava 的 RateLimiter组件则是采用令牌桶算法,以下是简单的使用示例:

OUTPUT:
Thread-1    2021-08-01 00:09:14
Thread-10    2021-08-01 00:09:14
Thread-9    2021-08-01 00:09:15
Thread-8    2021-08-01 00:09:15
Thread-6    2021-08-01 00:09:16
Thread-7    2021-08-01 00:09:16
Thread-5    2021-08-01 00:09:17
Thread-3    2021-08-01 00:09:17
Thread-4    2021-08-01 00:09:18
Thread-2    2021-08-01 00:09:18

从上面的结果上来看,令牌确实是 1 秒产生 2 个,而 acquire() 方法为阻塞等待令牌,它可以传递一个 int 类型的参数,用来指定获取令牌的个数,当然它还有一种替代方法 tryAcquire(),此方法在没有可用令牌的时候就会直接返回 false ,这样就不会阻塞等待了。当然 tryAcquire()可以设置超时事件,未超过最大等待事件会阻塞等待获取令牌,如果超过了最大等待时间还没有可用的令牌就会返回 false

OUTPUT:
limit
limit
limit
limit
limit
limit
limit
limit
Thread-10    2021-08-01 00:08:05
Thread-4    2021-08-01 00:08:05

通过以上例子我们可以总结:使用 RateLimiter实现的令牌桶算法不仅可以应对正常流量的限速,而且可以处理突发暴增的请求,实现平滑限流。

二丶分布式限流

单机限流场景下,各个服务节点负责各自机器的限流,不关注其他节点,更不关注集群的总调用量~!但是后台资源是有限的,在分布式的场景下,我们的关注点不能再集中于某个节点上,有时候虽然各个单节点的流量都没有超,但是各个节点的流量和却超过了后台资源的总承受量,所以必须控制服务在所有节点上的总流量,这就是 分布式限流

说到分布式,我们会想到 网关 的概念

相关阅读请空降:《吃透微服务》 - 服务网关之Gateway

当我们在了解完网关的概念与作用后,自然清楚总流量控制可以在网关层面进行限流,但是有种 P2P 直连模式的服务集群就没有网关的概念。这个时候要怎么办呢?

我们上面说到有时候单个节点没有超过总流量,但是节点流量和却超过了总流量。那我们不妨先汇总每个服务节点的流量,并将汇总后的流量与预设的总流量进行比较,如果超过了总流量就需要进行限流。

简单来说就是我们如果集群的承载量为 1000,但是汇总出来的总流量是 1200,这个时候超了 200,就需要进行限流!那我们这个时候就需要把流量降低到 ( 1 - (200/1200) ) = 0.83,而这个 0.83 就是个单机阈值,也就是我们的限流比例,每个节点都要将当前的流量降低这个比例。

通过限流比例算出各自的限流阈值,然后再根据各自的限流阈值去调用上面说到的单机限流几种算法去做单机限流。因此集群环境下的限流也是以单点的限流为基础,但是在流量判定上有所不同。上面说的是一种限流思路的方向,接下来说下两种具体的限流操作。

1、Redis + Lua

这个限流策略重点在于 Lua 脚本的编写,什么是 Lua 脚本?有些同学又不淡定了~了解分布式锁的同学应该清楚可以利用 Redis + Lua 实现分布式锁。

Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序种,从而为程序提供灵活的扩展和定制功能。

既然 Lua 是核心,那我们先理清一下 Lua的逻辑实现:

看着流程图自然而然代码就出来了~

1、首先定义两个全局变量分别用来接收 Redis 应用中传递的键和限流大小

2、在应用端传递 KEYS 是一个数据列表,在 Lua 脚本中通过索引下标方式获取数组内的值

3、在应用端传递 ARGV 参数比较灵活,可以是一个或多个独立的参数,但对应到 Lua 脚本中统一使用 ARGV 这个数组接收,获取方式也是通过数组下标获取

编辑好 Lua 脚本后,我们就可以在 Java 中愉快的使用了:

在具体的业务场景中,我们可以自定义一个限流注解,配合 AOP 切面达到限流的效果!

2、Nginx + Lua

使用 Nginx + Lua 的方式对系统侵入性较低!我们直接看代码

Lua部分

可以参考 OpenResty 官方给出的限流示例,稍加修改即可

然后我们需要修改 nginx.conf 配置文件:

http 块中添加

image-20210801225750137

然后在需要限流的 server 块中添加:

当然除了以上说到的两种实现思路外,我们还可以利用现成的中间件 HystrixSentinel

Sentinel 相关阅读请空降:《吃透微服务》 - 服务容错之Sentinel

说到最后

说完了两种场景的限流,当然只是在很粗浅的层面泛泛而谈!不妨再让我以一段骚话结尾:我们在习惯单应用服务的时候,会发现其实单纯的单点限流并不难,因为我们的关注点是 1 ,由 1 进行延伸,事态往往会变得不可控,如果说单应用服务的时候我们还有很多现成的组件可以选择,但是如果要考虑到整个分布式集群的限流方式,我们往往不知所措。我们需要考虑服务节点的调用监控,日志采集,日志聚合,计算分析,限流决策判断等等许多环节,但是不必恐惧,我们换个方向想想,当你考虑的越多,不正说明你提升也会越多,最怕的不是困难与挑战,而是你原地踏步不前的无知!

我是小菜,与你结伴而行~

不要空谈,不要贪懒,和小菜一起做个吹着牛X做架构的程序猿吧~点个关注做个伴,让小菜不再孤单。咱们下文见!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!
我是小菜,一个和你一起变强的男人。 💋
微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

写做
624 声望1.7k 粉丝