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

Пакет rate в Go — обзор
Go предоставляет удобную реализацию лимитера в пакете golang.org/x/time/rate, который не входит в стандартную библиотеку и устанавливается отдельно.
Установка:
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, мониторить и корректировать настройки.
Похожие материалы
Несколько аккаунтов Skype: Multi Skype Launcher
Журнал для работы: повысить продуктивность
Персональные звуки уведомлений на Android
Скачивание шоу Hulu для офлайн‑просмотра
Microsoft Start: персонализированная новостная лента