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

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

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

Коротко

limit_req - это дырявое ведро, а не «столько-то запросов в секунду». При rate=1r/s из десяти одновременных проходит один, и это проверено.

  • Ключ хранится в зоне разделяемой памяти: 1m - примерно 16 тысяч записей.
  • Ключ берут $binary_remote_addr, а не $remote_addr: он вчетверо короче.
  • rate=30r/m не значит «тридцать в начале минуты»: это один запрос раз в две секунды.
  • Лимит стоит на фазе preaccess - позже return, поэтому в блоке с return 200 он не сработает вовсе.

Если знаешь про дырявое ведро и различаешь 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 байт, поэтому мегабайт вмещает примерно 16 тысяч ключей - это про число одновременно отслеживаемых адресов, а не про размер самого адреса. Кончится зона - nginx начнёт вытеснять старые записи и напишет в лог. Десяти мегабайт хватает почти всем.

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

Дробные скорости пишут как 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 - дырявое ведро: при rate=1r/s из десяти одновременных запросов проходит ровно один, остальные получают 503 мгновенно, и простой права на пачку не даёт. Состояние живёт в зоне разделяемой памяти (1m - около 16 тысяч адресов), ключом берут $binary_remote_addr. rate=30r/m - это один запрос в две секунды. Лимит стоит на фазе preaccess, то есть позже return: блок с return 200 не ограничивает ничего. Отвергнутому отдают 429 вместо 503, а перед внедрением гоняют limit_req_dry_run on.

Комментарии

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

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

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