ratelimiter

package module
v0.0.0-...-e49c37e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 5, 2026 License: MIT Imports: 4 Imported by: 0

README

Rate Limiter

Go-библиотека для ограничения частоты запросов (rate limiting) с поддержкой 5 классических алгоритмов. Использует Redis и Lua-скрипты для атомарных операций в распределённой среде.

Зачем нужен Rate Limiter

Проблемы без Rate Limiting
  • Неравномерная нагрузка — «шумные соседи» потребляют львиную долю ресурсов и мешают другим клиентам
  • Перерасход ресурсов — бесконтрольное потребление API ведёт к лишним затратам
  • Каскадные отказы — один клиент может перегрузить сервис и вызвать падение для всех
  • DDoS-атаки — вредоносный трафик перегружает систему
Преимущества Rate Limiting
  • Предсказуемость — стабильная производительность для всех пользователей
  • Экономия — контроль использования платных API и инфраструктуры
  • Справедливое распределение — квоты для каждого клиента (IP, user ID, API key)
  • Защита сервисов — предотвращение перегрузки и отказоустойчивость

Подробнее о rate limiting в системном дизайне: Rate Limiter — System Design Space

Реализованные алгоритмы

Библиотека поддерживает 5 классических алгоритмов rate limiting. Выбор зависит от требований к точности, памяти и поведению при всплесках трафика.

Алгоритм Когда использовать Память Burst Точность
Token Bucket Допустимы кратковременные всплески 2 числа на ключ Высокая
Leaky Bucket Нужен равномерный поток запросов 2 числа на ключ Высокая
Fixed Window Простые лимиты, допустима boundary burst 1 число на ключ На границе окна ±100% на границе
Sliding Window Log Максимальная точность, меньше ограничений по памяти O(N) записей Высокая
Sliding Window Counter Баланс точности и памяти 1 число на ключ Частичный Хорошая
Token Bucket

Ведро с токенами пополняется с постоянной скоростью. Каждый запрос забирает один токен.

Плюсы: Эффективность по памяти (2 числа на пользователя), позволяет burst-трафик, простая реализация.
Минусы: Race conditions в distributed без атомарности (решено Lua-скриптами), сложно подобрать размер bucket.

Подходит для: API с допустимыми кратковременными всплесками (например, 100 req/min с burst до 20).

Leaky Bucket

Запросы «протекают» через условное отверстие с постоянной скоростью. Сглаживает трафик.

Плюсы: Равномерный поток запросов, защита от burst.
Минусы: Может замедлять легитимных пользователей при всплесках.

Подходит для: Систем, где важна равномерность нагрузки (очереди обработки, защита downstream-сервисов).

Fixed Window Counter

Счётчик запросов в фиксированном временном окне (например, минута). На границе окна сбрасывается.

Плюсы: Минимальная память, очень быстрая реализация (INCR в Redis).
Минусы: Boundary burst — в конце одного окна и начале другого можно сделать до 2× лимита запросов.

Подходит для: Простых лимитов, где допустима некоторая погрешность на границах окон.

Sliding Window Log

Хранит timestamp каждого запроса в sorted set. Окно «скользит» вместе с временем.

Плюсы: Максимальная точность, нет boundary burst.
Минусы: Память O(N), где N — количество запросов в окне.

Подходит для: Строгих лимитов, когда точность важнее памяти.

Sliding Window Counter

Гибрид: использует приближение на основе текущего и предыдущего окна. Один счётчик в Redis.

Плюсы: Хороший баланс точности и памяти (1 ключ на пользователя).
Минусы: Аппроксимация — не идеальная точность.

Подходит для: Production-сценариев с большим числом ключей, когда нужен компромисс точности и памяти.

Возможности

  • 5 алгоритмов: Token Bucket, Leaky Bucket, Fixed Window, Sliding Window Log, Sliding Window Counter
  • Распределённый режим — Redis + Lua-скрипты для атомарности
  • Множественные лимиты (например, 100 req/min и 1000 req/hour для одного ключа)
  • Информативный результат: Allowed, Remaining, RetryAfter
  • Минимальные зависимости: только Redis client

Требования

  • Go 1.22+
  • Redis 6+

Установка

go get github.com/Rinsvent/rate-limiter

Быстрый старт

package main

import (
    "context"
    "log"
    "time"

    "github.com/Rinsvent/rate-limiter"
    "github.com/redis/go-redis/v9"
)

func main() {
    rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
    defer rdb.Close()

    limiter, err := ratelimiter.NewLimiter("token-bucket", rdb, []ratelimiter.Rate{
        {Duration: time.Minute, Value: 100},
    })
    if err != nil {
        log.Fatal(err)
    }

    ctx := context.Background()
    result, err := limiter.Allow(ctx, "user:123")
    if err != nil {
        log.Fatal(err)
    }

    if !result.Allowed {
        log.Printf("Rate limit exceeded. Retry after %v", result.RetryAfter)
        return
    }

    log.Printf("Request allowed. Remaining: %d", result.Remaining)
}

API

Создание лимитера
limiter, err := ratelimiter.NewLimiter(algorithm, redisClient, rates)

Алгоритмы: "token-bucket", "leaky-bucket", "fixed-window-counter", "sliding-window-log", "sliding-window-counter"

Rates — массив лимитов (применяются по AND: запрос должен пройти все):

rates := []ratelimiter.Rate{
    {Duration: time.Minute, Value: 100},   // 100 req/min
    {Duration: time.Hour, Value: 1000},    // 1000 req/hour
}
Проверка запроса
result, err := limiter.Allow(ctx, key)
  • key — идентификатор (IP, user ID, API key и т.д.)
  • Result.Allowed — разрешён ли запрос
  • Result.Remaining — оставшиеся запросы в текущем окне
  • Result.RetryAfter — сколько ждать до повторной попытки (если Allowed=false)
HTTP-интеграция

Для ответа 429 Too Many Requests:

if !result.Allowed {
    w.Header().Set("Retry-After", strconv.Itoa(int(result.RetryAfter.Seconds())))
    http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
    return
}
w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(result.Remaining))

Примеры

# Redis должен быть запущен (docker compose up -d)
go run ./examples/basic

Docker

docker compose up -d
# Redis на localhost:6379

Тесты

# Требуется Redis на localhost:6379
make test

# Покрытие
make test-coverage

Лицензия

MIT — см. LICENSE.

Ссылки

Contributing

Приветствуются pull request'ы! Подробнее: CONTRIBUTING.md

Перед отправкой:

  1. Запустите make test
  2. Добавьте тесты для новой функциональности
  3. Соблюдайте стиль кода (gofmt)
Как внести вклад

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type FixedWindowCounter

type FixedWindowCounter struct {
	// contains filtered or unexported fields
}

FixedWindowCounter реализация fixed window counter

func NewFixedWindowCounter

func NewFixedWindowCounter(client *redis.Client, rates []Rate) (*FixedWindowCounter, error)

NewFixedWindowCounter создает новый fixed window counter

func (*FixedWindowCounter) Allow

func (fwc *FixedWindowCounter) Allow(ctx context.Context, key string) (*Result, error)

Allow проверяет, разрешен ли запрос

type LeakyBucket

type LeakyBucket struct {
	// contains filtered or unexported fields
}

LeakyBucket реализация leaky bucket

func NewLeakyBucket

func NewLeakyBucket(client *redis.Client, rates []Rate) (*LeakyBucket, error)

NewLeakyBucket создает новый leaky bucket

func (*LeakyBucket) Allow

func (lb *LeakyBucket) Allow(ctx context.Context, key string) (*Result, error)

Allow проверяет, разрешен ли запрос

type Limiter

type Limiter interface {
	Allow(ctx context.Context, key string) (*Result, error)
}

Limiter интерфейс для rate limiter

func NewLimiter

func NewLimiter(algorithm string, client *redis.Client, rates []Rate) (Limiter, error)

NewLimiter создает limiter по алгоритму

type Rate

type Rate struct {
	Duration time.Duration
	Value    int
}

Rate конфигурация rate

type Result

type Result struct {
	// Allowed true если запрос разрешен
	Allowed bool
	// Remaining оставшееся количество запросов в текущем окне
	Remaining int
	// RetryAfter время ожидания до следующей попытки (если Allowed=false)
	RetryAfter time.Duration
}

Result результат проверки rate limit

type SlidingWindowCounter

type SlidingWindowCounter struct {
	// contains filtered or unexported fields
}

SlidingWindowCounter реализация sliding window counter

func NewSlidingWindowCounter

func NewSlidingWindowCounter(client *redis.Client, rates []Rate) (*SlidingWindowCounter, error)

NewSlidingWindowCounter создает новый sliding window counter

func (*SlidingWindowCounter) Allow

func (swc *SlidingWindowCounter) Allow(ctx context.Context, key string) (*Result, error)

Allow проверяет, разрешен ли запрос

type SlidingWindowLog

type SlidingWindowLog struct {
	// contains filtered or unexported fields
}

SlidingWindowLog реализация sliding window log

func NewSlidingWindowLog

func NewSlidingWindowLog(client *redis.Client, rates []Rate) (*SlidingWindowLog, error)

NewSlidingWindowLog создает новый sliding window log

func (*SlidingWindowLog) Allow

func (swl *SlidingWindowLog) Allow(ctx context.Context, key string) (*Result, error)

Allow проверяет, разрешен ли запрос

type TokenBucket

type TokenBucket struct {
	// contains filtered or unexported fields
}

TokenBucket реализация token bucket

func NewTokenBucket

func NewTokenBucket(client *redis.Client, rates []Rate) (*TokenBucket, error)

NewTokenBucket создает новый token bucket

func (*TokenBucket) Allow

func (tb *TokenBucket) Allow(ctx context.Context, key string) (*Result, error)

Allow проверяет, разрешен ли запрос

Directories

Path Synopsis
examples
basic command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL