API 速率限制器是系统限制系统一个用于控制应用程序或服务对API请求的频率的服务。速率限制通常用于控制资源的设计速率使用、防止滥用和维护服务的背后稳定性。
类似的服务产品有:Express Rate Limit、Spring Boot Rate Limiter、Ratelimiter
难度级别:中等
想象一下,我们有一个服务正在接收大量请求,但它每秒只能处理有限数量的请求。为了处理这个问题,我们需要某种节流或速率限制机制,只允许一定数量的请求,这样我们的服务就能对所有请求进行响应。从高层次来看,速率限制器限制了一个实体(用户、设备、IP等)在特定时间窗口内可以执行的事件数量。例如:
总的来说,速率限制器限制了发送者在特定时间窗口内可以发出的请求数量,当达到上限时,它会阻止请求。
速率限制的作用就好比是给你的服务穿上一件“防弹背心”,能够抵御一些恶意攻击,比如拒绝服务攻击,暴力破解密码或者信用卡信息等。这些攻击往往像海量的HTTP/S请求砸过来,表面上看似乎是真实用户在操作,实则可能是机器人在背后操控,因此这种攻击更加难以发现,也更有可能导致你的服务、应用或API被拖垮。
另外,速率限制还能帮助我们节省开支,降低维护网络设施的费用,避免垃圾邮件和网络骚扰。以下是一些能从速率限制中获益,使服务或API更稳定可靠的情况:
我们的流量控制器需要达到以下要求:
功能要求:
非功能要求:
流量控制其实就是设定用户可以多快、多频繁地访问API的规则。在一段时间内,限制客户对API的使用叫做"节流"。节流可以在应用层面或者API层面进行。一旦超出节流限制,服务器就会返回HTTP的"429 - 请求过多"状态码。
以下是三种常见的节流类型,都被不同的服务使用过:
常用的流量控制算法有两种:
固定窗口算法:在这种算法中,时间窗口是从时间单位的开始到时间单位的结束。比如说,对于一个分钟,无论API请求在什么时候发出,我们都会看作是在0-60秒这个时间段内。比如在下面的图示中,0-1秒之间有两条消息,1-2秒之间有三条消息。如果我们的流量限制是每秒钟两条消息,那么这个算法只会控制'm5'。
滑动窗口算法:在这个算法中,时间窗口从发出请求的那一刻开始,再加上窗口的长度。例如,如果在一秒的第300毫秒和第400毫秒各发送了一条消息,我们会把这两条消息看作是从这一秒的第300毫秒开始到下一秒的第300毫秒之间的两条消息。在上面的例子中,如果流量限制是每秒钟两条消息,我们就会控制'm3'和'm4'。
流量控制器的职责是判断哪些请求将由API服务器接收,哪些请求将被拒绝。当新的请求来临时,Web服务器首先向流量控制器询问这个请求应该被接收还是应该被拒绝。如果请求没有被拒绝,那么它就会被发送到API服务器进行处理。
我们以每个用户的请求次数限制为例。在这种场景下,我们为每个用户设置计数器,记录用户已经发出了多少个请求,以及我们开始计数请求的时间戳。我们可以将这些信息存放在一个哈希表中,其中'key'是'UserID','value'是一个包含一个用于'Count'的整数和一个用于Epoch时间的整数的结构:
假设我们的流量控制器允许每个用户每分钟发出三个请求,那么每当有新请求进来,我们的流量控制器将执行以下步骤:
我们的算法存在什么问题?
如果我们使用Redis来存储我们的键值,解决原子性问题的一个解决方案是在读取-更新操作期间使用Redis锁。然而,这将以降低同一用户的并发请求速度和增加另一层复杂性为代价。我们可以使用Memcached,但是它会有相似的问题。
如果我们使用简单的哈希表,我们可以对每条记录进行锁定以解决我们的原子性问题。
我们需要多少内存来存储所有的用户数据呢?让我们假设一个简单的解决方案,即我们将所有的数据保存在一个哈希表中。
假设'UserID'需要8 bytes。我们也假设一个2 bytes的'Count',可以计数到65k,对我们的使用场景来说已经足够了。尽管epoch时间需要4 bytes,我们可以选择只存储分钟和秒部分,这可以用2 bytes来存储。因此,我们需要总共12 bytes来存储用户的数据:
8+2+2=12bytes
假设我们的哈希表每条记录需要额外20 bytes的开销。如果我们需要在任何时候跟踪一百万用户,我们需要的总内存是32MB:
(12+20)bytes∗1millinotallow=>32MB
如果我们假设需要一个4-byte的数来锁定每个用户的记录以解决我们的原子性问题,我们将需要总共36MB的内存。
这可以轻松地放在一台服务器上;然而我们不希望将所有的流量都路由到一台机器上。此外,如果我们假设一个速率限制为每秒10个请求,那么这将对我们的流量控制器产生1000万QPS!这对一台服务器来说太多了。实际上,我们可以假设我们将在一个分布式环境中使用类似Redis或Memcached这样的解决方案。我们将所有的数据存储在远程的Redis服务器中,所有的流量控制器服务器将在处理或节流任何请求之前读取(和更新)这些服务器。
如果我们能够追踪每个用户的每个请求,我们就可以维护一个滑动窗口。我们可以在哈希表的'value'字段中的Redis Sorted Set(有序集合)中存储每个请求的时间戳。
假设我们的速率限制器每分钟每用户允许3个请求,那么,每当有新请求进来,速率限制器将执行以下步骤:
我们使用滑动窗口,要存储所有用户的数据需要多少内存呢?假设'UserID'需要8 byte。每个epoch时间将需要4byte。假设我们需要每小时500个请求的速率限制。假设哈希表有20 byte的开销,Sorted Set有20 byte的开销。最多,我们需要总共12KB来存储一个用户的数据:
8 + (4 + 20 (Sorted Set开销)) * 500 + 20 (哈希表开销) = 12KB
这里我们为每个元素预留了20 byte的开销。在一个Sorted Set中,我们可以假设我们至少需要两个指针来维护元素之间的顺序 —— 一个指向前一个元素,一个指向后一个元素。在64位机器上,每个指针将占用8byte。所以我们将需要16byte用于指针。我们额外增加了一个word(4 byte)用于存储其他开销。
如果我们需要在任何时候跟踪一百万用户,我们需要的总内存将是12GB:
12KB∗1million =12GB
与Fixed Window(固定窗口)相比,Sliding Window算法占用了大量的内存;这会是一个可扩展性问题。如果我们能够结合使用上述两种算法来优化我们的内存使用会怎样呢?
如果我们用多个固定的时间窗口来追踪每个用户的请求计数,例如,1/60的我们速率限制的时间窗口大小会怎样呢?例如,如果我们有一个小时的速率限制,我们可以每分钟计数一次,并在收到新请求时计算过去一个小时内所有计数器的总和,以计算阈值。这样可以减少我们的内存占用。比如,我们限制每小时500次请求,每分钟最多10次请求。这意味着,当过去一小时内带时间戳的计数器之和超过请求阈值(500)时,用户就超过了速率限制。此外,她每分钟不能发送超过十个请求。这是一个合理而实用的考虑,因为没有真实的用户会频繁发送请求。即使他们这么做,他们也会因为每分钟限制都会重置而看到重试的成功。
我们可以在Redis哈希中存储我们的计数器,因为它为少于100个键提供了极其高效的存储。当每个请求在哈希中增加一个计数器时,它也会设置哈希在一小时后过期。我们将每个'time'(时间)标准化到一分钟。
我们使用带计数器的滑动窗口,存储所有用户的数据需要多少内存呢?假设'UserID'需要8byte。每个epoch时间将需要4byte,Counter(计数器)将需要2byte。假设我们需要每小时500个请求的速率限制。假设哈希表有20byte的开销,Redis哈希有20byte的开销。因为我们每分钟都会进行计数,所以最多,每个用户需要60个条目。我们需要总共1.6KB来存储一个用户的数据:
8 + (4 + 2 + 20 (Redis哈希开销)) * 60 + 20 (哈希表开销) = 1.6KB
如果我们需要在任何时候跟踪一百万用户,我们需要的总内存将是1.6GB:
1.6KB * 1百万 ~= 1.6GB
所以,我们的'带计数器的滑动窗口'算法比简单的滑动窗口算法少用86%的内存。
对于用户ID的数据,我们可以进行分片处理以分散用户数据。为了容错和复制,我们应该使用“一致性哈希”。如果我们希望对不同的API实施不同的限流标准,我们可以选择每个用户每个API进行分片。以“URL短链接生成器”为例,我们可以对每个用户或IP的createURL()和deleteURL() API设置不同的限流规则。
如果我们的API是分区的,一个实际的考虑是为每个API分片也设置一个相对较小的限流器。以我们的URL短链接生成器为例,我们希望限制每个用户每小时不超过100个短URL的创建。假设我们使用基于哈希的分区方式对createURL() API进行分区,我们可以设置每个分区的限流器,允许每个用户每分钟创建不超过3个短URL,另外每小时创建不超过100个短URL。
我们的系统可以从缓存近期活跃用户中获得巨大的好处。应用服务器可以在击中后端服务器之前快速检查缓存是否有所需记录。我们的限流器可以从Write-back缓存中获得显著的好处,只在缓存中更新所有计数器和时间戳。对永久存储的写入可以在固定的时间间隔进行。这样我们可以确保限流器对用户请求增加的延迟最小。读取操作始终先击中缓存,这在用户达到最大限制时非常有用,因为限流器将只读取数据而不进行任何更新。
对于API速率限制服务来说,最近最少使用(Least Recently Used,LRU)可能是一个合理的缓存驱逐策略。
让我们来讨论一下使用这两种方案的优缺点:
IP:在此方案中,我们对每个IP的请求进行限流;尽管在区分“好”与“坏”行为者方面并非最佳,但它仍然比完全没有限流要好。基于IP的限流最大的问题在于,当多个用户共享一个公网IP时,如在网吧或使用相同网关的智能手机用户。一个行为不当的用户可能导致对其他用户的限流。另一个问题可能出现在缓存IP限制时,因为即使一个计算机也有大量的IPv6地址可供黑客使用,让服务器因跟踪IPv6地址而耗尽内存是轻而易举的事!
用户:限流可以在用户认证后对API进行。一旦认证成功,用户将获得一个令牌,用户将在每次请求时传递该令牌。这将确保我们对具有有效认证令牌的特定API进行限流。但是,如果我们必须对登录API本身进行限流怎么办?这种限流的弱点是,黑客可以通过输入错误的凭证达到限流阈值来对用户进行拒绝服务攻击;之后,实际的用户将无法登录。
混合:一个正确的方法可能是同时进行每IP和每用户的限流,因为它们在单独实施时都有弱点,然而,这将导致更多的缓存条目,每个条目需要更多的细节,因此需要更多的内存和存储空间。
责任编辑:姜华 来源: 今日头条 速率限制API(责任编辑:时尚)
荣盛发展大股东质押公司7599万股股份 占公司总股本比例的1.75%
多家基金公司推出目标日期基金下滑曲线 直线型和阶梯型设计方案为主流