Ограничение частоты запросов
Даже корректный клиент способен отправлять запросы быстрее, чем сервис готов их обслуживать. Причиной бывает массовое редактирование, частый опрос или повтор после сетевых ошибок. Если ограничения остаются неописанными, клиент воспринимает любой отказ как случайный сбой и усиливает нагрузку немедленным повтором.
В этом уроке добавим частотный договор каталога. Результат — вы сможете объяснить, кто расходует бюджет, когда операция ещё не принята и как клиент выбирает ожидание. Числа являются выбранной учебной политикой, а не свойствами HTTP или измерением настоящего сервера. Запросы и таймеры не запускались.
Сначала определим область ограничения
Для защищённых изменений выбираем общий бюджет на subject. Alice u17 использует один бюджет независимо от текущего sid или Bearer token. Ротация credentials не даёт ей десять новых попыток. POST создания курса, PUT изменения, загрузка файла и запуск экспорта относятся к одному бюджету изменений.
Публичное чтение и опрос состояния получают отдельную политику. Нельзя случайно израсходовать все попытки записи просто потому, что интерфейс часто обновлял список. Одновременно отдельный GET-бюджет тоже нужен реализации: безопасность метода не делает обслуживание бесплатным.
IP-адрес мог бы применяться к грубому инфраструктурному ограничению до входа, но не заменяет subject в нашем прикладном договоре. За одним адресом могут находиться разные пользователи, а один пользователь способен обращаться с разных адресов. Обе политики можно сочетать, если клиенту понятна причина отказа и обслуживанию известен их порядок.
Простая модель накопленного бюджета
Выбираем token bucket: вместимость десять условных токенов, восстановление одного токена в секунду, одна попытка защищённого изменения стоит один токен. Полное ведро разрешает короткий всплеск, затем частота ограничивается восстановлением. При паузе запас растёт до десяти, а не бесконечно.
subject: u17
capacity: 10
refill: 1 token / second
cost: 1 token / protected write attempt
Это полное описание чисел нашего примера, но не готовая реализация алгоритма. Время берётся со стороны сервиса; проверка доступности и расход должны быть атомарными. Два параллельных запроса не могут оба увидеть один последний токен и оба независимо считать себя допущенными.
Несколько узлов с независимыми ведрами не дают общий предел десять. Для такого обещания требуется согласованное состояние или иная явно описанная распределённая политика. Более грубая приблизительная защита бывает полезна, но тогда нельзя выдавать её за точный глобальный бюджет пользователя.
Отказ до бизнес-изменения
Пусть Alice отправила слишком много операций подряд. После успешной идентификации и проверки относящейся к запросу политики очередная попытка не допускается к изменению. Иллюстрация ответа:
HTTP/1.1 429 Too Many Requests
Retry-After: 2
Content-Type: application/problem+json
Cache-Control: no-store
{"type":"https://catalog.example/problems/write-rate-limit","title":"Исчерпан бюджет изменений","status":429,"detail":"Подождите перед следующей попыткой записи."}
Здесь бизнес-операция не применена и новое задание не создано. Это существенное свойство нашего отказа, которое понадобится при повторе. Две секунды являются подсказкой, рассчитанной условным ограничителем в данном сценарии; это не гарантия резервирования места и не обещание, что следующий ответ непременно успешен.
429 используется для превышения частоты запросов. RFC не задаёт наш алгоритм, число токенов или способ определения пользователя. Retry-After допустим, но конкретная система должна сама выбрать его значение и изложить политику. Ответ 429 не должен храниться кешем. 429 в RFC 6585.
Что означает Retry-After
Поле может содержать задержку в секундах или HTTP-дату. Для примеров выбираем целое число секунд, чтобы не смешивать обработку ограничения с разницей часов клиента и сервера. Клиент ждёт указанную паузу прежде следующей подходящей попытки, сохраняя конечный предел числа повторов.
receive 429 + Retry-After: 2
keep original operation identity
wait at least the suggested delay
retry only if the operation contract permits it
stop after the configured attempt/deadline budget
Такая схема является описанием ожидаемого поведения, а не исполняемой программой. Не стоит устраивать бесконечный цикл: ограничения могут сохраняться, право пользователя может быть отозвано, а срок жизни прикладного ключа — закончиться. Интерфейс должен позволять человеку увидеть отложенное состояние и причину остановки.
При множестве клиентов полезно добавить случайную составляющую задержки. Иначе все получатели одинакового Retry-After проснутся одновременно и создадут следующий всплеск. Случайное ожидание не отменяет нижнюю подсказанную границу и не превращает неподходящий POST в безопасный повтор.
Повтор и частота — разные обещания
Для POST с нашим Idempotency-Key повтор того же намерения возвращает сохранённый результат, когда ключ ещё пригоден. Это защищает эффект операции, но обработка запроса всё равно требует ресурсов. Учебный договор допускает, что попытка получить сохранённый результат также расходует токен бюджета изменений.
Следовательно, дедупликация не является способом бесплатно обходить ограничение. Сотня повторов не создаёт сотню курсов, однако может дать 429. Клиент сохраняет прежний ключ и тело, когда снова пытается разрешить тот же исход. Создать новый ключ после каждого 429 означало бы изменить идентичность намерения и утратить защиту от другого неопределённого сбоя.
Если ключ ещё не был принят из-за ограничения, повтор после допуска может создать первый ресурс. Если операция была принята раньше, повтор найдёт прежний результат. Согласованность admission, обработки и реестра ключей должна быть описана реализации; нельзя отдельно записать «успешно» до фиксации данных.
Сетевой сбой требует другой оценки
Ответ 429 в нашем контракте явно сообщает недопуск бизнес-операции. Обрыв соединения после отправки POST ничего подобного не доказывает: сервер мог уже выполнить изменение. В этом случае применяется прежний договор разрешения неопределённого исхода и ключа, а не догадка «раз не получил ответ, не потратил токен и ничего не создал».
Идемпотентный PUT тоже может столкнуться с обновлённой версией. Если первая попытка прошла, повтор с прежним If-Match способен получить 412. После ожидания клиент читает текущий ресурс и разбирается с результатом; он не удаляет условие, чтобы избежать отказа.
Backoff регулирует время, Idempotency-Key — повтор одного намерения, If-Match — допустимость изменения известной версии. Три механизма решают разные вопросы. Можно использовать их вместе, но ни один не заменяет остальные.
Ограничение не разрешает чужую операцию
Освободившийся токен не даёт Alice право менять quality-курс c003. После допуска частоты сохраняются остальные проверки, включая область ресурса и If-Match. И наоборот, правильные права не резервируют токен навсегда: независимая другая вкладка того же subject может расходовать общий бюджет.
Поэтому интерфейс не показывает остаток как обещанное число успешных изменений. Это возможная техническая метрика на определённый момент, а не гарантия бизнес-результата. В данном контракте заголовки с остатком бюджета пока не введены, чтобы не создавать ложную точность для распределённой реализации.
Перегрузка сервиса отличается от квоты
503 Service Unavailable может обозначать временную невозможность обслуживания, например недоступность обязательного хранилища. Он тоже может содержать Retry-After, однако не утверждает, что именно Alice исчерпала свой пользовательский бюджет. Причина и граница принятия операции должны оставаться ясными. Retry-After и 503 в RFC 9110.
Не используйте 429 для любого внутреннего исключения. Клиент начнёт лечить собственную частоту, хотя проблема находится в инфраструктуре. И не обещайте применять изменения при 503 с тем же смыслом, что у гарантированного недопуска: неопределённый исход требует отдельного разбора в конкретной реализации.
При будущем ручном опыте сравнивайте пользователей, параллельные попытки, восстановление бюджета и число бизнес-эффектов. Сейчас никаких нагрузочных опытов не было. Договор позволяет следующему уроку отделить быстрый ответ принятия от долгой работы: экспорт может выполняться в фоне, а его чтение не должно превращаться в бесконечный поток опросов.