← все задачи
Python · задача 10 из 10
Ограничитель частоты вызовов
Продвинутый
25 минут
token bucketвремямногопоточность
Условие
Реализуйте ограничитель: не больше N вызовов за период T, но с возможностью короткого всплеска. Метод allow() возвращает True, если вызов разрешён, и False, если лимит исчерпан.
Что требуется
- Ограничение вида «10 запросов в секунду»
- Короткий всплеск допустим, если до этого было затишье
- Без фоновых потоков и таймеров — пересчёт при обращении
Пример
limiter = TokenBucket(rate=10, capacity=10) # 10 запросов в секунду limiter.allow() # True -> есть токены # ... 10 вызовов подряд ... limiter.allow() # False -> токены кончились, ждём пополнения
Сначала уточните
Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.
- Лимит общий или на каждого пользователя (тогда нужен словарь вёдер и их очистка)?
- Процесс один или их несколько — во втором случае состояние должно жить в Redis
- Нужно ли ждать разрешения (блокирующий вариант) или достаточно ответить «нельзя»?
Показать решение Скрыть решение
Решение
import threading
import time
class TokenBucket:
def __init__(self, rate, capacity=None):
self.rate = rate # токенов в секунду
self.capacity = capacity or rate # предел всплеска
self._tokens = float(self.capacity)
self._updated = time.monotonic()
self._lock = threading.Lock()
def allow(self, tokens=1):
with self._lock:
now = time.monotonic()
# пополняем ведро за прошедшее время, но не выше ёмкости
self._tokens = min(
self.capacity,
self._tokens + (now - self._updated) * self.rate,
)
self._updated = now
if self._tokens >= tokens:
self._tokens -= tokens
return True
return False
Почему так
Почему token bucket, а не счётчик за окно
- Счётчик «N запросов за минуту» даёт эффект границы: 10 запросов в конце одной минуты и 10 в начале следующей — 20 за две секунды
- Ведро сглаживает нагрузку: токены копятся равномерно, и всплеск ограничен ёмкостью
- Ёмкость — это явная ручка «насколько разрешён всплеск», и её отдельность от rate стоит подчеркнуть
Почему пополняем по времени, а не по таймеру
- Фоновый поток, который каждую секунду добавляет токены, — лишняя сущность и лишние переключения
- Достаточно запомнить момент последнего обращения и досчитать токены при следующем: состояние ленивое
- Такой ограничитель ничего не делает, пока им не пользуются, — на тысяче пользователей это важно
Почему monotonic и почему lock
- monotonic не прыгает от перевода часов и синхронизации NTP — на системном времени лимитер мог бы «пополниться» на час вперёд
- Чтение и запись _tokens — это read-modify-write: без блокировки два потока пройдут лимит одновременно
- GIL здесь не спасает: он не делает последовательность операций атомарной
Что спросят дальше
- Спросят про несколько процессов: состояние уезжает в Redis, и пересчёт делают Lua-скриптом, чтобы он был атомарным
- Спросят про лимит на пользователя: словарь вёдер плюс вытеснение неактивных, иначе утечка памяти
- Спросят про честное ожидание: сколько осталось до следующего токена — (tokens_needed - self._tokens) / self.rate, это же значение уходит в заголовок Retry-After
Тема пройдена
Это была последняя задача темы «Python». Возьмите следующую тему или вернитесь к разобранным через неделю — на собеседовании важно вспомнить, а не узнать.