1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 1 из 6

Как nginx считает частоту

Коротко

limit_req учитывает превышение заданного темпа для каждого ключа по модели дырявого ведра.

  • При rate=1r/s без burst из десяти одновременных запросов одного ключа проходит один; простой не накапливает разрешения на пачку.
  • Зона разделяемой памяти хранит состояния ключей. На 64-битной системе 1m вмещает примерно 8 тысяч записей.
  • $binary_remote_addr хранит адрес компактно: 4 байта для IPv4, 16 для IPv6.
  • Проверка выполняется после rewrite: ранний return завершит запрос до лимита.

Если знаешь про дырявое ведро и различаешь rate и «пачку в секунду» - листай до «Теперь сам».

Сначала ответь сам

/etc/nginx/nginx.confhttp
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
/etc/nginx/conf.d/shop.local.confhttp › server › location /api/
limit_req zone=one;

Десять человек нажали кнопку одновременно. Сколько запросов пройдёт?

$  команда
for i in $(seq 10); do curl -s -o /dev/null -w '%{http_code}\n' http://localhost/api/x & done; wait | sort | uniq -c
↳  выводfor i in $(seq 10); do curl ... /api/x & done; wait | sort | uniq -c
      1 200
      9 503

& запускает каждый curl в фоне, wait дожидается всех - так десять запросов уходят разом. Один прошёл, не десять, не «в среднем один в секунду» - ровно один, а девять получат 503 мгновенно. rate=1r/s означает «одна порция раз в секунду», и в момент пачки в ведре есть место ровно на одну.

На что это похоже

Турникет, который пропускает по одному человеку каждые десять секунд. Толпа из десяти человек не пройдёт «в среднем за сто секунд» - первый пройдёт, а девять упрутся в закрытую створку и уйдут.

Чтобы толпа проходила, к турникету пристраивают загон: люди ждут своей очереди, а не разворачиваются. Это burst из следующего урока.

Механизм: дырявое ведро

ведро вместимость 1 + burst запросы льются пачкой вытекает ровно rate переполнилось - лишнее выплёскивается: 503 про запас ведро не копит: простой не даёт права на пачку
Скорость вытекания постоянна. Всё, что не поместилось в ведро, отвергается сразу.

Последняя строка важна: ведро не копит право на запросы. Час тишины не даёт права выпустить потом тысячу - в ведре по-прежнему место на 1 + burst.

Правило

/etc/nginx/nginx.confhttp
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;

Зона - это разделяемая память под состояние ключей, общая для всех рабочих процессов. Одна запись (ключ плюс состояние ведра) на 64-битной системе занимает 128 байт, поэтому мегабайт вмещает примерно 8 тысяч ключей (на 32-битной - 64 байта и 16 тысяч) - это про число одновременно отслеживаемых адресов, а не про размер самого адреса. Кончится зона - nginx начнёт вытеснять старые записи и напишет в лог. Десяти мегабайт хватает почти всем.

Ключом берут $binary_remote_addr, а не $remote_addr: первый хранит адрес в двоичном виде (4 байта для IPv4), второй - строкой (до 15 символов). Сам ключ короче, но запись в зоне всё равно 128 байт, так что вчетверо меньше памяти зона не займёт: экономия заметна только на длинных строках, вроде адресов IPv6 (16 байт против строки до 39 символов).

Дробные скорости пишут как r/m:

$  команда
for i in 1 2 3 4 5; do curl -s -o /dev/null -w '%{http_code} ' http://localhost/rm/x; done
↳  выводfor i in 1 2 3 4 5; do curl ... /rm/x; done (rate=30r/m, без паузы)
200 503 503 503 503
$  команда
for i in 1 2 3 4; do curl -s -o /dev/null -w '%{http_code} ' http://localhost/rm/x; sleep 2; done
↳  выводте же запросы с паузой sleep 2 между ними
200 200 200 200

Тридцать в минуту - это один раз в две секунды, а не тридцать в начале минуты. Ведро течёт равномерно.

Разбор: где именно стоит лимит

Лимит работает на фазе preaccessРазбирается в разделе 13, глава «return против rewrite»: Фазы: кто и когда решает - одном из шагов обработки запроса, после выбора location и перезаписи, но до доступа и выдачи содержимого. Из этого следует неочевидное:

/etc/nginx/conf.d/shop.local.confhttp › server › location /api/
limit_req zone=one;
return 200 "ok\n";

Такой блок не ограничивает ничего: return выполняется на фазе rewrite, которая идёт раньше. Проверено - десять одновременных запросов дают десять двухсоток.

Ловушка не теоретическая: именно так пишут проверочные конфиги («сделаю заглушку, посмотрю, как работает лимит»), получают десять успешных ответов и делают вывод, что limit_req не работает. Проверять надо на том, что отдаёт файл или уходит на бэкенд.

Из той же фазы следует и порядок: лимит считается после перезаписи. Если rewrite склеивает десяток адресов в один, ключ у них будет общий.

Разбор: что видит клиент и что видишь ты

По умолчанию отвергнутый запрос получает 503. Поменять код - одна строка:

/etc/nginx/conf.d/shop.local.confhttp › server › location /api/
limit_req zone=one;
limit_req_status 429;
$  команда
for i in $(seq 10); do curl -s -o /dev/null -w '%{http_code}\n' http://localhost/api/x & done; wait | sort | uniq -c
↳  выводте же десять одновременных запросов, но с limit_req_status 429
      1 200
      9 429

429 честнее: 503 означает «сервер временно не может», а тут сервер прекрасно может - он не хочет от этого клиента. Клиентские библиотеки на 429 умеют отступать по таймеру, на 503 - обычно нет.

В логе это две разные строки, и уровни у них разные:

↳  выводgrep -E 'limiting requests|delaying request' error.log
[error] limiting requests, excess: 0.994 by zone "one"
[warn]  delaying request, excess: 0.700 by zone "one"

excess - насколько запрос переполнил ведро сверх нормы, в единицах запросов (меньше 1 - в пределах burst, больше - за краем). limiting на уровне error - запрос отвергнут. delaying на уровне warn - запрос выполнен, просто подождал в очереди. Спутать их легко, а означают они противоположное.

Что ломается без этого

Перед включением лимита на боевом сайте есть режим примерки:

/etc/nginx/conf.d/shop.local.confhttp › server › location /api/
limit_req zone=one burst=1;
limit_req_dry_run on;
$  команда
for i in $(seq 10); do curl -s -o /dev/null -w '%{http_code}\n' http://localhost/api/x & done; wait | sort | uniq -c
grep -c 'dry run' error.log
↳  выводдесять запросов при limit_req_dry_run on плюс подсчёт строк в логе
     10 200
9

Все десять прошли (200), но в лог легли девять строк «dry run» - ровно те, что были бы заблокированы. Никого не блокирует, но в лог пишет всё, что заблокировал бы. Неделя такого режима отвечает на вопрос «не отрежу ли я живых людей» гораздо надёжнее, чем рассуждения о среднем числе запросов.

Зачем это в работе

Числа для rate берут не из головы, а из лога: смотрят распределение запросов на адрес за минуту и ставят порог выше 99-го процентиля обычных пользователей (значения, ниже которого укладываются 99% из них). Живой человек делает единицы запросов в секунду, а бот - десятки.

И помни про два разных лимита: limit_req считает частоту, limit_conn - одновременность. Первый защищает от перебора паролей и парсинга, второй - от того, что один клиент займёт все соединения скачиванием. Про второй - следующий урок.

Теперь сам

На /api/login поставили limit_req zone=one; при rate=1r/s. Тестировщик жалуется: форма логина отдаёт 503 при обычной работе. В браузере он делает один запрос в несколько секунд. Что не так?

Скорее всего, страница логина делает не один запрос: рядом с формой уходят запросы за captcha, за списком стран, за токеном. Все они попадают в тот же location и в тот же ключ, и одного пользователя хватает, чтобы выбрать лимит. Лечится сужением location до самого /api/login с методом POST либо burst из следующего урока.

Главное

limit_req учитывает частоту запросов по ключу и выполняется на фазе preaccess. Проверяй его на маршруте, до которого эта фаза доходит: ранний return завершит запрос раньше. При замере пачки учитывай rate и burst, затем сопоставь коды отказов с логом лимита.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.