Rate Limiting и Throttling в Go: Полный гайд с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Rate Limiting и Throttling в Go: как защитить API и не сойти с ума

Представьте: вы написали идеальное API на Go. Оно летает, обрабатывает запросы за миллисекунды, и тут — бац! — на сервер валится шквал запросов. Может, это бот, может, баг в клиенте, а может, просто хайп вокруг вашего продукта. Без защиты сервер ляжет, пользователи уйдут, а вы будете разбираться в логах до утра.

Знакомо? Тогда rate limiting (ограничение скорости) и throttling (дросселирование) — ваши новые лучшие друзья. В этой статье на pythonlib.ru мы разберём, как реализовать их в Go — языке, который создан для высоконагруженных систем. Никакой воды, только живой код и реальные кейсы.

Rate Limiting vs Throttling: в чём разница?

Для начала давайте наведём порядок в терминах. Многие путают эти понятия, но разница есть.

  • Rate Limiting — это жёсткое ограничение: «Не больше 100 запросов в минуту». Если лимит превышен — сервер отвечает 429 Too Many Requests. Всё честно и строго.
  • Throttling — это мягкое регулирование: «Мы замедлим тебя, но не заблокируем». Например, вместо того чтобы отвечать сразу на все запросы, мы ставим их в очередь и обрабатываем с фиксированной скоростью.

В Go оба подхода реализуются элегантно благодаря горутинам и каналам. Давайте посмотрим на конкретные примеры.

Простейший rate limiter на каналах

Самый базовый способ ограничить скорость — использовать тикер из пакета time. Это как метроном: каждый «тик» разрешает один запрос.

package main

import (
    "fmt"
    "time"
)

func main() {
    // Создаём канал, который будет отправлять токены каждые 200 мс
    limiter := time.Tick(200 * time.Millisecond)

    // Имитируем 10 запросов
    for i := 1; i <= 10; i++ {
        <-limiter // Ждём разрешения
        fmt.Printf("Запрос %d обработан в %s\n", i, time.Now().Format("15:04:05.000"))
    }
}

Что здесь происходит? time.Tick создаёт канал, который каждые 200 миллисекунд отправляет значение. Наша горутина блокируется на <-limiter, пока не получит токен. В итоге — ровно 5 запросов в секунду, без вариантов.

Минус: если запросов меньше, чем тиков, мы просто ждём. Неэффективно? Да. Но для простых сценариев — идеально.

Burst-лимитинг с буфером

А что если нам нужно пропускать «всплески» трафика? Например, разрешить 10 запросов мгновенно, а потом ограничить до 5 в секунду. Для этого используем буферизированный канал — классический Token Bucket.

package main

import (
    "fmt"
    "time"
)

func main() {
    // Буферизированный канал на 5 токенов
    tokens := make(chan struct{}, 5)

    // Горутина-наполнитель: добавляет токен каждые 200 мс
    go func() {
        ticker := time.NewTicker(200 * time.Millisecond)
        defer ticker.Stop()
        for range ticker.C {
            select {
            case tokens <- struct{}{}:
            default:
                // Канал полон — ничего не делаем
            }
        }
    }()

    // Имитация 15 запросов
    for i := 1; i <= 15; i++ {
        <-tokens // Забираем токен (ждём, если пусто)
        fmt.Printf("Запрос %d обработан в %s\n", i, time.Now().Format("15:04:05.000"))
        time.Sleep(50 * time.Millisecond) // Небольшая задержка между запросами
    }
}

Первые 5 запросов пройдут мгновенно (буфер полон), а дальше — по одному каждые 200 мс. Этот паттерн идеально подходит для API, где клиент может внезапно начать слать много запросов (например, после перезапуска).

Throttling через WaitGroup и Worker Pool

Теперь перейдём к throttling. Допустим, у нас есть очередь задач (например, отправка email-рассылки). Мы не хотим блокировать клиента, но и нагружать базу данных 1000 одновременных запросов — плохая идея. Решение — worker pool с фиксированным числом горутин.

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Воркер %d начал задачу %d в %s\n", id, job, time.Now().Format("15:04:05"))
        time.Sleep(1 * time.Second) // Имитация работы
        fmt.Printf("Воркер %d закончил задачу %d\n", id, job)
    }
}

func main() {
    const numWorkers = 3
    jobs := make(chan int, 10)
    var wg sync.WaitGroup

    // Запускаем 3 воркера
    for i := 1; i <= numWorkers; i++ {
        wg.Add(1)
        go worker(i, jobs, &wg)
    }

    // Отправляем 10 задач
    for j := 1; j <= 10; j++ {
        jobs <- j
    }
    close(jobs)

    wg.Wait()
    fmt.Println("Все задачи выполнены")
}

Здесь мы создали 3 воркера, которые одновременно обрабатывают не более 3 задач. Остальные ждут в канале. Это классический throttling: мы не отказываем в обслуживании, но регулируем скорость обработки.

Middleware для HTTP-сервера

Самое частое применение rate limiting — защита HTTP API. В Go это удобно делать через middleware. Вот пример, который ограничивает количество запросов с одного IP до 10 в секунду.

package main

import (
    "fmt"
    "net/http"
    "sync"
    "time"
)

type RateLimiter struct {
    mu       sync.Mutex
    visitors map[string]*visitor
    rate     int
    interval time.Duration
}

type visitor struct {
    tokens  int
    lastSeen time.Time
}

func NewRateLimiter(rate int, interval time.Duration) *RateLimiter {
    rl := &RateLimiter{
        visitors: make(map[string]*visitor),
        rate:     rate,
        interval: interval,
    }
    // Запускаем горутину для очистки старых записей
    go rl.cleanup()
    return rl
}

func (rl *RateLimiter) cleanup() {
    for {
        time.Sleep(10 * time.Minute)
        rl.mu.Lock()
        for ip, v := range rl.visitors {
            if time.Since(v.lastSeen) > rl.interval {
                delete(rl.visitors, ip)
            }
        }
        rl.mu.Unlock()
    }
}

func (rl *RateLimiter) Allow(ip string) bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()                

Блоги

Рекомендаций по книгам