对于大型项目,通常最好使用 Cloudflare Rate Limiting 或 HAProxy 等工具。这些功能强大、可靠,可以为您处理繁重的工作。
但是对于较小的项目——或者如果您想了解事情是如何工作的——您可以在代码中创建自己的速率限制器。为什么?
在本指南结束时,您将了解如何在 TypeScript 中构建基本的节流器以保护您的 API 不被淹没。我们将介绍以下内容:
本指南旨在作为一个实用的起点,非常适合想要学习基础知识而又不想太复杂的开发人员。 但它还没有准备好生产。
在开始之前,我想对 Lucia 的速率限制部分给予正确的评价。
让我们定义 Throttler 类:
export class Throttler { private storage = new Map(); constructor(private timeoutSeconds: number[]) {} }
Throttler 构造函数接受超时持续时间 (timeoutSeconds) 列表。每次用户被阻止时,持续时间都会根据此列表逐渐增加。最终,当达到最终超时时,您甚至可以触发回调以永久禁止用户的 IP - 尽管这超出了本指南的范围。
以下是创建阻止用户增加间隔的节流器实例的示例:
const throttler = new Throttler([1, 2, 4, 8, 16]);
此实例第一次会阻止用户一秒钟。两人第二次,以此类推。
我们使用 Map 来存储 IP 地址及其相应的数据。映射是理想的选择,因为它可以有效地处理频繁的添加和删除。
专业提示:使用地图来处理经常变化的动态数据。对于静态、不变的数据,对象更好。 (兔子洞1)
当您的端点收到请求时,它会提取用户的 IP 地址并咨询 Throttler 以确定是否应允许该请求。
案例 A:新用户或不活跃用户
如果在 Throttler 中未找到该 IP,则这可能是用户的第一个请求,或者他们已经处于不活动状态足够长的时间。在这种情况下:
案例 B:活跃用户
如果找到该IP,则说明该用户之前曾发出过请求。这里:
在后一种情况下,我们需要检查自上一个块以来是否已经过去了足够的时间。通过索引,我们知道应该引用哪个 timeoutSeconds。如果没有,只需反弹即可。否则更新时间戳。
export class Throttler { // ... public consume(key: string): boolean { const counter = this.storage.get(key) ?? null; const now = Date.now(); // Case A if (counter === null) { // At next request, will be found. // The index 0 of [1, 2, 4, 8, 16] returns 1. // That's the amount of seconds it will have to wait. this.storage.set(key, { index: 0, updatedAt: now }); return true; // allowed } // Case B const timeoutMs = this.timeoutSeconds[counter.index] * 1000; const allowed = now - counter.updatedAt >= timeoutMs; if (!allowed) { return false; // denied } // Allow the call, but increment timeout for following requests. counter.updatedAt = now; counter.index = Math.min(counter.index 1, this.timeoutSeconds.length - 1); this.storage.set(key, counter); return true; // allowed } }
更新索引时,上限为timeoutSeconds的最后一个索引。如果没有它,counter.index 1 就会溢出,接下来访问 this.timeoutSeconds[counter.index] 将导致运行时错误。
此示例演示如何使用 Throttler 来限制用户调用 API 的频率。如果用户发出太多请求,他们会收到错误,而不是运行主逻辑。
const throttler = new Throttler([1, 2, 4, 8, 16, 30, 60, 300]); export async function GET({ getClientAddress }) { const IP = getClientAddress(); if (!throttler.consume(IP)) { throw error(429, { message: 'Too Many Requests' }); } // Read from DB, call OpenAI - do the thing. return new Response(null, { status: 200 }); }
当对登录系统使用速率限制时,您可能会遇到这个问题:
为了防止这种情况,请使用用户的唯一用户ID而不是他们的IP进行速率限制。此外,您必须在成功登录后重置节流器状态,以避免不必要的阻塞。
在 Throttler 类中添加一个重置方法:
export class Throttler { // ... public reset(key: string): void { this.storage.delete(key); } }
登录成功后使用:
const user = db.get(email); if (!throttler.consume(user.ID)) { throw error(429); } const validPassword = verifyPassword(user.password, providedPassword); if (!validPassword) { throw error(401); } throttler.reset(user.id); // Clear throttling for the user
当您的节流器跟踪 IP 和速率限制时,重要的是要考虑如何以及何时删除不再需要的 IP 记录。如果没有清理机制,您的节流器将继续在内存中存储记录,随着数据的增长,可能会导致性能问题。
为了防止这种情况,您可以实现清理功能,在一定时间不活动后定期删除旧记录。以下是如何添加简单的清理方法以从节流器中删除陈旧条目的示例。
export class Throttler { // ... public cleanup(): void { const now = Date.now() // Capture the keys first to avoid issues during iteration (we use .delete) const keys = Array.from(this.storage.keys()) for (const key of keys) { const counter = this.storage.get(key) if (!counter) { // Skip if the counter is already deleted (handles concurrency issues) return } // If the IP is at the first timeout, remove it from storage if (counter.index == 0) { this.storage.delete(key) continue } // Otherwise, reduce the timeout index and update the timestamp counter.index -= 1 counter.updatedAt = now this.storage.set(key, counter) } } }
安排清理的一个非常简单的方法(但可能不是最好的)是使用 setInterval:
const throttler = new Throttler([1, 2, 4, 8, 16, 30, 60, 300]) const oneMinute = 60_000 setInterval(() => throttler.cleanup(), oneMinute)
这种清理机制有助于确保您的节流器不会无限期地保留旧记录,从而保持应用程序的高效。虽然这种方法简单且易于实现,但可能需要针对更复杂的用例进一步细化(例如,使用更高级的调度或处理高并发)。
通过定期清理,您可以防止内存膨胀,并确保一段时间内未尝试发出请求的用户不再被跟踪 - 这是使您的速率限制系统可扩展且资源高效的第一步。
如果您喜欢冒险,您可能有兴趣阅读属性的分配方式及其变化方式。另外,为什么不考虑诸如内联缓存之类的虚拟机优化,单态性特别青睐这种优化。享受。 ↩
免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。
Copyright© 2022 湘ICP备2022001581号-3