Ответ
Rate Limiter (ограничитель скорости) — это механизм контроля частоты выполнения каких-либо операций, например, количества HTTP-запросов от одного клиента за определенный промежуток времени. Его основная цель — защита сервиса от перегрузки и злоупотреблений (DDoS, брутфорс).
Основные алгоритмы:
-
Token Bucket (Ведро с токенами) Самый популярный алгоритм. "Ведро" имеет определенную емкость (
burst) и пополняется токенами с фиксированной скоростью (rate). Каждый запрос "забирает" один токен. Если токенов нет — запрос отклоняется или ожидает. -
Leaky Bucket (Дырявое ведро) Запросы помещаются в очередь (ведро). Они обрабатываются с постоянной скоростью, как вода, вытекающая через отверстие. Если очередь переполнена, новые запросы отбрасываются. Сглаживает пики запросов.
-
Fixed Window Counter (Счетчик в фиксированном окне) Простой алгоритм. Счетчик запросов обнуляется в начале каждого временного окна (например, каждую минуту). Минус: возможен всплеск нагрузки на стыке окон (например, двойной лимит за короткое время).
-
Sliding Window Log (Скользящее окно) Более сложный и точный. Хранит временные метки запросов и учитывает только те, что попадают в текущее скользящее окно. Решает проблему всплесков на границах, но требует больше памяти.
Реализация в Go:
Стандартным решением является пакет golang.org/x/time/rate, который реализует алгоритм Token Bucket.
import (
"context"
"fmt"
"time"
"golang.org/x/time/rate"
)
func main() {
// Лимитер: 2 запроса в секунду, с "запасом" в 5 запросов.
limiter := rate.NewLimiter(rate.Limit(2), 5)
// Неблокирующая проверка
for i := 0; i < 10; i++ {
if limiter.Allow() {
fmt.Println("Request allowed")
} else {
fmt.Println("Request denied")
}
time.Sleep(200 * time.Millisecond)
}
// Блокирующая проверка (ожидает, пока токен не станет доступен)
ctx := context.Background()
fmt.Println("nWaiting for token...")
if err := limiter.Wait(ctx); err == nil {
fmt.Println("Token acquired, request can proceed.")
}
}
Распределенные системы:
Для нескольких экземпляров сервиса локальные лимитеры неэффективны. В таких случаях используют централизованное хранилище (например, Redis) для хранения счетчиков, реализуя один из алгоритмов на стороне хранилища. Популярные библиотеки: go-redis/redis_rate.
Ответ 18+ 🔞
Так, слушай, про ограничители скорости, а то народ без них как слон в посудной лавке — всё в щепки разнесёт. Rate Limiter, блядь, это такая штука, чтобы клиенты не устроили тебе пиздец, навалив запросов, как говна из экскаватора.
Зачем это надо, ёпта? Чтобы сервис не лег от DDoS или какого-нибудь долбоёба, который пароль методом тыка подбирает. Просто не пускает лишних, и всё.
Какие бывают, эти алгоритмы:
-
Token Bucket (Ведро с токенами) Самый, блядь, популярный. Представь ведро. В нём токены. Оно пополняется с какой-то скоростью. Хочешь запрос — отдай токен. Нет токенов? Иди нахуй, жди или получай отказ. Есть ещё «запас» (burst), можно немного превысить среднюю скорость, если ведро не пустое.
-
Leaky Bucket (Дырявое ведро) Тут запросы — это вода, которую льют в ведро. А обрабатываются они с постоянной скоростью, как будто из дырки вытекают. Ведро переполнилось — новые запросы просто выплескиваются, нахуй не нужны. Хорошо сглаживает пики, чтоб не было резких скачков.
-
Fixed Window Counter (Счётчик в фиксированном окне) Примитив, блядь. «За эту минуту — не больше 100 запросов». Началась новая минута — счётчик обнулился. Минус в том, что на стыке окон можно впихнуть двойную порцию, прям как обожравшийся студент в столовой в последние секунды перед закрытием.
-
Sliding Window Log (Скользящее окно) Умный, но прожорливый до памяти. Хранит метки времени всех запросов и считает только те, что влезли в последние N секунд. Проблему стыка окон решает, но мозги ебёт.
А как на Go это делать?
В Go, слава богу, уже всё придумали. Бери пакет golang.org/x/time/rate — там как раз Token Bucket.
import (
"context"
"fmt"
"time"
"golang.org/x/time/rate"
)
func main() {
// Создаём лимитер: 2 запроса в секунду, но можно "рывком" 5.
limiter := rate.NewLimiter(rate.Limit(2), 5)
// Быстрая проверка без блокировки
for i := 0; i < 10; i++ {
if limiter.Allow() {
fmt.Println("Request allowed")
} else {
fmt.Println("Request denied")
}
time.Sleep(200 * time.Millisecond)
}
// А тут будем ждать, как дураки, пока токен появится
ctx := context.Background()
fmt.Println("nWaiting for token...")
if err := limiter.Wait(ctx); err == nil {
fmt.Println("Token acquired, request can proceed.")
}
}
А если сервисов несколько, распределённых?
А вот тут, сука, начинается веселье. Локальный лимитер на каждом инстансе — это как у каждого охранника своя гостовая книга. Клиент будет ходить вокруг них, как уж на сковородке, и везде получать свой лимит. Надо центральную штуку. Обычно Redis берут, там все счётчики хранят. Есть библиотеки, типа go-redis/redis_rate, которые эту хуйню организуют. Без этого в распределёнке — пизда.