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()