使用漏桶算法进行速率限制

分布式系统中最常见的可靠性模式之一是限制任务处理的速率。此任务可以是要处理的请求或事件。这样做是为了平滑流量的形状并避免流量突发,或者在底层系统运行时仅允许在任何给定时间进行最大特定数量的操作。速率限制器模式用于负载均衡器、公共 API以及作为不同层的网络策略的一部分。

实现速率限制器的一种方式是一种称为“漏桶”算法的算法。我也看到这在面试中成为编码挑战。尽管我知道该算法,但我自己从未实现过。通常使用算法实现概念验证可以帮助我更好地理解设计决策和注意事项。在写这篇文章时,我借此机会使用 Go 的泛型,我以前也没有使用过。


算法背后的想法很简单:想象有一桶任务正在被传入的任务填充。这个桶底部还有孔,任务可以滴入任务处理器。



描述漏桶算法的图像



存储桶可能处于以下几种状态:空、满或正在处理一些任务但既不空也不满。只要桶没有装满,它就会一直以恒定的速率将任务交给处理器。当它已满时,它应该“溢出”并且在某些任务完成之前不允许传入任务。

这概述了漏桶速率限制器应该如何工作。现在让我们来看看实现。


我通常从定义我想向外界公开的接口开始。这些 API 在开发过程中是流动的,但它让我扎根于抽象层并隐藏了实现的复杂性:


速率限制器的接口定义:


Task

遵循相同的思路,定义用户将与之交互的另一个部分是我的下一步:

漏桶限速器构造函数

选择使用Go 通道从速率限制器提交和读取任务是我做出的选择,因为我认为它会产生一个更简单的实现,更容易在本文中消化,但它可能不是在生产中使用的最佳选择。

通道带来的另一个有趣的特性是用户可以控制任务输入和输出的并发级别。这在语言规范中解释如下:

容量(以元素数量计)设置通道中缓冲区的大小。如果容量为零或不存在,则通道是无缓冲的,并且只有在发送方和接收方都准备好时通信才会成功。否则,如果缓冲区未满(发送)或非空(接收),则通道被缓冲并且通信成功而不会阻塞。

您可以将输出通道的容量视为从桶中滴出的速率任务,而输入通道的容量可以视为桶的大小。当我们达到这些限制时,发送者或处理器将被阻止,直到通道不再满为止。我们将在本文后面看到如何处理存储桶已满的情况。

RateLimiter


我们首先定义内部结构,例如我们在执行期间需要的参数以及保持速率限制器当前状态的其他参数:

intervalpollInterval
running
StartrunningratePerSecond

阻塞来自输入通道的接收操作将是另一种选择,但是在这种情况下,如果用户想要停止速率限制,我们需要关闭输入通道以摆脱阻塞状态。这就是为什么我在这里选择从输入通道中轮询项目的原因。


Stop
running

最后,让我们看一下如何通过示例应用程序使用这个速率限制器:


在构建了速率限制器之后,我们设置了一些东西来为处理任务做准备:一个 goroutine 将任务添加到输入通道中,另一个控制何时停止处理以避免无限期地运行示例程序。

select

还有其他策略,例如“令牌桶”,可以替代漏桶算法。如果你有一个有趣的速率限制策略,我可以通过评论告诉我,接下来我可以查看并实施。

你可以在这里找到完整的代码。