Из-за временных ограничений на территории РФ наблюдаются проблемы с оплатой. Если платёж не проходит, оставьте запрос в службу поддержки.Служба поддержки работает 24/7 — мы всегда на связи по вопросам хостинга и серверов.Открыт прием заявок на аренду выделенных серверов и размещение оборудования в дата-центре.Напоминаем: рекомендуем включить резервное копирование для дополнительной защиты данных.Доступна новая линейка VPS/VDS с NVMe-дисками и увеличенной производительностью.Технические работы на части серверов завершены. Все сервисы работают в штатном режиме.
Статья8 мин чтенияПросмотры0

Один лимит на несколько воркеров: проверяем общий token bucket API

На детерминированной модели сравниваем локальные и общий token bucket: два воркера получают четыре ответа 429, а единый лимитер распределяет восемь запросов без превышения.

Комментарии 0

Вымышленная координатор регулирует общий пропуск тележек с посылками через один механический турникет
В этой статье

Если несколько воркеров отправляют запросы по одному общему лимиту внешнего API, отдельный ограничитель в каждом процессе умножает разрешённый всплеск. Лимитер должен быть общим для того же контура, по которому считает сервер: например, для одного токена, арендатора или метода. Иначе восемь аккуратных очередей вместе ведут себя как одна очень уверенная лавина.

На маленьком примере два воркера одновременно получают восемь заданий. Серверная учебная модель принимает всплеск из трёх запросов и восстанавливает по два разрешения в секунду. Два локальных token bucket дают четыре ответа 429. Один общий bucket распределяет те же восемь запросов от 0 до 2,5 секунды и не получает ни одного 429.

Схема клиента

Отправлено в t=0

Ответы 200

Ответы 429

Отдельный bucket у A и B

6

4

4

Один общий bucket

3

8

0

Что гарантирует HTTP, а что выбирает клиент

Код 429 означает, что клиент отправил слишком много запросов за некоторый интервал. В RFC 6585 прямо сказано: стандарт не определяет, как сервер считает запросы и кого считает одним отправителем. Ограничение может действовать на ресурс, весь сервис или учётные данные.

Token bucket — инженерный выбор для учебного клиента, а не обязательный алгоритм HTTP. Он хранит ёмкость всплеска, текущие токены и скорость пополнения. Один запрос забирает один токен; если токенов нет, клиент вычисляет ближайшее допустимое время. Реальный API может использовать фиксированное окно, скользящее окно или внутреннюю комбинацию правил.

Условия воспроизводимой проверки

Проверка выполнена 1 ноября 2026 года в CPython 3.12.14. Модель работает только в памяти: не открывает сеть, не пишет файлы и не подключается к базе. Виртуальное время задаётся числами, поэтому результат не зависит от скорости компьютера.

  • два воркера A и B получают по четыре задания в момент t=0;

  • сервер и клиентская модель имеют ёмкость 3 и скорость пополнения 2 токена в секунду;

  • в первом сценарии у каждого воркера собственный полный bucket;

  • во втором сценарии оба воркера резервируют время в одном bucket;

  • после последнего запроса дополнительно моделируется общий Retry-After на 1,5 секунды;

  • это проверка механизма, а не измерение лимита конкретного API или редакции 1С-Битрикс.

Код учебной модели

from __future__ import annotations from dataclasses import dataclass EPSILON = 1e-9 @dataclass class TokenBucket: capacity: float refill_per_second: float tokens: float updated_at: float = 0.0 blocked_until: float = 0.0 @classmethod def full(cls, capacity: int, refill_per_second: float) -> "TokenBucket": return cls(float(capacity), refill_per_second, float(capacity)) def _refill(self, now: float) -> None: if now < self.updated_at: raise ValueError("time_must_be_monotonic") elapsed = now - self.updated_at self.tokens = min( self.capacity, self.tokens + elapsed * self.refill_per_second, ) self.updated_at = now def reserve(self, requested_at: float) -> float: now = max(requested_at, self.blocked_until, self.updated_at) self._refill(now) if self.tokens + EPSILON < 1.0: wait = (1.0 - self.tokens) / self.refill_per_second now += wait self._refill(now) self.tokens -= 1.0 return round(now, 6) def allow(self, now: float) -> bool: self._refill(now) if self.tokens + EPSILON < 1.0: return False self.tokens -= 1.0 return True def pause(self, now: float, seconds: float) -> None: if seconds < 0: raise ValueError("negative_pause") self.blocked_until = max(self.blocked_until, now + seconds) def run(schedule: list[tuple[str, float]]) -> tuple[int, int]: server = TokenBucket.full(capacity=3, refill_per_second=2.0) accepted = 0 rejected = 0 for worker, sent_at in sorted(schedule, key=lambda item: item[1]): ok = server.allow(sent_at) print(f"{worker} t={sent_at:.1f}: {'200' if ok else '429'}") accepted += int(ok) rejected += int(not ok) return accepted, rejected jobs = ["A", "B"] * 4 local = {name: TokenBucket.full(3, 2.0) for name in {"A", "B"}} naive_schedule = [(name, local[name].reserve(0.0)) for name in jobs] naive_result = run(naive_schedule) print(f"separate: accepted={naive_result[0]} rejected={naive_result[1]}") assert naive_result == (4, 4) shared = TokenBucket.full(3, 2.0) shared_schedule = [(name, shared.reserve(0.0)) for name in jobs] shared_result = run(shared_schedule) print(f"shared: accepted={shared_result[0]} rejected={shared_result[1]}") assert shared_result == (8, 0) assert [sent_at for _, sent_at in shared_schedule] == [ 0.0, 0.0, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5, ] shared.pause(now=2.5, seconds=1.5) after_429 = [shared.reserve(2.6), shared.reserve(2.7)] print(f"after Retry-After: {after_429}") assert after_429 == [4.0, 4.0]

Наблюдаемый результат

A t=0.0: 200 B t=0.0: 200 A t=0.0: 200 B t=0.0: 429 A t=0.0: 429 B t=0.0: 429 A t=0.5: 200 B t=0.5: 429 separate: accepted=4 rejected=4 A t=0.0: 200 B t=0.0: 200 A t=0.0: 200 B t=0.5: 200 A t=1.0: 200 B t=1.5: 200 A t=2.0: 200 B t=2.5: 200 shared: accepted=8 rejected=0 after Retry-After: [4.0, 4.0]

Процесс завершился с кодом 0, все утверждения прошли. В раздельной схеме оба воркера считают, что вправе немедленно отправить по три запроса: в t=0 на сервер приходит шесть вызовов, хотя глобальная ёмкость равна трём. В t=0,5 оба локальных bucket снова выдают по токену, а сервер успевает восстановить только один. Отсюда четыре принятых запроса и четыре ответа 429.

Общий bucket видит совокупную очередь. Он разрешает три запроса в t=0, затем выдаёт по одному разрешению через 0,5 секунды. Серверная модель с теми же параметрами принимает все восемь. После имитации Retry-After оба следующих задания получают время 4,0: пауза действует на общий контур, а не только на воркер, который увидел ошибку.

Как определить область общего лимитера

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

  • Зафиксируйте идентификатор контура без секрета: например, хеш идентификатора учётных данных, арендатора и группы методов.

  • Резервируйте токен атомарно. Два процесса не должны одновременно увидеть один и тот же остаток.

  • Для нескольких процессов храните состояние в общей службе или используйте отдельный координатор. Внутрипроцессный объект из примера распределённую задачу не решает.

  • Используйте монотонное время для интервалов. Перевод системных часов не должен мгновенно наполнить bucket или отправить его в прошлое.

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

Формы Retry-After и их безопасный разбор уже проверены в материале «429 Retry-After: секунды или дата — проверяем паузу клиента API». Здесь результат другой: мы проверяем координацию параллельных отправителей до и после общей паузы.

Тут есть подвох: общий bucket не знает лимит лучше сервера

Даже правильно разделённый token bucket не гарантирует отсутствие 429. Параллельно может работать другой клиент, сервер способен изменить квоту, а его окно может не совпадать с моделью. Ответ 429 остаётся рабочим сигналом обратной связи: клиент замедляется, соблюдает Retry-After при наличии и не увеличивает число одновременных повторов.

Если общая служба координации недоступна, опасно молча создавать полный локальный bucket в каждом воркере: именно так ограничение внезапно умножается. Для конкретной интеграции заранее выбирают режим отказа — остановить выдачу, поставить задания в ограниченную очередь или перейти на документированную уменьшенную скорость. Решение зависит от допустимой задержки и последствий запроса.

Повтор после паузы должен быть безопасным

Ограничитель отвечает на вопрос «когда отправить», но не меняет бизнес-смысл повторной команды. Если соединение оборвалось после обработки запроса, повтор может снова создать движение товара. Для дельт и абсолютных значений этот риск отдельно разобран в статье «Дельта в API: как исключить повторное прибавление количества товара».

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

Критерии приёмки

  1. Два и более воркера с одним контуром квоты резервируют разрешения в одном состоянии.

  2. При ёмкости 3 и скорости 2 запроса в секунду стартовый всплеск не превышает трёх запросов.

  3. Пауза после 429 блокирует все воркеры того же контура до вычисленного момента.

  4. Сбой координатора не создаёт новый полный bucket в каждом процессе без явной политики.

  5. Метрики различают ожидание локального лимитера, ответ 429 сервера и исчерпание бюджета повторов.

  6. Повторяемая бизнес-операция защищена от дубля независимо от скорости отправки.

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

Обсуждение 0

Делись опытом и задавай вопросы. Комментарии без ссылок появляются после проверки редактором.

Пока никто не написал. Начни обсуждение.