请求向导:像专家一样掌握速率限制的艺术

本文深入探讨了速率限制算法,包括固定窗口计数器、滑动窗口日志、Token桶和漏桶算法,提供了Node.js代码示例,并讨论了每种算法的适用场景和优缺点。同时,文章还分析了在生产环境中Token桶算法成为最常用选择的原因,以及与其他算法相比的优势,例如灵活性、可扩展性和用户体验。

想象一下,你有一个流畅的服务器——甚至可能是一个区块链强力服务器——但突然冒出一群机器人,把它当成自助餐一样对待。请求像打折店的黑色星期五一样涌入,你可怜的系统喘不过气来。这时,速率限制就闪亮登场了——无名英雄,数字保镖,他说:“对不起,伙计,每分钟五个请求,请取号。” 在本文中,我们将一头扎进速率限制算法的狂野世界,解开它们的秘密,并启动一些巧妙的实现。

速率限制只是一种控制客户端(无论是用户、IP 或某些隐藏的 API 密钥)在设定时间内可以骚扰你的服务器多少次的巧妙方法。它是你抵御过载的盾牌,是你公平使用的通行证,也是你对拒绝服务(DoS)攻击或过度热心的机器人的鄙视。把它想象成交通警察,控制着互联网的混乱。请继续关注我们,到最后,你不仅会明白为什么它会改变游戏规则,还会明白如何像专业人士一样把它应用到你自己的项目中。准备好智胜数字踩踏事件了吗?让我们开始吧。

速率限制的用途是什么?

  • 防止过载: 保护服务器免受过多请求的冲击。
  • 安全性: 减轻滥用,例如暴力攻击或抓取。
  • 公平的资源分配: 确保所有用户公平地访问 API 或服务。
  • 成本管理: 限制按次付费系统中的使用(例如,云 API)。
  • 性能: 在高流量下保持系统稳定。

Twitter 使用速率限制

一个经典的例子是 Twitter 的 API 速率限制。2023 年,Twitter(现在的 X)将免费 API 访问限制为每月 1,500 条推文,并施加了读取限制(例如,经过验证的用户每月 10,000 篇帖子)。这可以防止机器人抓取大量数据集,确保病毒事件期间的服务器稳定性,并将用户推向付费层。如果没有速率限制,单个用户可能会淹没 API,从而降低数百万用户的服务质量。

不同类型的速率限制算法(使用 Node.js 代码)

以下是四种常见的速率限制算法,以及使用 Express.js 的简单 Node.js 实现。

每种算法都假定一个基本的 npm install express 设置。

1. 固定窗口计数器

  • 它的作用: 想象一下,时钟每分钟都从零开始。你可以在该分钟内获得,比如,5 个允许的请求。如果你用完了全部 5 个,那么你将被阻止,直到下一分钟重新开始。
  • 缺点: 如果你在分钟结束前用完了全部 5 个,你可以在之后立即偷偷摸摸地再用 5 个,从而在短时间内翻倍。

何时使用

  • 流量低的简单系统: 适用于具有可预测、适中请求速率的基本 API 或区块链节点(例如,小型 DApp 的后端)。
  • 对成本敏感的场景: 最小的内存和计算开销使其对于资源受限的环境(如轻量级区块链网关)来说很便宜。
  • 粗略控制即可: 当精确度不重要,并且你可以容忍窗口边缘的潜在突发时。

区块链示例: 限制托管区块链服务上的钱包创建请求(例如,每个 IP 每小时 10 个钱包),其中确切的时间安排不是问题。

避免在以下情况下使用: 你需要平稳地处理突发流量或防止边缘情况下的过载(例如,晚上 11:59 的 5 个请求,凌晨 12:00 的另外 5 个请求)。

const express = require('express');
const app = express();

const requests = {};
const WINDOW_SIZE = 60 * 1000; // 60 秒
const MAX_REQUESTS = 5;

app.use((req, res, next) => {
    const now = Date.now();
    const userId = req.ip; // 使用 IP 作为标示符
    if (!requests[userId] || requests[userId].windowStart < now - WINDOW_SIZE) {
        requests[userId] = { count: 1, windowStart: now };
    } else if (requests[userId].count < MAX_REQUESTS) {
        requests[userId].count++;
    } else {
        return res.status(429).send('Too Many Requests');
    }
    next();
});

app.get('/', (req, res) => res.send('Hello!'));
app.listen(3000, () => console.log('Server on port 3000'));

2. 滑动窗口日志

它的作用: 想象一下一个日记本,你每次发出请求时都会记下来。你被允许在过去 60 秒内发出 5 个请求,无论它们是什么时候发生的。旧的条目会随着时间的推移而被擦除。

何时使用:

  • 需要高精度: 非常适合需要精确速率执行的场景,例如限制智能合约调用以防止漏洞利用。
  • 安全关键型系统: 通过确保没有突发流量通过,即使跨越时间边界,也可以使用它来阻止滥用(例如,NFT 铸造上的机器人垃圾邮件)。
  • 小型用户群: 当由于客户端较少而内存使用不是可伸缩性问题时,效果很好。

区块链示例: 速率限制对以太坊节点的 API 调用(例如,每个密钥每小时 100 个请求),以准确地提供历史交易数据而不会过载。

避免在以下情况下使用: 你正在处理数百万用户(例如,公共区块链浏览器),因为存储时间戳在内存和成本方面扩展性不佳。

const express = require('express');
const app = express();

const requestLog = {};
const WINDOW_SIZE = 60 * 1000; // 60 秒
const MAX_REQUESTS = 5;

app.use((req, res, next) => {
    const now = Date.now();
    const userId = req.ip;
    requestLog[userId] = requestLog[userId] || [];
    requestLog[userId] = requestLog[userId].filter(ts => ts > now - WINDOW_SIZE);
    if (requestLog[userId].length < MAX_REQUESTS) {
        requestLog[userId].push(now);
        next();
    } else {
        res.status(429).send('Too Many Requests');
    }
});

app.get('/', (req, res) => res.send('Hello!'));
app.listen(3000);

3. Token桶

  • 它的作用: 想象一下一个装有Token的桶——比如说,开始时有 5 个。每个请求消耗一个Token。桶会缓慢地重新填充(比如每秒 1 个Token)。如果Token用完,你就需要等待。
  • 缺点: 你需要注意重新填充的时间安排,这会增加一些工作量。

何时使用:

  • 具有灵活性的突发流量: 非常适合需要容纳偶尔的峰值(例如,DeFi 交易的突然激增)同时保持总体速率的系统。
  • 用户友好的体验: 允许短时间突发而不会被拒绝,从而改善合法用户的用户体验(例如,DEX 上的交易者)。
  • 可伸缩的 API: 由于其简单性和有效性的平衡,被广泛用于区块链中间件(例如,Infura、Alchemy)。

区块链示例: 限制向Layer2汇总提交交易(例如,5 个 tx/秒,带有 20 个Token的桶),以处理代币销售期间的峰值负载。

避免在以下情况下使用: 你需要严格、平稳的输出(没有突发),或者计算资源非常有限,因为重新填充逻辑会增加轻微的复杂性。

const express = require('express');
const app = express();

const buckets = {};
const CAPACITY = 5;
const REFILL_RATE = 1; // 每秒 1 个Token

app.use((req, res, next) => {
    const now = Date.now() / 1000; // 以秒为单位
    const userId = req.ip;
    buckets[userId] = buckets[userId] || { tokens: CAPACITY, lastRefill: now };

    const timePassed = now - buckets[userId].lastRefill;
    const newTokens = timePassed * REFILL_RATE;
    buckets[userId].tokens = Math.min(CAPACITY, buckets[userId].tokens + newTokens);
    buckets[userId].lastRefill = now;

    if (buckets[userId].tokens >= 1) {
        buckets[userId].tokens--;
        next();
    } else {
        res.status(429).send('Too Many Requests');
    }
});

app.get('/', (req, res) => res.send('Hello!'));
app.listen(3000);

4. 漏桶

  • 它的作用: 想象一个底部有一个小孔的桶。请求涌入,但它们只以稳定的速度泄漏出来(比如每秒 1 个)。如果一次涌入的请求过多,桶会溢出,多余的请求会被丢弃。
  • 缺点: 如果你赶时间,一些请求可能会被丢弃而不是等待。

何时使用:

  • 需要平稳的输出: 非常适合需要一致请求速率的下游系统,比如为区块链预言机提供价格数据。
  • 流量整形: 使用它来防止突然的峰值冲击区块链节点或智能合约,从而确保稳定性。
  • 队列容忍度: 当可以接受延迟请求,但丢弃请求不是灾难性的时,效果很好。

区块链示例: 限制对比特币节点的块数据查询(例如,1 个查询/秒),以避免在高需求期间压垮对等网络。

避免在以下情况下使用: 丢弃请求是不可接受的(例如,关键的交易提交),或者你需要允许突发以改善用户体验。

const express = require('express');
const app = express();

const queues = {};
const QUEUE_SIZE = 5;
const LEAK_RATE = 1000; // 1 个请求/秒

app.use((req, res, next) => {
    const now = Date.now();
    const userId = req.ip;
    queues[userId] = queues[userId] || { queue: [], lastLeak: now };
    const timeSinceLeak = now - queues[userId].lastLeak;
    const leaks = Math.floor(timeSinceLeak / LEAK_RATE);
    if (leaks > 0) {
        queues[userId].queue = queues[userId].queue.slice(leaks);
        queues[userId].lastLeak += leaks * LEAK_RATE;
    }

    if (queues[userId].queue.length < QUEUE_SIZE) {
        queues[userId].queue.push(now);
        next();
    } else {
        res.status(429).send('Too Many Requests');
    }
});

app.get('/', (req, res) => res.send('Hello!'));
app.listen(3000);

5. 滑动窗口计数器

  • 它的作用: 想象一下一个 60 秒的窗口,分成 10 秒的小块。每个块都统计请求(例如,这里 2 个,那里 3 个)。你将过去 60 秒内的小块加起来,目标是总共 5 个。它会随着时间的推移而滑动。
  • 缺点: 比固定窗口更棘手,因为你要处理多个块。

何时使用: 中等精度,但比滑动窗口日志具有更好的内存效率,适用于具有可变流量的 API(例如,提供块数据的区块链浏览器)。

const express = require('express');
const app = express();

const windows = {};
const WINDOW_SIZE = 60 * 1000; // 60 秒
const SUB_WINDOW = 10 * 1000; // 10 秒
const MAX_REQUESTS = 5;

app.use((req, res, next) => {
    const now = Date.now();
    const userId = req.ip;
    windows[userId] = windows[userId] || { slots: {}, lastUpdate: now };

    // 清理旧的子窗口
    const cutoff = now - WINDOW_SIZE;
    for (let ts in windows[userId].slots) {
        if (parseInt(ts) < cutoff) delete windows[userId].slots[ts];
    }

    // 当前的子窗口
    const currentSlot = Math.floor(now / SUB_WINDOW) * SUB_WINDOW;
    windows[userId].slots[currentSlot\

确定生产环境中“最常用”的速率限制算法,尤其是在区块链应用或通用系统中,是很棘手的,因为它很大程度上取决于具体的用例、系统架构和行业背景。没有一项通用的调查能够确定所有生产系统中的确切使用情况统计数据,但根据广泛的采用、实际优势和实际示例(包括区块链),Token桶算法脱颖而出,成为生产中最常用的算法。以下是原因,以及对比其他算法的介绍。

Token桶:生产中的最爱

为什么使用最多:

  • 具有突发性的灵活性: 它允许受控数量的请求突发通过(高达桶的容量),同时随着时间的推移执行稳定的速率。这非常适合 API 或区块链系统,在这些系统中,用户可能会发送大量请求(例如,在代币销售期间),但仍然需要长期限制。
  • 可扩展性: 它的内存效率很高——每个用户只跟踪Token计数和上次重新填充时间——并且与 Redis 等分布式系统配合良好,Redis 是生产中速率限制的主要工具。
  • 实际应用: 主要云提供商,如 Amazon API GatewayGoogle Cloud Endpoints 和区块链 API 提供商,如 Infura,都使用受Token桶启发的方案。例如,Infura 对以太坊 API 调用的 100,000 个请求/天限制符合基于Token的模型,该模型会逐渐重新填充。
  • 易于调整: 调整桶大小和重新填充速率可以让工程师平衡严格性和用户体验,使其能够适应各种需求——比如限制区块链上的交易提交。
  • 区块链契合: 在区块链中,它非常适合处理突然的峰值,同时防止网络过载,这是为去中心化应用程序(DApp)提供服务的节点或 API 的常见问题。

为什么其他算法使用较少?

固定窗口计数器

  • 使用情况: 在更简单的设置中很常见,比如 NGINX 的 limit_req 模块 或基本的区块链中间件。
  • 为什么不太受欢迎: 它容易出现“边缘突发”——例如,晚上 11:59 的 5 个请求和凌晨 12:00 的另外 5 个请求绕过了预期的限制。这使得它在高风险生产系统中不太可靠,这些系统需要精确控制,比如高峰时段的区块链 API。
  • 仍然使用: 适用于简单重于精度的低预算或低流量应用程序。

滑动窗口日志

  • 使用情况: 在安全敏感的区块链场景(例如,智能合约调用限制)或高精度 API 中可见。
  • 为什么不太受欢迎: 它很准确,但内存消耗大,需要存储每个请求的时间戳。在生产中,对于数百万用户(想想以太坊节点查询),如果没有大量的优化或昂贵的基础设施,这种方法的可扩展性不好。
  • 利基市场: 用于精度重于资源成本的场合,但未被广泛采用。

漏桶

  • 使用情况: 在流量整形中很受欢迎,比如 Redis 速率限制器 或网络级区块链节点管理。
  • 为什么不太受欢迎: 它强制执行严格、平稳的输出速率,这可能会在突发期间丢弃请求——这对于 DeFi 交易等实时应用程序中的用户体验来说非常糟糕。如果没有同步开销,也很难在分布式系统中实现。
  • 仍然使用: 非常适合稳定流量(例如,预言机更新),但不如Token桶通用。

滑动窗口计数器

  • 使用情况: 在现代系统中(例如,Redis 教程重点介绍)或需要平衡精度的区块链浏览器中涌现。
  • 为什么不太受欢迎: 它是固定窗口和滑动窗口日志之间的折衷方案,但它是较新的,并且在生产中经过的实战测试较少。添加的复杂性(跟踪子窗口)并不总是能证明优于Token桶的优势。
  • 潜力: 越来越受欢迎,但尚未成为主要选择。

为什么Token桶在生产中获胜

  • 实际平衡: 它达到了一个最佳点——足够简单易于实现,可以使用 Redis 等工具进行扩展,并且可以灵活地处理突发流量。区块链应用程序具有不可预测的负载(例如,memecoin 发布),可以在此基础上蓬勃发展。
  • 行业证据: 除了区块链之外,像 GitHub(5,000 个请求/小时的 API 限制)和 Stripe 这样的公司都使用类似Token桶的系统,这表明它在高规模、面向用户的服务中占据主导地位。在区块链中,AlchemyQuickNode 在节点 API 中也采用了这种方法。
  • 用户体验: 与漏桶相比,丢弃的请求更少,避免了固定窗口的边缘问题,从而让用户更满意——这在加密货币等竞争激烈的市场中至关重要。

阅读更多我的文章:

  • 原文链接: medium.com/@universalPho...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
universalPhoton
universalPhoton
江湖只有他的大名,没有他的介绍。