Гид по технологиям

Ограничение скорости в Go: руководство по rate limiting

5 min read Разработка Обновлено 16 Dec 2025
Ограничение скорости в Go: практическое руководство
Ограничение скорости в Go: практическое руководство

Ограничение скорости (rate limiting) в Go помогает контролировать нагрузку, защищать API от брутфорс‑атак и снижать расходы на хостинг. В Go это удобно реализуется с помощью пакета golang.org/x/time/rate; важные решения — глобальный или per‑client лимитер, стратегия burst и выбор между локальным и распределённым хранилищем.

Что такое ограничение скорости?

Ограничение скорости — это техника управления доступом, которая ограничивает число сетевых запросов за фиксированные интервалы времени или в режиме «токен‑бакета». Часто используется для защиты от DDoS и брутфорс‑атак, уменьшения интенсивного скрапинга и контроля API‑трафика.

Кратко: rate limiting — это правило «сколько операций разрешено за единицу времени».

Gopher — талисман языка Go

Пакет rate в Go — обзор

Go предоставляет удобную реализацию лимитера в пакете golang.org/x/time/rate, который не входит в стандартную библиотеку и устанавливается отдельно.

Обзор пакета rate в Go

Установка:

go get "golang.org/x/time/rate"

Импорты для примера:

import (
    "encoding/json"
    "golang.org/x/time/rate"
    "log"
    "net/http"
)

Пояснение по используемым пакетам:

  • encoding/json — кодирование ответа в JSON.
  • log — вывод ошибок в консоль.
  • net/http — построение HTTP‑эндпойнтов и middleware.

Простое API с одним эндпойнтом

Опишем структуру ответа и простейший обработчик, который возвращает JSON.

type Message struct {
    Response    string `json:"response"`
    Description string `json:"description"`
}

func endpointExample(writer http.ResponseWriter, request *http.Request) {
    writer.Header().Set("Content-Type", "application/json")
    writer.WriteHeader(http.StatusOK)
    message := Message{
        Response:    "Successful",
        Description: "You've successfully hit the API endpoint",
    }
    err := json.NewEncoder(writer).Encode(&message)
    if err != nil {
        return
    }
}

Простой middleware с rate limiter

Ниже — базовый пример middleware, который использует rate.NewLimiter(3, 6). Первый аргумент — скорость (число событий в секунду), второй — burst (вместимость «корзины» токенов).

func rateLimiterMiddleware(next func(writer http.ResponseWriter, request *http.Request)) http.HandlerFunc {
    limiter := rate.NewLimiter(3, 6) // 3 req/s, burst 6
    return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
        if !limiter.Allow() {
            writer.Write([]byte("rate limit exceeded "))
            return
        } else {
            endpointExample(writer, request)
        }
    })
}

Запуск сервера:

func main() {
    http.HandleFunc("/home", rateLimiterMiddleware(endpointExample))
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Println("There was an error listening on port :8080", err)
    }
}

Тест в bash (10 запросов подряд):

for i in {1..10}; do curl http://localhost:8080/home; done

Результат: после исчерпания burst‑квоты некоторые запросы начнут получать сообщение об отклонении.

Результат работы программы с ограничением скорости

Важно: в коде выше лимитер общим для всех клиентов и работает в рамках одного процесса сервера.

Как это работает: token bucket и методы пакета

Пакет использует модель token bucket (корзина токенов). Ключевые моменты:

  • Параметр r (rate) — скорость восстановления токенов (событий в секунду).
  • Параметр burst — максимальное накопление токенов.
  • Allow() — немедленно возвращает true или false, не блокируя поток.
  • Wait(ctx) — блокирует до появления токена (полаcка ожидания).
  • Reserve/ReserveN — резервация будущих токенов с проверкой доступности.

Когда использовать Allow() vs Wait():

  • Allow() подходит для немедленного отклонения «лишних» запросов (fast‑fail).
  • Wait() применяют, если можно терпимо задержать запрос (например, в фоновом задании).

Per‑client лимитирование (по IP или API‑ключу)

Общая проблема простого подхода — он однаков для всех клиентов. Частое решение — хранить лимитеры в карте по ключу (IP, API‑ключ). Пример паттерна:

var visitors = make(map[string]*rate.Limiter)
var mu sync.Mutex

func getLimiter(key string) *rate.Limiter {
    mu.Lock()
    defer mu.Unlock()
    l, exists := visitors[key]
    if !exists {
        l = rate.NewLimiter(1, 5) // пример: 1 req/s, burst 5
        visitors[key] = l
    }
    return l
}

func perClientMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := r.RemoteAddr // в продакшне используйте X-Forwarded-For с проверкой доверия
        limiter := getLimiter(ip)
        if !limiter.Allow() {
            http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Замечания:

  • Для работы за прокси нужно корректно извлекать IP клиента.
  • Без очистки карта visitors будет расти; добавьте механизм очищения неактивных записей (GC).

Когда локального лимитера недостаточно

Локальные in‑memory лимитеры не подходят, если:

  • У вас несколько инстансов сервиса за балансировщиком — лимит нужно синхронизировать.
  • Нападение исходит из множества IP (Distributed attack) — локальная логика не защитит.

Решения для распределённых систем:

  • Центральный счётчик в Redis (atomic INCR + TTL) — простой фиксированный‑окно метод.
  • Lua‑скрипты в Redis для атомарных «скользящих» окон.
  • Использование API‑gateway / WAF (Cloudflare, AWS WAF, Nginx/Envoy) с поддержкой rate limiting.

Альтернативные алгоритмы и подходы

  • Fixed window (фиксированное окно): простая реализация, плоха при пиковых границах.
  • Sliding window (скользящее окно): сглаживает пограничные эффекты.
  • Token bucket (токен‑бакет): гибкая модель с burst.
  • Leaky bucket (протекающее окно): ограничивает среднюю скорость, буферизуя всплески.

Выбор зависит от требований к точности, затратам на синхронизацию и сложности реализации.

Практический чеклист перед вводом в продакшн

  • Оцените ожидаемую нагрузку и пиковые QPS.
  • Решите: глобальный лимит или per‑client?
  • Выберите алгоритм (token bucket / sliding window).
  • Для кластеров — подготовьте распределённый backend (Redis, etcd) или используйте внешние решения.
  • Настройте HTTP статус 429 и читаемый ответ с Retry‑After при возможности.
  • Тестируйте нагрузкой (локально и на staging).
  • Добавьте метрики: отказов по rate limit, средняя задержка, hit/miss лимитера.

Критерии приёмки

  • При 10 последовательных запросах к /home с лимитом 3req/s и burst 6 не больше 6 запросов проходят сразу.
  • HTTP‑статус 429 или понятный текст возвращается при превышении лимита.
  • Пер‑клиент лимиты не влияют на другие ключи/клиентов.
  • В многорегиональной инсталляции поведение согласовано (при использовании распределённого стореджа).

Тест‑кейсы и пример тестирования

  • Скрипт: for i in {1..100}; do curl -s -o /dev/null -w “%{http_code}\n” http://localhost:8080/home; done Ожидается: первые N статусов 200, затем 429 или текст «rate limit exceeded».
  • Тесты конкурентности: запуск нескольких параллельных потоков curl/ab/hey.
  • Проверка per‑client: отправка запросов с разными X‑Forwarded‑For — несколько независимых лимитов.

Руководство по выбору: локальный vs распределённый

flowchart TD
  A[Один инстанс?] -->|Да| B[Локальный limiter]
  A -->|Нет| C[Несколько инстансов]
  C --> D{Нужна точная глобальная квота?}
  D -->|Да| E[Использовать Redis/общий store или API gateway]
  D -->|Нет| F[Делегировать балансировщику частично]

Когда rate limiting не поможет (примеры провалов)

  • Много источников атак (botnet) распределяют нагрузку так, что per‑IP лимит незначителен.
  • Если атакующий использует законных пользователей (украденные токены), per‑IP не защитит.
  • Неправильная конфигурация burst может допустить всплески и перегрузить сервис.

Резюме

Ограничение скорости — простая и эффективная защита ваших API и сервисов. Пакет golang.org/x/time/rate предоставляет удобные механизмы (token bucket) для встраивания в middleware. Выберите стратегию (глобальная vs per‑client) и учтите распределённую архитектуру: при нескольких инстансах требуетя централизованный сторедж или внешние решения (WAF, API‑gateway).

Важно: тестируйте поведение лимитера под нагрузкой и добавьте метрики и очистку для пер‑клиент карт.

Ключевые действия: определить QPS, выбрать алгоритм, внедрить обработку 429, мониторить и корректировать настройки.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Несколько аккаунтов Skype: Multi Skype Launcher
Программное обеспечение

Несколько аккаунтов Skype: Multi Skype Launcher

Журнал для работы: повысить продуктивность
Productivity

Журнал для работы: повысить продуктивность

Персональные звуки уведомлений на Android
Android.

Персональные звуки уведомлений на Android

Скачивание шоу Hulu для офлайн‑просмотра
Стриминг

Скачивание шоу Hulu для офлайн‑просмотра

Microsoft Start: персонализированная новостная лента
Новости

Microsoft Start: персонализированная новостная лента

Как изменить имя в Epic Games быстро
Гайды

Как изменить имя в Epic Games быстро