Для крупных проектов обычно лучше использовать такие инструменты, как Cloudflare Rate Limiting или HAProxy. Они мощные, надежные и возьмут на себя всю тяжелую работу.
Но для небольших проектов — или если вы хотите узнать, как все работает — вы можете создать свой собственный ограничитель скорости прямо в своем коде. Почему?
К концу этого руководства вы узнаете, как создать базовый дроссель в TypeScript, чтобы защитить ваши API от перегрузки. Вот что мы рассмотрим:
Это руководство задумано как практическая отправная точка и идеально подходит для разработчиков, которые хотят изучить основы без ненужных сложностей. Но он не готов к производству.
Прежде чем начать, я хочу отдать должное разделу Люсии «Ограничение скорости».
Давайте определим класс Throttler:
export class Throttler { private storage = new Map(); constructor(private timeoutSeconds: number[]) {} }
Конструктор Throttler принимает список длительности таймаута (timeoutSeconds). Каждый раз, когда пользователь блокируется, продолжительность постепенно увеличивается в зависимости от этого списка. В конце концов, когда истечет последний тайм-аут, вы даже можете запустить обратный вызов, чтобы навсегда заблокировать IP-адрес пользователя, хотя это выходит за рамки данного руководства.
Вот пример создания экземпляра дросселя, который блокирует пользователей с увеличением интервалов:
const throttler = new Throttler([1, 2, 4, 8, 16]);
Этот экземпляр впервые заблокирует пользователей на одну секунду. Второй раз на двоих и так далее.
Мы используем карту для хранения IP-адресов и соответствующих им данных. Карта идеальна, поскольку она эффективно обрабатывает частые добавления и удаления.
Совет для профессионалов: используйте карту для динамических данных, которые часто меняются. Для статических, неизменных данных лучше использовать объект. (Кроличья нора 1)
Когда ваша конечная точка получает запрос, она извлекает IP-адрес пользователя и консультируется с регулятором, чтобы определить, следует ли разрешить запрос.
Случай А: Новый или неактивный пользователь
Если IP-адрес не найден в Throttler, это либо первый запрос пользователя, либо он был неактивен достаточно долго. В этом случае:
Случай Б: Активный пользователь
Если 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 }); }
При использовании ограничения скорости в системах входа вы можете столкнуться с этой проблемой:
Чтобы предотвратить это, для ограничения скорости используйте уникальный идентификатор пользователя вместо его 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