# теория · шаг 1 из 6
Как nginx считает частоту
Коротко
limit_req учитывает превышение заданного темпа для каждого ключа по модели дырявого ведра.
- При rate=1r/s без burst из десяти одновременных запросов одного ключа проходит один; простой не накапливает разрешения на пачку.
- Зона разделяемой памяти хранит состояния ключей. На 64-битной системе 1m вмещает примерно 8 тысяч записей.
- $binary_remote_addr хранит адрес компактно: 4 байта для IPv4, 16 для IPv6.
- Проверка выполняется после rewrite: ранний return завершит запрос до лимита.
Если знаешь про дырявое ведро и различаешь rate и «пачку в секунду» - листай до «Теперь сам».
Сначала ответь сам
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
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
1 200
9 503
& запускает каждый curl в фоне, wait дожидается всех - так десять запросов
уходят разом. Один прошёл, не десять, не «в среднем один в секунду» - ровно один, а девять получат
503 мгновенно. rate=1r/s означает «одна порция раз в секунду», и в момент
пачки в ведре есть место ровно на одну.
На что это похоже
Турникет, который пропускает по одному человеку каждые десять секунд. Толпа из десяти человек не пройдёт «в среднем за сто секунд» - первый пройдёт, а девять упрутся в закрытую створку и уйдут.
Чтобы толпа проходила, к турникету пристраивают загон: люди ждут своей очереди,
а не разворачиваются. Это burst из следующего урока.
Механизм: дырявое ведро
Последняя строка важна: ведро не копит право на запросы. Час тишины не даёт
права выпустить потом тысячу - в ведре по-прежнему место на 1 + burst.
Правило
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
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
200 200 200 200
Тридцать в минуту - это один раз в две секунды, а не тридцать в начале минуты. Ведро течёт равномерно.
Разбор: где именно стоит лимит
Лимит работает на фазе preaccessРазбирается в разделе 13, глава «return против rewrite»: Фазы: кто и когда решает - одном из шагов обработки запроса, после выбора location и перезаписи, но до доступа и выдачи содержимого. Из этого следует неочевидное:
limit_req zone=one;
return 200 "ok\n";
Такой блок не ограничивает ничего: return выполняется на фазе rewrite,
которая идёт раньше. Проверено - десять одновременных запросов дают десять
двухсоток.
Ловушка не теоретическая: именно так пишут проверочные конфиги («сделаю
заглушку, посмотрю, как работает лимит»), получают десять успешных ответов и
делают вывод, что limit_req не работает. Проверять надо на том, что отдаёт
файл или уходит на бэкенд.
Из той же фазы следует и порядок: лимит считается после перезаписи. Если
rewrite склеивает десяток адресов в один, ключ у них будет общий.
Разбор: что видит клиент и что видишь ты
По умолчанию отвергнутый запрос получает 503. Поменять код - одна строка:
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
1 200
9 429
429 честнее: 503 означает «сервер временно не может», а тут сервер прекрасно может - он не хочет от этого клиента. Клиентские библиотеки на 429 умеют отступать по таймеру, на 503 - обычно нет.
В логе это две разные строки, и уровни у них разные:
[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 -
запрос выполнен, просто подождал в очереди. Спутать их легко, а означают они
противоположное.
Что ломается без этого
Перед включением лимита на боевом сайте есть режим примерки:
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
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, затем сопоставь коды отказов с логом лимита.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий