每日发送数十亿请求而不破坏系统:我们的速率限制器实战
在Detectify,我们帮助客户保护其攻击面。为了有效且全面地测试其资产,我们必须向其系统发送大量请求,但这可能带来服务器过载的风险。自然,我们通过速率限制器解决了这一挑战,确保测试在安全进行的同时为客户提供最大价值。
继介绍引擎框架和深入探讨监控客户攻击面的技术后,本文揭示了拼图的重要部分:我们的速率限制器服务如何工作以使所有Detectify扫描安全。
速率限制器的需求
在之前的文章中,我们解释了用于平滑引擎在客户系统上执行安全测试曲线的技术,避免可能过载其服务器的峰值。随着我们不断向研究库存添加更多安全测试,并且多个引擎同时工作,可以想象所有测试的组合负载可能达到显著水平,并可能为客户带来问题。
为此,我们引入了全局速率限制器,旨在限制针对任何给定目标的每秒最大请求数。设置了合理的默认值,客户可以根据需要灵活配置此限制。
产品需求
在深入速率限制器解决方案之前,理解需求至关重要。速率限制作为概念在软件工程中并不新鲜,有许多工具可用于应对速率限制挑战。然而,我们需要确定是否有任何独特之处需要自定义解决方案,或者现成产品是否足够。
我们探索了几种流行的开源工具和基于云的解决方案。不幸的是,在分析过程中,我们找不到符合我们标准的选项。Detectify基于来源(方案、主机名和端口号的组合)应用限制,并需要允许个别目标和高度动态的配置。大多数现有工具在这方面不足,导致我们决定实现自己的速率限制器。
文献研究
速率限制是软件工程中的知名概念。尽管我们构建了自己的,但不必从零开始。我们研究了各种类型的速率限制,包括阻塞、节流和整形。我们还探索了最常用的算法,如令牌桶、漏桶、固定窗口计数器和滑动窗口计数器等。此外,我们研究了可用的不同拓扑。
我们的目标是尽可能保持实现简单,同时满足需求。最终,我们决定实现阻塞令牌桶速率限制器。有趣的是,鉴于我们的安全测试需求以及一些安全测试在“堆栈深处”的方式,我们选择了服务准入方法而非更常见的代理方法。
简而言之,阻塞速率限制器在超过限制时拒绝目标的请求。相比之下,节流和整形速率限制器通过减慢、延迟或降低其优先级来管理请求。令牌桶算法通过为每个目标维护一个“桶”来工作,该桶以时钟频率补充令牌,数量等于达到限制所需。每个请求消耗桶中的一个令牌以获得准入。当桶耗尽令牌时,请求被拒绝,直到桶重新填充。服务准入方法意味着想要对目标执行安全测试的引擎首先需要从全局速率限制器获得准入,而代理方法将作为引擎和目标之间请求的更透明“中间件”。
技术选择
定义了算法和拓扑后,是时候探索最能满足我们需求的技术了。全局速率限制器需要处理高吞吐量的请求,具有显著的并发水平,同时以极低延迟运行并可扩展。
我们进一步扩展了这些需求,并确定解决方案应在内存中运行,并涉及尽可能少的内部操作。例如,利用原子操作和具有过期策略的简单锁。在热点并发区域,我们选择了单线程方法,顺序运行,避免并发控制的开销。
讨论选项后,我们得出结论,最佳解决方案是在由Redis分片集群支持的长寿命ECS任务上运行全局速率限制器服务。由于我们使用AWS,我们发现使用ElastiCache创建Redis分片集群很方便。
代码展示!
全局速率限制器服务相当直接,提供了一个简单的API用于请求目标准入。更有趣的方面在于服务和Redis之间令牌桶算法的实现。
我们旨在利用原子操作和具有过期策略的简单锁,同时在高并发区域顺序运行任务。这种顺序执行在Redis中很直接,因为它以单线程方式运行。我们的重点是将并发挑战放在它身上。具有过期策略的简单锁在Redis中很方便,这是它擅长的领域之一。此时,我们只需专注于设计算法,尽可能减少服务-Redis交互并尽可能原子化。经过几次迭代,我们确定了一个在Redis服务器上运行Lua脚本的解决方案。Redis保证脚本的原子执行,完美符合要求。
让我们看看代码,随后有详细解释:
|
|
脚本接受几个参数:桶名称及其令牌限制值。至于时间窗口,我们只使用1秒时间窗口。然后,进入检查准入,我们首先尝试从桶中减少一个令牌。如果有可用令牌,我们准入请求。否则,我们检查是否是时候重新填充桶。为此,我们使用Redis的能力设置锁键(如果不存在),并提供1秒时间窗口作为过期时间。如果我们成功设置锁键,意味着我们进入了新的时间窗口,可以重新填充桶,同时返回批准进行。如果我们没有足够的令牌,并且还不能重新填充桶,我们拒绝请求。令牌键也有过期时间,以便在一段时间内没有对目标的请求后不必进行额外清理。
性能如何?
自其诞生以来,我们对性能感到满意。平均而言,它每秒处理20K请求,偶尔峰值高达40K请求每秒。p99延迟通常低于4毫秒,错误率接近0%。
与可观察性相关的一个有趣挑战是确定对单个目标执行了多少请求。对于熟悉时间序列数据库的人来说,使用目标作为标签将不起作用,导致基数爆炸。另一种选择可能是依赖日志并构建基于日志的指标,但如果看看我们处理的量,可以想象在财务方面将极其昂贵。
为了应对这一挑战,我们必须创造性思考。经过一些构思,我们决定在桶重新填充时记录。虽然这种方法不提供对目标的确切请求数,但它指示了在不同时间点可以发送到目标的最大请求数。这对我们来说是基本信息,因为它允许我们监控并确保不超过指定限制。
更多引擎和测试,更安全的客户
保护客户安全是关于实现完美的技术平衡,以确保在其攻击面上运行安全测试的安全方式,而不会导致意外负载问题并可能影响其业务。我们全局速率限制器的实现使我们能够安全地增加库存中的引擎和安全测试数量,运行更多安全测试而不破坏其系统。
从工程角度来看,实现全局速率限制器提出了一个有趣的技术挑战,我们找到了一个适合我们的解决方案。如果出现任何变化,由于整个过程中的广泛构思,我们准备好适应以确保为客户提供安全可靠的体验。也就是说,去黑客自己吧!