Что такое Rate Limiter и какие подходы к его реализации существуют в Go?

Ответ

Rate Limiter (ограничитель скорости) — это механизм контроля частоты выполнения каких-либо операций, например, количества HTTP-запросов от одного клиента за определенный промежуток времени. Его основная цель — защита сервиса от перегрузки и злоупотреблений (DDoS, брутфорс).

Основные алгоритмы:


  1. Token Bucket (Ведро с токенами)

    Самый популярный алгоритм. "Ведро" имеет определенную емкость (burst) и пополняется токенами с фиксированной скоростью (rate). Каждый запрос "забирает" один токен. Если токенов нет — запрос отклоняется или ожидает.



  2. Leaky Bucket (Дырявое ведро)

    Запросы помещаются в очередь (ведро). Они обрабатываются с постоянной скоростью, как вода, вытекающая через отверстие. Если очередь переполнена, новые запросы отбрасываются. Сглаживает пики запросов.



  3. Fixed Window Counter (Счетчик в фиксированном окне)

    Простой алгоритм. Счетчик запросов обнуляется в начале каждого временного окна (например, каждую минуту). Минус: возможен всплеск нагрузки на стыке окон (например, двойной лимит за короткое время).



  4. 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.