js在线优化(Nextjs性能优化手把手教你写一个内存速率限制器)

js在线优化(Nextjs性能优化手把手教你写一个内存速率限制器)

adminqwq 2026-01-15 信息披露 5 次浏览 0个评论
Next.js性能优化:手把手教你写一个内存速率限制器

本文详细介绍了 API 速率限制器的工作原理及常见算法(固定窗口、滑动窗口、令牌桶),并手把手教你如何在 Next.js App Router 项目中构建一个内存速率限制器。文章还展示了如何使用 Artillery 进行负载测试,确保限流功能的准确性和系统的弹性。

API 速率限制器(Rate Limiter)是 Web 服务的一个服务端组件,用于限制客户端在一定时间内可以对端点发起的 API 请求数量。例如,X(前身为 Twitter)限制特定用户每三小时只能发布 300 条推文。

速率限制器通过阻止超过设定使用限制的请求来强制执行 API 的负责任使用。

通过阅读本文,你将:

了解速率限制器的工作原理在 Next.js App Router 项目中构建一个内存速率限制器使用 Artillery 对速率限制器进行负载测试,以验证其准确性和弹性速率限制器的好处

速率限制器控制在给定时间窗口内允许多少请求。如果你考虑使用它们,你需要了解它们有几个好处。

首先,它们有助于防止 Web 服务器被滥用。速率限制器保护 Web 服务器免受无谓增加负载的过度使用。它们可以阻止来自机器人的拒绝服务(DoS)攻击产生的过多请求,从而使 Web 服务不会因不必要的过载而崩溃,并能继续为合法用户提供服务。

它们还有助于管理使用外部 API 的成本。某些 API 端点会请求外部 API 来完成其操作——例如,通过电子邮件服务提供商发送电子邮件的 API 端点。当端点依赖付费的外部 API 且用户对端点的访问不受限制时,过度使用可能导致 Web 服务的成本增加且昂贵。速率限制器可以阻止此类端点的过度使用,有助于将成本保持在合理的最低水平。

速率限制器如何工作

速率限制器使用三步机制工作。该过程包括跟踪来自特定客户端的请求,监控其使用情况,并在超过阈值后阻止额外的请求。

更详细地说,速率限制器:

跟踪请求:速率限制器记录发起请求的 API 客户端以及特定于客户端的属性(例如 IP 地址或 userId)。这些特定属性是用作识别客户端的引用或键。监控使用情况:根据限流机制,速率限制器会增加或减少用于确定使用阈值的指标。例如,在三小时的时间段内,Twitter 可以跟踪并增加用户对 create tweet 端点发起 API 请求的次数。确保符合阈值:速率限制器检查每个请求的使用阈值。如果已超过,它将阻止请求访问 API 端点的功能,并响应 429 状态码。Next.js性能优化:手把手教你写一个内存速率限制器

限流算法

你可以根据速率限制器的要求使用不同的算法来实现限流。每种限流算法都有其优缺点。以下是一些你可以尝试的流行限流算法。

固定窗口算法 (Fixed Window Algorithm)

在固定窗口限流算法中,跟踪固定时间段内发出的请求数量,每个请求都会增加跟踪的请求计数。如果超过了时间范围内的请求数量,时间范围内进来的任何额外请求都会被阻止。在时间段结束时,请求计数重置,并随每个请求增加。

其机制易于理解,且内存效率高。其挑战在于,接近时间窗口开始或结束时的流量峰值可能允许超过允许的请求数。

滑动窗口算法 (Sliding Window Algorithm)

滑动窗口算法解决了固定窗口算法的问题,即接近时间窗口开始或结束时的流量峰值可能允许超过允许的请求数。

它的工作原理如下:

它在缓存中跟踪所发出请求的时间戳。当有新请求时,它会删除所有早于当前时间窗口开始的时间戳,并将新请求的时间戳追加到缓存中。如果缓存中的请求计数高于阈值,则请求被阻止。否则,它是允许的。

虽然此算法比固定窗口算法更准确,但由于存储时间戳,它消耗更多内存。

令牌桶算法 (Token Bucket Algorithm)

在令牌桶算法中,包含预定义数量令牌的桶被分配给用户。令牌以预定义的速率添加到桶中,例如每秒添加 2 个令牌。

一旦桶满了,就不再添加令牌。每个请求消耗一个或多个令牌,如果令牌耗尽,请求将被阻止,直到桶中再次有令牌。

令牌桶算法的好处是内存效率高,易于实现,并且足够准确,即使在流量突发期间也能阻止额外的请求。

在本教程中,我们将使用固定窗口算法来构建速率限制器。我们还将使用 Artillery 对其进行实战测试,以验证其弹性和准确性。

构建内存速率限制器

如果你是后端开发人员,你可能已经注意到用户有时会滥用 Next.js 应用程序中的重置密码 API 端点。这是一个令人担忧的问题,因为 API 端点会请求你的电子邮件服务提供商发送电子邮件,而你需要为此付费。

因此,你可能希望限制用户对此端点的请求,以防止滥用 API 并节省成本。这就是速率限制器发挥作用的地方。

速率限制器核心逻辑

src/lib/server/rate-limiter.ts 文件导出一个名为 applyRateLimiter 的函数,它接受三个参数:

请求对象响应对象getOptsFn

getOptsFn 是一个函数,它接受请求对象,并在执行时返回特定于请求的属性,用于速率限制器的跟踪、监控和阻止。getOptsFn 是一个函数而不是静态对象,以便请求处理程序可以为每个请求动态创建特定属性。

src/lib/server/rate-limiter.ts 还有一个名为 cache 的内存映射。cache 存储请求的键(或唯一标识符)并将其映射到其使用情况。每分钟运行一个间隔,从缓存中删除 expiredAt 值已过期的键。这有助于管理缓存使用的内存量。

type GetOptionsFn = (req: NextApiRequest) => { key: string; maxTries: number; expiresAt: Date;};const cache = new Map<string, Usage>();// 每分钟清除缓存中的过时键setInterval(() => { const currentDate = new Date(); for (const [key, usage] of cache) { if (!usage) continue; if (currentDate > usage.expiresAt) { cache.delete(key); } }}, 60000);

当速率限制器执行时,它使用 getOptsFn 从请求中生成以下内容:

key:用于跟踪其使用情况的请求唯一标识符maxTries:在指定时间窗口内可以发出的最大次数expiresAt:时间窗口的到期时间const opts = getOptsFn(req);const usage = cache.get(opts.key);if (!usage) { cache.set(opts.key, { tries: 1, maxTries: opts.maxTries, expiresAt: opts.expiresAt, }); return;}

速率限制器随后检查请求的 key 是否存在于缓存中。如果不存在,它将在缓存中设置它。如果请求的键存在于 cache 中,速率限制器会检查缓存中的未阻止尝试次数 (usage.tries) 是否小于允许的使用次数 (usage.maxTries)。如果是 true,意味着请求未超过其最大尝试次数。它还会检查缓存中存储的请求时间窗口的到期时间是否已过。

如果不阻止请求,则以下条件之一为 true:

请求未超过其最大尝试次数且其时间窗口未过缓存中请求使用的当前时间窗口 (usage.expiresAt) 已过const currentDate = new Date();const retryAfter = usage.expiresAt.getTime() - currentDate.getTime();const canProceed = usage.tries < opts.maxTries && retryAfter >= 0;if (canProceed) { cache.set(opts.key, { ...usage, tries: usage.tries + 1, }); return;}if (retryAfter <= 0) { // 如果 usage.expiresAt 已过 cache.set(opts.key, { tries: 1, maxTries: opts.maxTries, expiresAt: opts.expiresAt, }); return;}

如果两个条件都为假,请求将被阻止,并返回 429 响应状态码。

res.setHeader("Retry-After", retryAfter);return res.status(429).json({ error: { message: "Too many requests" },});

根据 REST 规范,429 HTTP 响应可能包含 Retry-After 标头,让客户端知道在发出新请求之前要等待多长时间。

请求处理程序

你可以在 src/pages/api/reset-password-init.ts 中找到重置密码请求处理程序。

generateOptions 是最终作为 getOptsFn 传递给速率限制器的函数。对于此端点,属性如下:

key:格式为 [method].[endpoint].[email] 的字符串。这使其对每个请求都是唯一且特定的。expiresAt:时间窗口到期的时间。maxTries:时间窗口内允许的最大尝试次数。const generateOptions = function (req: NextApiRequest) { const now = new Date(); const inFiveSeconds = new Date(now.getTime() + 5000); return { expiresAt: inFiveSeconds, key: `post.reset-password.${req.body.email.toLowerCase()}`, maxTries: 1, };};

对于重置密码处理程序,请求被限制为每五秒一次。

使用 Artillery 进行弹性负载测试

Artillery 是一个用于测试和报告 Web 应用程序在重负载下性能的工具。

要使用 Artillery,请通过 npm install -g artillery@latest 命令全局安装它。

负载测试配置

在项目根目录的 loadtest/setup.yaml 文件中,包含 Artillery 执行负载测试的指令。指令告诉 Artillery 创建虚拟用户,分三个阶段向应用程序发出 API 请求:

预热 (Warm up):持续 10 秒,从每秒 1 个请求增加到每秒 5 个请求。爬升 (Ramp up):持续 30 秒,从每秒 5 个请求增加到每秒 10 个请求。峰值阶段 (Spike phase):持续 20 秒,从每秒 10 个请求增加到每秒 30 个请求。

总测试时间为 60 秒。

config: target: http://localhost:3000/api phases: - duration: 10 arrivalRate: 1 rampTo: 5 name: Warm up - duration: 30 arrivalRate: 5 rampTo: 10 name: Ramp up - duration: 20 arrivalRate: 10 rampTo: 30 name: Spike phase

plugins 部分包含用于分析结果的扩展指令。例如,ensure 插件包含如果至少 99% 的请求响应延迟为 100ms 或更短则报告“OK”的设置。

运行负载测试

确保应用程序正在运行,并在项目根目录运行以下命令:

artillery run loadtest/setup.yaml --output loadtest/results.json审查结果

无论发出的请求数量如何,我们的速率限制器设置只允许每五秒一个请求。这意味着在六十秒的时间内,应该允许的请求数量是 12 个。

如果你查看 loadtest/results.json,你会看到只有 12 个请求的状态码为 200。这意味着我们的速率限制器即使在高负载下也保持有效和准确。

对于延迟,你应该考虑终端日志中 ensure 插件的报告。

Checks:ok: http.response_time.p95 < 75ok: http.response_time.p99 < 100

这意味着 95% 的请求延迟小于 75 毫秒,99% 的请求延迟小于 100 毫秒。这是很好的结果。

笔者锐评

在微服务和 Serverless 架构盛行的今天,Rate Limiter 是保护后端服务的“防弹衣”。文章中介绍的内存(In-Memory)限流方案非常适合单实例部署或小型应用,简单、快速且零依赖。

但在实际的生产级、多实例(Cluster/Distributed)环境中,内存限流会失效,因为每个实例都有自己的内存缓存,无法共享计数。这时候通常需要引入 Redis 这样的集中式存储来实现分布式限流。

另外,Artillery 是一个非常棒的负载测试工具,它的 YAML 配置方式非常直观,且支持复杂的场景模拟。建议开发者在上线任何关键 API 之前,都像文中一样跑一遍压测,不仅是为了测试限流,更是为了摸清系统的性能底线。

求点赞 求关注 ❤️ 求收藏 ⭐️ 你的支持是我更新的最大动力!

转载请注明来自海坡下载,本文标题:《js在线优化(Nextjs性能优化手把手教你写一个内存速率限制器)》

每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,5人围观)参与讨论

还没有评论,来说两句吧...