На каких уровнях архитектуры можно реализовать Rate Limiter?

Ответ

Rate Limiter (ограничитель скорости запросов) можно и нужно реализовывать на разных уровнях архитектуры, в зависимости от целей. Вот основные из них, от внешнего к внутреннему:

  1. Уровень шлюза / балансировщика (API Gateway / Load Balancer)

    • Где: Nginx, Kong, Traefik, AWS API Gateway.
    • Зачем: Это первая линия защиты. Ограничивает трафик еще до того, как он попадет в ваши сервисы. Защищает всю инфраструктуру от DDoS-атак и всплесков трафика. Обычно настраивается по IP-адресу или API-ключу.
  2. Уровень приложения (HTTP Middleware)

    • Где: В коде самого приложения, как промежуточный слой для обработки HTTP-запросов. Это самый распространенный способ для бэкенд-сервисов.
    • Зачем: Позволяет реализовать гибкую логику: ограничение для конкретного пользователя, эндпоинта или на основе бизнес-логики. Часто используется алгоритм "Token Bucket" (Ведро с токенами).
      // Пример с использованием стандартной библиотеки golang.org/x/time/rate
      func RateLimit(next http.Handler) http.Handler {
      // 10 запросов в секунду с возможностью "всплеска" до 20 запросов
      limiter := rate.NewLimiter(rate.Limit(10), 20)
      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
          if !limiter.Allow() {
              http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
              return
          }
          next.ServeHTTP(w, r)
      })
      }
  3. Распределенный Rate Limiter (Distributed Rate Limiter)

    • Где: Использует внешнее хранилище, такое как Redis или Memcached.
    • Зачем: Критически важен для горизонтально масштабируемых приложений. Если у вас несколько экземпляров сервиса, локальный ограничитель (как в примере выше) будет работать некорректно. Централизованное хранилище (например, Redis и его атомарные команды INCR, EXPIRE) обеспечивает единый счетчик для всех экземпляров.
  4. Уровень бизнес-логики

    • Где: Внутри конкретной функции или метода сервиса.
    • Зачем: Для ограничения специфических, ресурсоемких операций, не связанных напрямую с HTTP-запросами. Например: "не более 5 отправок SMS в час для одного пользователя" или "не более 1 генерации отчета в минуту".

Ответ 18+ 🔞

А, слушай, про Rate Limiter! Ну это ж классика, блядь. Как в том анекдоте про очередь в сортир — если все ринутся разом, будет пиздец, а не дела. Так и тут.

Вот смотри, эту штуку можно, а часто и нужно, впиливать на разных этажах твоей архитектуры, от крыльца до самого погреба. Главное — понимать, зачем на каждом уровне. Поехали по слоям, от внешнего к внутреннему.

1. Уровень шлюза или балансировщика (API Gateway / Load Balancer)

  • Где это: Nginx, Kong, Traefik, AWS API Gateway — вся эта братия.
  • Зачем, блядь: Это твой бронезаслон, первая линия обороны! Всякий левый трафик отсекается ещё до того, как он доберётся до святая святых — твоих сервисов. Идеально, чтобы вся инфраструктура не легла, как подкошенная, от какого-нибудь внезапного DDoS или просто от распиздяев, которые дергают API как сумасшедшие. Тут обычно бьют по IP или API-ключу. Просто, железно, эффективно.

2. Уровень самого приложения (HTTP Middleware)

  • Где это: Прям в теле твоего бэкенд-сервиса, как промежуточный слой-фильтр для HTTP-запросов. Самый, наверное, народный способ.
  • Зачем: А вот тут уже можно похитрее, блядь! Не просто "с этого IP много", а "этому юзеру — столько, этому эндпоинту — эдак, а на эту операцию — вот такой лимит". Гибкость, ёпта! Часто внутри используется алгоритм "Ведро с токенами" (Token Bucket). Смотри, как просто в Go:
// Пример с использованием стандартной библиотеки golang.org/x/time/rate
func RateLimit(next http.Handler) http.Handler {
    // 10 запросов в секунду с возможностью "всплеска" до 20 запросов
    limiter := rate.NewLimiter(rate.Limit(10), 20)
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !limiter.Allow() {
            http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Но есть нюанс, блядь! Если у тебя сервис размножился на несколько инстансов, этот локальный лимитер превратится в манда-с-ушами. У каждого своё ведро, и общий лимит проебётся.

3. Распределённый Rate Limiter (Distributed Rate Limiter)

  • Где это: Тут уже подключаем внешнее хранилище — Redis, Memcached. Без них никуда.
  • Зачем, ёпта: Это как раз ответ на проблему выше! Для горизонтально масштабируемого приложения — святая вода. Все инстансы тычутся в один центральный счётчик в Redis (через его атомарные команды INCR и EXPIRE), и поэтому знают общую картину. Никаких "ой, а я думал, он ещё не превысил". Все в курсе, блядь.

4. Уровень бизнес-логики

  • Где это: Прям внутри какой-нибудь конкретной функции, которая делает что-то дорогое или опасное.
  • Зачем: Это уже не про HTTP, а про внутренние тараканы. Например: "не больше пяти смсок в час юзеру, а то спамить начнёт" или "генерацию отчёта — не чаще раза в минуту, а то сервак взвоет". Тут уже твоя фантазия и требования бизнеса рулят.

Короче, выбор уровня — это не "или-или", а часто "и-и". На шлюзе отбиваешь грубые атаки, в приложении — тонкую логику для легитов, а в бизнес-процессах следишь, чтобы свои же не угробили систему жадностью. Всё просто, как три копейки!