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