在一个大规模的分布式架构当中,我们应该如何设计和选择一个最适合我们的 API 限流技术呢?接下来我会从以下四个章节来分别介绍:
- 为什么需要限流?
- 常见限流算法
- 分布式限流技术要点
- 分布式限流实践方案
一、为什么需要限流
我们为什么需要限流?相信大家在设计所有系统的时候,都会首先问自己这样一个问题。
API 限流需要解决的问题
之所以会有限流这个问题,是因为我们生活在一个资源有限的社会当中,当资源出现供不应求的时候,就会引发一系列的问题。针对资源的问题,我们通常会增加一些资源的限制,比如我们出门的时候,会遇到交通的限行。回到 API 这个概念上同样如此。
当后端的资源有限的时候,我们如果没有限流就会遇到一些恶意攻击,导致服务出现一系列的雪崩情况。相信大家可能在若干年前春运购票的时候都经历过类似的场景。限流除了刚才提到的针对恶意的请求之外,还可以起到流量整形的作用。不管我们进来的流量是什么样的频率,我们要保证请求转发到后端的时候,它是平整的、平稳的。在业务的维度,我们通常还会对用户进行 SLA 的分级,比如说有付费用户,有免费用户。这两类用户他们能够请求到的资源肯定是不一样的。所以 API 限流,也可以针对这两类用户做一个隔离。第四点就是现在 API 经济非常火。我们会把 API 当成商品去售卖,有商品呢肯定有库存,有购买的限制。所以 API 限流可以解决这方面的问题。
API 的限流能力
现在我们大致了解了 API 主要解决哪些问题,我们也对 API 的限流能力做了一些总结和归纳
我们将它分成了三类:
第一类是基础能力。最基本的就是我们会按照一个固定的时间维度来限制 API 的调用次数,比如说一分钟请求一万次。第二个基础能力,就是请求的缓冲队列。当后端资源出现不足的时候,我们除了直接拒绝请求之外,我们其实还可以把 API 缓冲到一个队列当中。当后端的资源释放之后,我们再把请求释放过去。这种对于客户端来说是更友好的一种限流的行为。
第二类是业务维度的能力。主要是针对 API 报文当中的一些业务属性进行限流。比如说我们希望针对 API 的调用者,或者 API 的客户端 IP 去进行不同的限流。
第三类信息反馈的能力。最基本的信息反馈就是针对 HTTP 的标准,来返回一个比如说 429 之类的错误。除了返回一个错误之外,我们还可以做的更多。比如说在被限流的时候,我们可以返回给客户端过多久之后可以继续重试。就像我图中列的,X-Ratelimit-Retry-After 的 header。当然如果没有限流,我们也可以告诉用户你还有多少次可以调用。这些信息都会非常好地帮助客户端来进行下一步的决策。
二、常见限流算法
接下来我们将会介绍,限流中的几种非常常见的算法。主要包括:
1.固定窗口
2.滑动日志
3.滑动窗口
4.漏桶算法
5.令牌桶
-
固定窗口
第一种叫做固定窗口,这种算法是一种最常见的算法。其中窗口这个概念,主要对应限流场景当中的时间单位。
比如上图当中我们每秒钟限流 10 次,那么固定窗口就对应的是 1 秒可以看到在第一个窗口当中每一个方块代表一个进入的请求。绿色的方块代表我们可以有效地放通给后端的请求,红色的方块代表被限流的请求。在每秒钟限流 10 次这个场景当中,可以看到在窗口 1 当中,因为从左到右是时间维度,所以先进来的 10 个请求会被放通。再进来的请求就变成红色的被限流的请求。那窗口 2 也是如此。
-
-
优势
-
这个算法的优势就是逻辑非常简单,实现起来非常快,而且它维护成本比较低。另外一个优势就是,它的时间和空间复杂度都非常低。因为我们从技术角度只需要维护当前窗口中的一个计数器就可以了。
-
-
劣势
-
当然它的劣势也是非常明显的,因为该算法仅能保证在固定的窗口当中去放通固定的请求数。但是在两个窗口的间隙的时候,我们却没有办法保证。
我们想象一个极端的场景,如上图,假设在第一个窗口的最后一毫秒进来了 9 个请求,紧接着在第二个窗口的第一毫秒又进来了 9 个请求。这个时候,我们分别看这两个窗口,它们都是 OK 的。但是我们如果看中间,我们中间截取一个新的窗口的话。在这个窗口当中,它成功请求很可能会超过我们限流的 10 次。极端情况下,可能至多会不超过两倍的限流值。
当然这个问题对于请求相对比较平稳的情况下,它是没有问题。但是由于咱们没有办法控制客户端到底是怎么请求,所以说极端情况下,还是会对后端产生一些影响的。这个问题主要出现于这个窗口是固定的。那么你可能会想,我们如果把窗口改成动态的,会不会更好?答案是肯定的。
-
滑动日志
这样的话,我们就可以讨论一下第二个算法,叫做滑动日志。
在该算法当中,我们与其维护的是窗口,现在我们维护的是每一条请求的日志。日志当中最重要的一个属性就是时间戳。每当一个新的请求过来之后,我们会根据该请求动态计算出来当前窗口起始的边界。因为我们已经有时间戳了,所以向前遍历就可以很简单地拿到边界值。在之后我们会计算一下窗口当中全部的请求计数,然后再拿该计数和限流的值去对比,就能得出当前该请求是要被限流还是要放通。
-
-
优势
-
这个做法也很好理解,它最大的优势就是,它是 100% 准确的。因为我们保留了所有请求的日志,而且是针对每一个请求都会重新计算动态的窗口,实现成本也很低。
-
-
劣势
-
但是它的劣势就是,时间空间复杂度都是相对比较差的。因为我们可以看到,我们需要保留每一条请求的日志,在存储上面需要额外的开销。那么在性能上,我们也是每一次请求都需要去重新做动态计算的过程。虽然我们可以通过一些,比如像二分查找之类的算法,来进行优化。但相比起第一个方案,还是有很大的缺点。
-
滑动窗口
你可能会想到,这个算法和第一个算法,恰巧是完全相反的优缺点的关系。我们有没有可能把两个算法折中一下,结合一下,取一个中间的状态。我们现在有了第三个算法,叫做滑动窗口。
在滑动窗口的算法当中,同样我们需要针对当前的请求来动态查询窗口。但窗口中的每一个元素,不再是请求日志了,而是每一个子窗口。所以可以看到子窗口的概念就非常类似于方案一,每一个子窗口的大小我们是可以动态调整的。
比如说我们现在的例子,是每分钟请求 100 次。我们可以把每一个子窗口的时间维度设置为一秒。这样的话在一个大的时间窗口,一分钟的窗口当中,就可以有 60 个子窗口。这样每当一个请求来了之后,我们去动态计算这个窗口的时候,我们只需要向前最多找 60 次。所以这个时间复杂度,就可以从线性变成常量级了。找到了边界之后,我们同样需要计数。但这个时候,是把每一个子窗口的计数器累加起来,而不是遍历所有的日志。所以时间的复杂度相对来说也会更低一些。
第三种算法就是把前两个算法取了结合的折中状态,所以它在性能上明显优于第二种,但是它的准确度又差于第二种,所以它是一个更平衡的算法。
-
漏桶算法
接下来我要介绍两种算法,都跟桶有关,第一种叫漏桶算法。
咱们先来看上面边这张图。在这个算法当中,我们把每一次请求当成一个小水滴,水滴请求到限流组件之后,我们会先把它储存在一个桶中。这个桶的底部有一个小洞,这个洞会匀速地向外流水。漏水的过程,我们把它当成请求放通的过程。可以看到,请求进来的速率是不能控制的,不同客户端可能有不同的速率会往桶里面流水。但是由于桶底下这个洞的大小是可控的,所以我们能保证请求不管以什么样的速率进来,我们都能让它以相同的速率往后端去转发。
可以看到在这个算法当中,桶的大小控制了当前能够接受的最大并发数,而实际的限流值是取决于桶最终往外的流速。虽然我们把它抽象成了一个桶,但从技术角度我们理解,它更像是一个先进先出的队列。
-
-
优势
-
它最大的优势也就在于它的流量整形功能。它比较适用于像我们刚经历过的 618 这种场景。我们不管是在哪个平台购物,都需要经过一个支付环节。而在支付系统当中,它需要和上游的很多银行系统去对接。这些银行系统的负载是要低于整个支付平台,所以这个时候我们就需要针对不同的银行的 SLA 去限流。在这个场景当中,漏桶算法就是一个非常合适的算法。
-
-
劣势
-
当然它也有劣势,劣势之一就是它的实现复杂度相比起前几种(算法)会更高,维护成本也会更高。而且第二个是因为它的流量整形只能保证匀速向外放行,所以它并不能满足一些流量经常会突增的场景。
-
令牌桶
这个时候就有了下一种算法,叫做令牌桶。
令牌桶它是基于漏桶之上的一种改进版本。在这个桶中我们存的不再是请求了,而是令牌。有一个匀速的机制,会往桶中去放入新的令牌。当然桶是有容量的,所以当桶满了之后,新的令牌就会被丢弃。每当有一个新的请求过来的时候,我们就会去桶中尝试拿取一个令牌。如果有空闲令牌,这个时候请求就可以放通,如果它没有令牌,那么我们就限流。
这个算法跟上一个漏桶比起来,它最大的区别就是我们可以允许短时间的流量突增。因为在上一个算法当中,不管同时进来多少个请求,我们只能匀速地向后放行。但是在令牌桶当中,我们可以同时往后放行的请求数取决于桶中最大的令牌数量,也就是桶的容量。
-
-
优势
-
所以针对一些流量可能会出现突增,而且后端可以接受突增的场景,令牌桶是一种更适合的方案。第一(它可以)支持平均速率的流量整形,第二它还可以允许一定的突增,所以它是一个更优的选择。
-
-
劣势
-
当然它的劣势同样也是它的实现复杂度相对会高一些。
-
各算法适用场景
我们刚才介绍了总共五种算法,时间复杂度跟空间复杂度我就不展开说了,大家可以直接看图。我主要重点说一下各个算法的适用场景。
第一种固定窗口,它适用于流量相对比较均匀的,而且对于限流的准确度要求不严格的场景。因为我刚才介绍到了,它可能会在极端情况下出现突增。如果后端不能支持的话,系统可能会因为这种突增造成一定的故障。
第二种滑动日志,它是跟第一种算法完全反过来,它是适用于需要准确度非常高。但是由于这个算法本身性能差一些,所以我们需要在场景中容忍这种性能。
第三种滑动窗口就是前两个算法的折中了,它也是适用于更多的场景。
第四种就是漏桶,它适用于需要保持流量非常平稳,流量整形的场景。
第五种令牌桶适用于需要流量整形,同时又需要有一定的流量突增的场景,也比较常见于网络的 QoS 场景。
三、分布式限流的技术要点
我们刚才介绍了几种限流中非常常见的算法,就好比我们在学习一门剑法,我们现在已经掌握了几种基本的剑术,比如有砍、有劈、有刺。我们如何把这些基本的剑法应用到最终的实战当中,这个时候我们就要结合具体的场景来看一看了。比如在分布式限流的场景当中,我们首先要看一下,我们比较关注哪些点,然后再看如何去设计。
-
准确性
第一点和刚才介绍算法的时候,屡次提到的一点相同,就是准确性。在分布式架构当中,准确性和单体有什么区别呢?主要的区别就产生于在分布式架构当中,同一个操作或者同一份数据,它可能是分散在不同的节点当中的。这个时候我们就需要保证分布式系统当中数据的一致性。
第二我们就要保证限流操作的原子性。因为刚才我提到了在分布式架构当中同一个操作,它可能会分散到不同节点去运行。如果某一次节点出现故障了,其他节点还是正常运行的话,最终的结果肯定跟预期是不一致的。下边这个图有一个例子,是在固定窗口算法当中,我们每一次限流计数涉及到两个比较重要的操作。
首先就是判断窗口判断一下当前的请求应该落在当前的窗口,还是应该新启一个窗口。如果是在当前窗口,就直接计数加一。如果是新开窗口,我们就需要重新创建一个新的窗口。这里有一次读一次写,如果在读写过程当中又有其他的操作,来把读写的 KEY 做了变更,这时这次读写的状态就不准确了,也会导致在限流过程中出现一些数据的误判。针对读写的完整操作,需要保证的它的原子性,或者大家如果比较熟悉数据库,我们应该理解事务,它主要就是保证这些要点的。
-
性能
第二点就是性能,性能虽然不是只有分布式架构当中需要关注的。但是由于我们在分布式架构当中增加了额外的节点,增加了额外的链路。所以我们需要去考虑额外链路,或者额外的分布式算法所带来的额外延迟。再者就是空间成本,我们知道现在不管是内存还是硬盘,它最终都会跟成本、钱挂钩,所以往往也是很多业务上面重点需要考虑的。
-
可扩展性
接着就是可扩展性。其实我们选择分布式架构一个主要的原因之一,就是为了架构能够平滑地去扩展。这里扩展主要包含两个方面,一个叫横向扩展 ,一个叫纵向扩展。在介绍扩展性之前,我想先讲一讲什么叫限流实体。
还是回到一个例子,因为我是做 API 网关产品的,API 网关产品最重要的限流维度就是 API。我们需要针对不同的 API 进行不同的限流。在这个场景当中,我们把 API 就作为一个限流实体。横向扩展就是指,我们需要平滑地支持更多 API 的限流。因为每个 API 的限流值不一样。我们需要保证,每一个 API 的限流实体能够进行独立的限流判断,不能互相影响。纵向扩展就是指,一个 API 能支持的 QPS,可能是几千、几万甚至几十万,这个叫一个 API,一个限流实体的扩展,我们叫纵向扩展。
-
可用性
最后一点就是可用性。我们知道限流是保护系统可用性的非常重要的一个环节。其本身的可用性也是更重要的。如果限流这个环节出现故障,我们没有很好地降级,很好地容错的话,很可能引发一系列的雪灾效应。在互联网的历史当中,这种例子也是数不胜数,我就不一一列举了。
如果我们要保证它的可用性,我这里列举了几个需要考量的点:
第一我们需要避免链路上的单点故障,每一个环节我们都需要考虑,它如何能做到单点容灾。
第二如果出现故障。即使我们考虑得非常详细,仍然有出现故障的可能性,这个时候我们需要有相应的降级策略。
第三个就是可观测性。我虽然设计得很好,但是我还是需要有一些机制能够主动的发现这些故障。这样的话,能保证我们睡觉睡得更安稳。
现在我们知道了限流在分布式当中都需要关注的主要技术点。
四、分布式限流实践方案
接下来我们看一看,在最终实践的时候,我们应该怎么做。
-
API 网关限流需求分析
第一个我想介绍的是腾讯 API 网关的一个案例。首先在我们设计方案之前,我们需要进行需求分析,对于 API 网关这类产品来说,它主要的限流功能需求,我们大概分成了三类:
第一类就是产品 SLA 的限流,因为我们做的是一个商业化产品,我们有不同类型的用户,我们有共享用户,就是它底层的计算资源是共享的。我们有独享用户,独享用户里又分成了不同的独享规格,每一类用户他们能支持的最大 QPS 是不一样的。这个是一类产品维度的限流。针对这类需求的特点就是:第一我们对它的性能要求非常高,因为每一次请求都要经过限流这个环节,如果每次环节都带来额外的性能开销,对于很多客户来说是不能接受的。第二点就是准确度要求不是很高。这是产品 SLA 层面的限流,我们不需要保证 100%的准确率。有 5%—10%的误差是OK的。针对这类需求,我们最终选择的是固定窗口。
第二类需求就是,用户针对自己的业务维度、业务需求来自定义的限流。比如说,他可能希望针对不同的 API 配置不同的限流值,来保护它的后端。这类要求跟第一个需求不一样的是,它对于准确度要求相对是高的。因为它是用户自己的需求,它是用来保护后端,如果限流不准可能对后端会有额外的压力。第二,因为用户有的业务可能是会有流量激增的,所以我们在设计的时候也要考虑到这一点。这里我们用到的算法就是令牌桶,具体原因刚才也介绍到了,因为它能支持一定的流量突增,也能起到流量的平稳作用。
第三类需求就是现在我们有 API 市场的功能。我们可以把 API 上架到市场,用户把它的 API 设置调用的额度,调用者每调用一次,会付出一定的费用。针对这类的需求,第一它没有时间窗口,它只有一个库存的概念;第二它对于准确性要求极高,如果少计数一次,那么用户可能就少收一份钱。但是如果多计数一次,调用者可能要多花钱。它对于准确度(要求)是非常高的,所以这里我们用到的是计数器,它没有时间窗口的概念。但是这个计数器,我们也不需要用到缓存时每次都需要跟后端存储,实时去同步交互。因为要保证可用性,除了功能需求之外,性能上也有一些要求,比如说单集群。我们规划的是需要能够支持百万的 QPS,单 API 能支持十万的 QPS,同时也需要能够支持平滑的横向扩展。
-
方案一:Redis 中心存储(腾讯云 API 网关)
针对以上需求,我们最终选择的是基于 Redis 中心存储的方案。其原因主要有:
第一,产品本身已经在使用 Redis 了,所以使用 Redis 没有额外引入,更多的运维成本。
第二,我们用的是腾讯云上的 Redis,它对于业务方来说几乎是零运维成本。这对于我们来说是非常划算的。
第三,Redis 支持很多类型的存储结构,比如它有 String,有 Hash,还有 Sorted Set,这一系列的存储结构非常适合有多场景,每种场景又适用于不同算法的场景。所以我们最终选择的是 Redis。
当然在你的业务当中,是可以采用更多其他的 KV 存储的方式的。我们看到有 MemoryCache,也有其他的 KV 存储,所以要结合你自身的自己业务的情况来做决策。在我们的链路当中也是非常简单的,可以看到请求进来之后,会先经过 Redis 的一个实时计算,针对不同的场景我们也会用到不同的算法。在 Redis 计算之后,我们会得出我们是否要进行限流。
在这个链路当中可以看到,Redis 成为了一个关键环节。它本身也存在着单点故障的风险。针对单点故障,我们采用的是降级到本地限流,也就是进程级别的限流。
还有一种选择就是不限流,但是不限流对于后端来说,风险是更大的。所以当我们采用本地限流的时候,这里要考虑的点就是如何能保证在降级的时候,对用户的影响最少。因为没有中心存储了,节点之间又没有通信,所以这个时候,我们只能通过提前计算来保证每个进程。它们之间的限流值累加起来是接近于分布式限流的限流值。
举个例子,假设我们现在全局限流是每分钟 1000 次。我们有两个节点,每个节点有八个进程。这个时候可以做一个简单的除法,就能得出来每个进程大概需要多少的限流值。当然这个并不是特别准确,因为我们要依赖于负载均衡,依赖于进程之间的负载均衡。大家了解负载均衡,它没有办法保证 100%的流量均衡,所以它存在一定的准确度衰减。但是在故障降级这种场景当中是可以接受的。还有一点非常重要,就是在 Redis 恢复之后,仍然是需要将本地的数据同步回 Redis。因为在我们的场景当中,限流窗口是用户定的。可能是分钟,可能是秒,也可能是更长。针对这种更长的场景真的故障的时候,我们损失掉一些数据,整体准确度下降了。所以在故障恢复之后,还是需要将本地的数据同步回 Redis。
-
-
方案要点:
-
我们再看一下这个方案当中的一些要点,比如:
在存储方面,我们是依赖于 Redis 来做限流数据的存储。在性能方面,我们第一用到了一个连接池来管理所有 Redis 的 I/O,这样能减少一些连接的损耗。第二我们用到了 Redis 的就近接入的能力,我们所有的环节都是要做跨机房容灾的。所以要做就近接入减少机房之间的链路损耗。
在原子性方面,Redis 提供了一个 Lua 脚本的能力,这个非常适合我们。因为不同算法,它虽然有对应数据结构,但是我们还是需要保证限流整体逻辑的原子性。Lua 脚本这个能力就可以非常好地解决这个问题。但是这块大家要注意,Redis 在实现 Lua 脚本的时候,它为了保证原子性,在 Lua 脚本执行的过程当中相当于阻塞了其他脚本,或者其他操作的写入。所以说如果你的脚本执行时间非常长,对于 Redis 整体的性能是有非常大影响的。所以这一点大家在使用的时候,一定要额外考虑。
在容错方面,刚才介绍到了,我们是在 Redis 故障的时候会有一个降级的逻辑。
在扩展性上,我们用到的是 Redis 的一个集群的架构。在该架构当中,可以平滑地去扩展它的节点来实现横向的扩展。但是在纵向上 Redis 确实有它的局限性,虽然它的节点可以横向扩展,但是每一个 Key 最终只能在同一个分片上面去进行存储、进行写。在单 Key 的写操作上还是存在瓶颈。
在隔离性上,第一我们可以通过 Redis Key 进行逻辑的隔离,第二如果是针对更高级别隔离需求,我们可以通过多个 Redis 实例来实现租户的隔离。
最终是可见性。因为刚才提到了,为了保证整个限流系统的稳定性,我们还是需要一些外部的监控。针对 API 网关,我们有多个环节的监控。第一是针对 API 网关最重要的请求日志,进行收集跟监控。同时针对每一个组件的系统日志,我们也都会收集。同时针对一些 Error Log 进行告警。针对节点上这块就不多说了,大家应该都有。Redis 的监控也非常重要,因为它存储了所有的限流数据,所以它的 CPU 它的耗时、慢查询、错误率等等这些指标,都可以反映出当前限流的状态。
-
-
优势
-
它的优势就是刚才提到的,我们利用 Redis 在不增加运维成本的情况下解决了限流的问题,同时它还能提供多种数据结构来满足多样的场景。第二它支持横向扩展。
-
-
劣势
-
它的劣势方面,刚才也提到了在纵向扩展的时候是有它的局限性。而且 Redis 是一个分布式的中心存储,所以我们在限流的时候如果同步请求它的话,会增加一个毫秒级的额外延迟。这对于某一些业务来说,可能是不能接受的。第三个劣势,它依赖于中心存储,不适用于边缘节点的。
-
-
方案一优化:异步数据同步
-
针对边缘计算场景的这种劣势,我们进行了优化,优化方案叫做异步数据的同步。
-
-
-
方案要点:
-
-
因为刚才提到了,当每个请求来,我们就要同步地去请求 Redis,所以 Redis 成为了一个瓶颈,Redis 的 I/O,它的延迟就会直接反映到 API 网关的每一次请求上。
针对这个问题,优化思路就是把同步变成异步。每一次请求过来的时候,我们会同步地将请求在本地进行限流的逻辑处理,通过异步的机制,将限流的本地数据同步到 Redis,会有一个异步的同步。当然这里的问题同样存在,也就是说,如果现在限流窗口是一分钟,在一秒的时候突然来了大量的突发请求。这个时候我们还没有来得及将这些数据同步回 Redis,所以很有可能这些请求,就会造成限流击穿,造成后端的雪崩效应。这种(情况)我们是不希望看到的,针对这个问题,我们增加了一个额外的叫做同步计数检查的环节。每一个请求来了之后,我们会先检查同步计数的条件来判断这个请求,是要强制到 Redis 那边做计算,还是要在本地计算。这里同步检查的条件,主要是判断本地计数的计数器是否超过了分布式限流阈值的一定比例。
举例如果现在限流是一分钟 100 次,现在一个单节点的限流已经有 50 次了。如果我们只有两个节点,其实很可能超过了(限流阈值)。这个时候我们就会在本地设置一定的比例阈值,比如本地限流不能超过全局限流的 10%,一旦超过了就会同步触发一次强制的同步。通过这个机制就可以保证不会出现刚才说的,请求突增把限流打穿的场景。
-
-
-
优势
-
-
这个方案的优势就是我们把同步变成了异步。原来 Redis 造成的瓶颈现在基本就不存在了。第一它降低了延迟,第二它可以极大地提升整体的吞吐率。因为现在本身限流系统是分布式的,所以它不会受到 Redis 单 Key 的限制。第二个优势就是可调节,这个非常重要,因为不同场景下,对于准确率和性能的取舍是不一样的。我们可以调节同步计数的条件,比如我们可以调整本地计数和分布式全局阈值的比例,来权衡我们多久需要同步一次。这样的话,就可以调整性能和准确性的关系。
-
-
-
劣势
-
-
它的劣势就是它的准确性会下降,而且没有办法实时去预估剩余的配额。因为有些场景客户端需要知道限流还能调用多少次的。但是由于现在变成了异步计数,所以当前本地拿到这个数据并不准确。同样还有一些场景是不能支持的,比如说刚才我提到的 API 市场。因为它需要 100% 准确,不能有异步同步的机制,因为异步同步并不能保证一定会同步。可能中间有一些网络异常,有可能网络异常的同时本地节点又挂掉了,这个时候内存数据就丢了。所以这种优化也只能针对部分场景来启用。
-
方案二:负载均衡 + 本地限流
在我们刚才介绍的方案以外,我们还有多种方案都可以解决分布系统中的问题,现在介绍的是一种基于负载均衡的方案。
-
-
方案要点:
-
可以看到当该方案当中请求进来之后,会先经过负载均衡,将请求分发给不同的 API 服务节点。在每一个服务节点之上,我们又会部署一个 Nginx 的反应代理服务器,在 Nginx 上面增加了限流。请求进来之后,它会轮询到每一个节点,由每个节点进行独立限流。在这个方案当中,我们实际是将一个分布式的限流转化成了每个节点的本地限流。它在方案上是非常简单的。但是它也有问题,因为这个架构依赖于负载均衡,我能将流量均匀地分发给每个节点,当流量不均匀的时候,可能从全局看的话,它的限流就是不准确的。当然它也有它的适用性,比如在架构当中,每一个服务的限流值是固定的,所以我们只需要,针对节点维度去限流就 OK 了。
-
-
优势
-
这个方案就没有问题,我们也不需要进行动态的节点数量和限流值的同步变更。它另外一个优势,就是它的延迟非常低。因为我提到了它是把分布式限流转化成了本地限流。它没有进行额外的节点之间的通信。同时它的精度可以做得非常高,因为本地限流是纯内存计算,所以 Nginx 的限流模块是能实现毫秒级的限流的。这在我们前面的方案是完全做不到的。
-
-
劣势
-
它的劣势就是刚才提到的,第一它是依赖于负载均衡,将流量均匀地分发给每一个节点。第二就是有可能扩缩容的时候,我们需要重新计算它的单节点限流值。
-
方案对比
上面介绍了四种,分布式限流当中的方案。咱们可以看到每一种方案都有它的优势跟缺点,没有任何一种方案一定比其他好。所以在我们选择的时候,还是要针对自己的业务需求,来选择最适合的场景。
第一种中心的存储限流比较适用于限流对象并发量非常有限的情况下,因为它有垂直的扩容风险,那么同样它会产生毫秒级的额外延迟。这些如果都能容忍的话,中心存储是一种非常好的方式,它也非常常见于 API 网关的产品当中。因为 API 网关的后端对应的是不同的业务系统,所以我们需要一个中心化的解决方案。
第二种是负载均衡加本地限流的方案,它更适用于对精度要求非常高的场景。如果你需要毫秒级限流,那么方案二可以优先考虑一下。
-
思考
微服务的架构当中,在整体的限流设计当中,不管采用哪种方案,我们都有一些基本的原则:
第一就是我们希望,尽可能地将限流逻辑前置,这样我们可以减少不必要的资源消耗。
第二就是在设计限流 Key 的时候,如果我们用的是 KV 的存储去解决限流问题,我们就需要一个限流的 Key。Key 的格式需要尽可能相互兼容。像 API 网关迭代过程当中,出现某一个场景,最早我们用的是 A 算法,后面我们变成了 B 算法。如果在限流的 Key 当中耦合了算法进去,这个时候我们就必须要更换 Key 的格式,这样就没有办法保证向前兼容了。
第三个是类似,我们在限流 Key 的设计当中要加入足够多的业务信息。当出现限流不准,我们可以快速地定位到是哪一个资源的限流,这样可以节约我们排查问题的成本。
最后一个不限于是限流了,它在我们所有设计当中都是可以适用一条原则,叫 Less Code==Less Bug。我们尽可能地希望用最简单的逻辑去实现我们想要的功能。从一开始设计的时候,没有必要去考虑太多不一定发生的场景,这样很有可能会限制我们的设计,而且会引入更多的不确定性。
总结:如何设计限流系统
最后我们还是要总结一下,当我们去设计一个限流系统的时候应当从几方面进行。
收集需求:
第一步不要着急去选择算法,不要着急去设计方案,而是先把需求梳理清楚。比如产品有哪些场景会用到限流,系统上都需要有哪些关键点。同样还有目前有没有一些合适的产品已经在应用了。所以这都是决定了我们后面决策的一些重要因素。
选择算法:
第二点就是根据业务场景来选择合适的算法,这里大家可以重点参考算法的对比表格。
设计方案:
第三点就是在方案设计的时候,根据第一点收集到的需求来选择一个合适的技术架构。如果公司内部已经有现成的限流方法,我们也可以去考虑一下是不是可以避免重复造轮子。最后我们要知道,限流是保护服务的一个兜底手段,所以一定要额外重点考虑。限流系统本身的容灾、降级、还有监控等等这样的方案。
最后我想跟大家分享一个我个人比较认可的一个观念。没有一个完美的技术方案,只有最合适的。如果你没有想好,你应该用哪个方案的时候,我们先从最简单的方案开始。
希望以上内容对你有所帮助。