Оригинал: Rate Limiting and Throttling Techniques in PHP APIs

Перевод для канала Мы ж программист

API играют важнейшую роль в современных веб-приложениях, обеспечивая связь между различными системами и сервисами. С ростом зависимости от API эффективное управление их использованием становится критически важным.

Ограничение скорости (rate limiting) и троттлинг (throttling) – важные методы, используемые для контроля трафика API, обеспечивающие стабильность, безопасность и справедливое использование.

В этой статье мы подробно рассмотрим эти понятия, изучим их реализацию в PHP и приведем практические примеры.

Что такое Rate Limiting?

Ограничение скорости (rate limiting) – это техника, используемая для ограничения количества API-запросов, которые клиент может сделать за определенный промежуток времени.

Это предотвращает злоупотребление или чрезмерное использование API, защищает ресурсы сервера и обеспечивает справедливое использование всеми клиентами.

Например:

  • Разрешение максимум 100 запросов в минуту на каждого пользователя.
  • Блокирование запросов после достижения лимита.

Что такое Throttling?

Троттлинг похож на ограничение скорости, но он направлен на замедление запросов, а не на их полную блокировку.

Троттлинг регулирует поток трафика для поддержания стабильности системы при высокой нагрузке.

Например:

  • Разрешение 5 запросов в секунду на пользователя и задержка последующих запросов до истечения временного интервала.

Зачем использовать Rate Limiting и Throttling?

  • Предотвращение злоупотреблений: Защищает API от перегруженности вредоносными пользователями или ботами.
  • Справедливое использование: Обеспечивает равный доступ к ресурсам для всех пользователей.
  • Контроль расходов: Снижает риск превышения платы за облачные ресурсы.
  • Производительность: Предотвращение перегрузки сервера и поддержание времени отклика.

Базовые техники Rate Limiting и Throttling

1. Алгоритм фиксированного окна (Fixed Window Algorithm)

Алгоритм фиксированного окна делит время на фиксированные интервалы (например, 1 минута). Каждый интервал имеет предел, и запросы, превышающие этот предел, отклоняются.

Пример: Если клиенту разрешено 100 запросов в минуту, то все запросы после 100-го в течение той же минуты блокируются.

Реализация на PHP:

PHP
<?php
      class FixedWindowRateLimiter {
          private $limit;
          private $timeWindow;
          private $storage;
      
          public function __construct($limit, $timeWindow) {
              $this->limit = $limit;
              $this->timeWindow = $timeWindow; // In seconds
              $this->storage = [];
          }
      
          public function isRequestAllowed($clientId) {
              $currentWindow = floor(time() / $this->timeWindow);
      
              if (!isset($this->storage[$clientId])) {
                  $this->storage[$clientId] = ['window' => $currentWindow, 'count' => 0];
              }
      
              if ($this->storage[$clientId]['window'] !== $currentWindow) {
                  // Reset the count for a new window
                  $this->storage[$clientId] = ['window' => $currentWindow, 'count' => 0];
              }
      
              if ($this->storage[$clientId]['count'] < $this->limit) {
                  $this->storage[$clientId]['count']++;
                  return true;
              }
      
              return false; // Limit exceeded
          }
      }
      
      // Usage
      $limiter = new FixedWindowRateLimiter(100, 60);
      $clientId = 'user123';
      
      if ($limiter->isRequestAllowed($clientId)) {
          echo "Request allowed.";
      } else {
          echo "Rate limit exceeded.";
      }
?>

2. Алгоритм скользящего окна (Sliding Window Algorithm)

В отличие от фиксированного окна, скользящее окно рассчитывает лимит на основе скользящего периода времени, что позволяет более равномерно распределять запросы.

Реализация на PHP:

PHP
<?php
      class SlidingWindowRateLimiter {
          private $limit;
          private $timeWindow;
          private $storage;
      
          public function __construct($limit, $timeWindow) {
              $this->limit = $limit;
              $this->timeWindow = $timeWindow; // In seconds
              $this->storage = [];
          }
      
          public function isRequestAllowed($clientId) {
              $currentTime = time();
              $windowStart = $currentTime - $this->timeWindow;
      
              if (!isset($this->storage[$clientId])) {
                  $this->storage[$clientId] = [];
              }
      
              // Remove outdated timestamps
              $this->storage[$clientId] = array_filter(
                  $this->storage[$clientId],
                  fn($timestamp) => $timestamp > $windowStart
              );
      
              if (count($this->storage[$clientId]) < $this->limit) {
                  $this->storage[$clientId][] = $currentTime;
                  return true;
              }
      
              return false; // Limit exceeded
          }
      }
      
      // Usage
      $limiter = new SlidingWindowRateLimiter(100, 60);
      $clientId = 'user123';
      
      if ($limiter->isRequestAllowed($clientId)) {
          echo "Request allowed.";
      } else {
          echo "Rate limit exceeded.";
      }
?>

3. Алгоритм маркерной корзины (Token Bucket Algorithm)

Алгоритм маркерной корзины использует маркеры (токены) для представления доступного объема запросов. Токены добавляются с фиксированной скоростью, и каждый запрос потребляет один токен.

Реализация на PHP:

PHP
<?php
      class TokenBucketRateLimiter {
          private $limit;
          private $tokens;
          private $lastRefill;
          private $refillRate;
      
          public function __construct($limit, $refillRate) {
              $this->limit = $limit;
              $this->tokens = $limit;
              $this->lastRefill = time();
              $this->refillRate = $refillRate; // Tokens per second
          }
      
          public function isRequestAllowed() {
              $currentTime = time();
              $elapsed = $currentTime - $this->lastRefill;
      
              // Refill tokens
              $this->tokens = min(
                  $this->limit,
                  $this->tokens + $elapsed * $this->refillRate
              );
      
              $this->lastRefill = $currentTime;
      
              if ($this->tokens >= 1) {
                  $this->tokens--;
                  return true;
              }
      
              return false; // No tokens available
          }
      }
      
      // Usage
      $limiter = new TokenBucketRateLimiter(10, 1); // 10 tokens max, 1 token/sec
      
      if ($limiter->isRequestAllowed()) {
          echo "Request allowed.";
      } else {
          echo "Rate limit exceeded.";
      }
?>

4. Алгоритм текущего ведра (Leaky Bucket Algorithm)

Алгоритм текущего ведра обрабатывает запросы с фиксированной скоростью, ставя в очередь лишние запросы, если ведро переполнено.


Интеграция с Middleware

Middleware – распространенный подход к ограничению скорости в таких фреймворках, как Laravel или Symfony.

Пример на Laravel:

PHP
namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\RateLimiter;

class RateLimitMiddleware {
    public function handle($request, Closure $next) {
        $key = $request->ip(); // Rate limit based on IP

        if (RateLimiter::tooManyAttempts($key, 100)) {
            return response('Rate limit exceeded.', 429);
        }

        RateLimiter::hit($key, 60); // 60 seconds decay

        return $next($request);
    }
}

Дополнительные темы по ограничению скорости и троттлингу

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

1. Распределенное ограничение скорости

В системах с высоким трафиком API часто размещаются на нескольких серверах или распределены по географическим регионам. Для обеспечения согласованного ограничения скорости на всех узлах требуется централизованный или синхронизированный механизм.

Подходы:

  • Централизованное хранилище данных: Используйте общую базу данных или кэш, например Redis или Memcached, для хранения данных, ограничивающих скорость.
  • Распределенные алгоритмы: Реализуйте распределенные алгоритмы, например Lua-скрипты Redis для атомарных операций.

Пример с Redis для распределенного ограничения скорости:

PHP
<?php
      class RedisRateLimiter {
          private $redis;
          private $limit;
          private $timeWindow;
      
          public function __construct($redis, $limit, $timeWindow) {
              $this->redis = $redis; // Instance of Redis
              $this->limit = $limit;
              $this->timeWindow = $timeWindow; // In seconds
          }
      
          public function isRequestAllowed($key) {
              $key = "rate_limit:{$key}";
      
              $currentCount = $this->redis->get($key);
      
              if ($currentCount === false) {
                  // Key does not exist, initialize
                  $this->redis->set($key, 1, $this->timeWindow);
                  return true;
              }
      
              if ($currentCount < $this->limit) {
                  $this->redis->incr($key);
                  return true;
              }
      
              return false; // Limit exceeded
          }
      }
      
      // Usage
      $redis = new Redis();
      $redis->connect('127.0.0.1', 6379);
      
      $limiter = new RedisRateLimiter($redis, 100, 60);
      $clientId = 'user123';
      
      if ($limiter->isRequestAllowed($clientId)) {
          echo "Request allowed.";
      } else {
          echo "Rate limit exceeded.";
      }
?>

Преимущества:

  • Согласованные лимиты на всех серверах.
  • Высокая производительность благодаря операциям Redis в памяти.

Проблемы:

  • Сетевые задержки при доступе к центральному хранилищу.
  • Возможная единая точка отказа, если сервер Redis не реплицирован.

2. Динамическое ограничение скорости

Статические ограничения скорости могут не подходить для всех случаев использования. Динамическое ограничение скорости настраивает лимиты в зависимости от таких факторов, как роли пользователей, уровни подписки или нагрузка на систему.

Примеры использования:

  • Премиум-пользователи: Разрешить более высокие лимиты для платных клиентов.
  • Нагрузка на систему: Снижение лимитов при высоком трафике для защиты инфраструктуры.

Реализация PHP:

PHP
<?php
      class DynamicRateLimiter {
          private $baseLimit;
          private $timeWindow;
          private $storage;
      
          public function __construct($baseLimit, $timeWindow) {
              $this->baseLimit = $baseLimit;
              $this->timeWindow = $timeWindow; // In seconds
              $this->storage = [];
          }
      
          public function getLimitForClient($clientId) {
              // Example logic for dynamic limits
              if ($clientId === 'premium_user') {
                  return $this->baseLimit * 2; // Double limit for premium users
              }
              return $this->baseLimit;
          }
      
          public function isRequestAllowed($clientId) {
              $limit = $this->getLimitForClient($clientId);
              $currentWindow = floor(time() / $this->timeWindow);
      
              if (!isset($this->storage[$clientId])) {
                  $this->storage[$clientId] = ['window' => $currentWindow, 'count' => 0];
              }
      
              if ($this->storage[$clientId]['window'] !== $currentWindow) {
                  $this->storage[$clientId] = ['window' => $currentWindow, 'count' => 0];
              }
      
              if ($this->storage[$clientId]['count'] < $limit) {
                  $this->storage[$clientId]['count']++;
                  return true;
              }
      
              return false; // Limit exceeded
          }
      }
      
      // Usage
      $limiter = new DynamicRateLimiter(100, 60);
      $clientId = 'premium_user';
      
      if ($limiter->isRequestAllowed($clientId)) {
          echo "Request allowed.";
      } else {
          echo "Rate limit exceeded.";
      }
?>

3. Обработка ошибок и уведомления

Когда клиенты превышают лимиты тарифов, очень важно предоставить четкую обратную связь и рекомендации, чтобы избежать разочарования. К распространенным методам относятся:

  • Код состояния HTTP: Возвращать ответ 429 Too Many Requests.
  • Заголовок Retry-After: Укажите, как долго клиент должен ждать перед повторной попыткой.
  • Документация API: Четкое указание ограничений скорости и их поведения.

Пример ответа:

JSON
{
  "error": "Rate limit exceeded",
  "retry_after": 30, // seconds
  "message": "Please wait 30 seconds before making further requests."
}

На PHP:

PHP
http_response_code(429);
header('Retry-After: 30');
echo json_encode([
    "error" => "Rate limit exceeded",
    "retry_after" => 30,
    "message" => "Please wait 30 seconds before making further requests."
]);

4. Тестирование и мониторинг

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

  • Модульные тесты: Проверьте различные сценарии ограничения скорости.
  • Нагрузочное тестирование: Моделирование высокого трафика для проверки производительности и ограничений.
  • Инструменты мониторинга: Используйте такие инструменты, как New Relic, Datadog, или собственные панели мониторинга для отслеживания использования API и показателей ограничения скорости.

Лучшие практики

  1. Внедряйте кэширование: используйте быстрые решения для хранения данных, такие как Redis, для эффективного ограничения скорости.
  2. Настраивайте лимиты: Адаптируйте лимиты в зависимости от поведения пользователей, планов подписки или конечных точек API.
  3. Обрабатывайте сбои: Предоставляйте содержательные сообщения об ошибках и варианты восстановления при превышении лимитов.
  4. Безопасность: Защитите логику ограничения скорости от взлома, обеспечив защиту механизмов идентификации клиента (например, ключей API).
  5. Документация: Четко изложите политику ограничения скорости в документации по API.

Заключительные слова

Ограничение скорости и троттлинг – важные методы обеспечения стабильности, безопасности и справедливости PHP API.

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

Поняв различные алгоритмы (фиксированное окно, скользящее окно, маркерное ведро и текущее ведро) и реализовав их с помощью таких инструментов, как Redis или промежуточное ПО PHP, вы сможете создавать масштабируемые и эффективные системы ограничения скорости.

Более того, включение таких дополнительных функций, как распределенное ограничение скорости, динамические ограничения и обратная связь с пользователем, гарантирует, что ваш API останется надежным и удобным для пользователей.

Потратьте время на разработку и внедрение стратегии ограничения скорости, которая соответствует потребностям вашего приложения. Преимущества с точки зрения производительности, безопасности и удовлетворенности пользователей вполне оправдывают затраченные усилия.