nphp15 May 2026 18:15

Часто вижу реализации rate limiter завязанные на Redis или другие внешние хранилища. Но для однопроцессных скриптов, CLI-утилит или быстрого прототипирования нужен вариант без зависимостей.

Вот рабочая реализация алгоритма token bucket на чистом PHP:

PHP
<?php
class TokenBucket
{
private float $tokens;
private float $lastRefill;
public function __construct(
private readonly float $capacity,
private readonly float $refillRate, // токенов в секунду
) {
$this->tokens = $capacity;
$this->lastRefill = microtime(true);
}
public function consume(float $amount = 1.0): bool
{
$this->refill();
if ($this->tokens < $amount) {
return false;
}
$this->tokens -= $amount;
return true;
}
public function getTokens(): float
{
$this->refill();
return $this->tokens;
}
private function refill(): void
{
$now = microtime(true);
$elapsed = $now - $this->lastRefill;
$this->tokens = min(
$this->capacity,
$this->tokens + $elapsed * $this->refillRate
);
$this->lastRefill = $now;
}
}
 
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Запустите в sandbox, увидите что первые 10 проходят сразу (burst), потом начинает ограничивать до 5 rps. Меняйте usleep и параметры чтобы понять поведение.

Replies (3)
petr_sys15 May 2026 18:53

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

PHP
<?php
class RateLimiterPool
{
private array $buckets = [];
public function __construct(
private readonly float $capacity,
private readonly float $refillRate,
) {}
public function consume(string $key, float $amount = 1.0): bool
{
if (!isset($this->buckets[$key])) {
$this->buckets[$key] = [
'tokens' => $this->capacity,
'lastRefill' => microtime(true),
];
}
$b = &$this->buckets[$key];
$now = microtime(true);
$elapsed = $now - $b['lastRefill'];
$b['tokens'] = min($this->capacity, $b['tokens'] + $elapsed * $this->refillRate);
$b['lastRefill'] = $now;
if ($b['tokens'] < $amount) {
return false;
}
$b['tokens'] -= $amount;
return true;
}
}
$pool = new RateLimiterPool(capacity: 3.0, refillRate: 1.0);
$requests = ['192.168.1.1', '192.168.1.2', '192.168.1.1', '192.168.1.1', '192.168.1.1', '192.168.1.2'];
foreach ($requests as $ip) {
$ok = $pool->consume($ip);
echo "$ip: " . ($ok ? 'OK' : 'RATE LIMITED') . "\n";
}
 
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Запустите, видно что два IP ограничиваются независимо.

0
bohdan_v15 May 2026 21:48

Важное замечание по Swoole: в корутинном окружении microtime(true) работает корректно, но сам объект TokenBucket нельзя шарить между корутинами как статическое свойство, иначе получите race condition на $this->tokens.

В Hyperf для такого используйте либо coroutine context, либо вынесите состояние в атомарный счетчик через Swoole\Atomic\Long. Вот упрощенный пример с атомарным счетчиком для однотипного ограничения:

PHP
<?php
// Этот код для демонстрации концепции, запускайте в обычном PHP sandbox
$counter = 0;
$limit = 5;
$window = 10; // секунд
$start = time();
function tryRequest(int &$counter, int $limit, int $window, int $start): bool
{
if (time() - $start > $window) {
return false; // окно истекло, в реальности сбрасывали бы счетчик
}
if ($counter >= $limit) {
return false;
}
$counter++;
return true;
}
for ($i = 1; $i <= 8; $i++) {
$ok = tryRequest($counter, $limit, $window, $start);
echo "Request $i: " . ($ok ? "OK (count: $counter)" : "DENIED (limit: $limit)") . "\n";
}
הההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההההה
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
0
sergey_web16 May 2026 07:55

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

Для продакшн кода с важной логикой пока предпочитаю явные переменные именно по этой причине. Для трансформации данных типа sanitize/format pipeline очень удобно.

0
Write a reply
Markdown. ```php blocks are runnable.