Как реализовать ограничение частоты запросов (rate limiting) к эндпоинту API?

Ответ

Для ограничения частоты запросов к API (rate limiting) в Go чаще всего используют пакет golang.org/x/time/rate. Он реализует алгоритм "token bucket" (метод токенов в ведре).

Основная идея — есть "ведро" с определенной емкостью, которое пополняется токенами с фиксированной скоростью. Каждый запрос "забирает" один токен. Если токенов нет, запрос блокируется или отклоняется.

Пример с golang.org/x/time/rate:

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

// Создаем limiter: 1 токен в секунду, с "ведром" на 5 токенов.
// Это позволяет обрабатывать короткие всплески до 5 запросов.
var limiter = rate.NewLimiter(1, 5)

// Middleware для проверки ограничения
func limitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Allow() проверяет, можно ли взять токен. Если нет, возвращает false.
        if !limiter.Allow() {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    // Оборачиваем наш обработчик в middleware
    http.ListenAndServe(":8080", limitMiddleware(mux))
}

Распределенные системы

Для систем, работающих на нескольких инстансах, локальный limiter не подойдет, так как каждый инстанс будет иметь свой собственный счетчик. В таких случаях состояние лимитера (количество токенов) выносят во внешнее хранилище, например, Redis, используя атомарные операции для управления счетчиками.

Ответ 18+ 🔞

Слушай, а вот этот ваш rate limiting — это ж просто пиздец какой-то важный механизм, особенно когда твой API начинают долбить как последнюю суку на районе. Представь: сидит твой сервер, никого не трогает, и тут — бац! — налетает стадо ботов или просто какой-то еблан с кривыми руками, который в цикле запросы шлёт. Без лимитера твой сервер просто ляжет и скажет «памагите», как та Муму в озере.

Вот, например, в Go есть отличная библиотека golang.org/x/time/rate. Она работает по принципу «ведра с токенами», ёпта. Вообрази себе ведро, блядь. Оно вмещает, скажем, 5 токенов. И каждую секунду в него капает один новый токен. Каждый запрос к API — это как взять один токен из ведра. Пока токены есть — все збс, запросы проходят. А как токены кончились — всё, пидорас, стой, жди пополнения. Это позволяет и обычную нагрузку держать, и короткие всплески до 5 запросов обработать — очень удобная хуйня.

Вот смотри, как это выглядит в коде, блядь:

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

// Создаем лимитер: 1 токен в секунду, ведро на 5 токенов.
// Это как раз для тех самых всплесков, понимаешь?
var limiter = rate.NewLimiter(1, 5)

// Делаем middleware — это такая обёрточка для обработчиков, сука
func limitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Allow() — это проверка: а есть ли токен, бля? Нет? Ну и иди нахуй.
        if !limiter.Allow() {
            http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    // Оборачиваем весь наш роутер в этот лимитирующий middleware
    http.ListenAndServe(":8080", limitMiddleware(mux))
}

Всё, сука, просто и элегантно! Теперь если какой-то мудак начнёт слать больше запросов, чем положено, он будет получать статус 429 («Too Many Requests») и может идти лесом, пока его ведро не пополнится.

Но вот тут, блядь, начинается самое интересное! Этот лимитер — он локальный, на одном инстансе работает. А если у тебя сервис распиздяйский, распределённый, на десяти машинах крутится? Тогда каждый инстанс будет считать свои токены, и общий лимит в 10 запросов в секунду превратится в 100, потому что каждый из десяти серверов пропустит по 10! Это ж пиздец полный, понимаешь? Клиент будет думать, что всё ок, а на самом деле твоё общее хранилище или база данных просто сдохнут от такого напора, как я от этой истории про Герасима.

Для таких случаев, ёпта, нужно использовать внешнее хранилище состояния. Типа Redis, блядь. Туда все инстансы будут ходить, атомарно увеличивать счётчик и проверять, не превысили ли лимит. Это уже посложнее, конечно, но без этого в распределёнке — нихуя не получится, только иллюзия защиты.

Короче, rate limiting — это не просто «поставить и забыть». Это как следить за порядком в баре: пускать всех по одному, но если нагрянет толпа — успеть вовремя сказать «стой, мудила, мест нет!». Иначе будет овердохуища проблем, и тебе придётся всё это разгребать в три смены, блядь.