流量控制(Rate limiting)是指防止操作频率超过规定的限制。在大规模系统中,流量控制通常用于保护底层服务和资源。流量控制通常用作分布式系统中的防御机制,以便共享资源可以保持可用性。它还通过限制在给定时间段内可以到达API的请求数量,保护API免受意外或恶意的过度使用。
为什么我们需要流量控制?
流量控制是任何大型系统的一个非常重要的部分,可用于实现以下功能:
- 避免拒绝服务(DoS)攻击导致的资源匮乏。
- 通过对资源的自动扩展设置虚拟上限,流量控制有助于控制运营成本,如果不加以监控,可能会导致指数级花费。
- 流量控制可用于防御或缓解某些常见攻击。
- 对于处理大量数据的API,可以使用流量控制来控制数据流。
算法
API 流量控制有多种算法,各有其优缺点。让我们简要讨论其中一些算法:
漏桶算法
漏桶是一种通过队列提供简单、直观的流量控制方法的算法。注册请求时,系统将其附加到队列的末尾。以规则间隔或先进先出(FIFO)处理队列中的第一项。如果队列已满,则丢弃(或漏出)其他请求。
令牌桶算法
这里使用了桶的概念。当请求到来时,必须从桶中获取并处理令牌。如果桶中没有可用的令牌,请求将被拒绝,请求者必须稍后重试。因此,令牌桶在特定时间段后会被刷新。
固定窗口算法
系统使用 n 秒的窗口大小来跟踪固定窗口算法速率。每个传入请求都会增加窗口的计数器。如果计数器超过阈值,则丢弃请求。
滑动日志算法
滑动日志流量控制涉及跟踪每个请求的时间戳日志。系统将这些日志存储在按时间排序的哈希集或表中。它会丢弃时间戳超过阈值的日志。当新请求到来时,我们计算日志总数以确定请求速率。如果请求将超过阈值速率,则限制该请求。
滑动窗口算法
滑动窗口是一种混合方法,它结合了固定窗口算法的低处理成本和滑动日志的改进边界条件。像固定窗口算法一样,我们跟踪每个固定窗口的计数器。接下来,我们根据当前时间戳计算前一窗口请求速率的加权值,以平滑突发流量。
分布式系统中的流量控制
当涉及分布式系统时,流量控制变得复杂。分布式系统中流量控制带来的两大问题是:
不一致
当使用由多个节点组成的集群时,我们可能需要强制执行全局流量控制策略。因为如果每个节点都要跟踪其流量控制,消费者在向不同节点发送请求时可能会超过全局流量的限制。节点数量越多,用户越有可能超过全局限制。
解决这个问题最简单的方法是在负载均衡器中使用粘滞会话(Sticky Sessions),这样每个消费者只会被发送到一个节点,但这会导致缺乏容错和扩展问题。另一种方法可能是使用像 Redis 这样的集中式数据存储,但这会增加延迟并导致竞争条件(Race conditions)。
竞争条件
当我们使用简单的*“获取然后设置(get-then-set)”*方法时,会出现这个问题,在这种方法中,我们检索当前的流量控制计数器,将其递增,然后将其推回数据存储。该模型的问题是,在执行读增量存储的整个周期所需的时间内,可能会出现额外的请求,每个请求都试图使用无效(较低)计数器值存储增量计数器。这允许消费者发送大量请求以绕过流量控制。
避免此问题的一种方法是在周围使用某种分布式锁机制,防止任何其他进程访问或写入计数器。但锁将成为一个重要的瓶颈,并且无法很好地扩展。更好的方法可能是使用*“先设置后获取(set-then-get)”*方法,允许我们快速增加和检查计数器值,而不让原子操作妨碍。