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

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

burst, nodelay и limit_conn

Коротко

Голый limit_req отвергает всё лишнее мгновенно. burst добавляет к ведру очередь, nodelay пропускает очередь сразу, delay=N - середина.

  • Замер при rate=1r/s и десяти одновременных: без burst - 1 успех, с burst=3 - 4, и трое из них ждут 1, 2 и 3 секунды.
  • С nodelay те же четверо проходят мгновенно, а лимит всё равно считается.
  • delay=2 пропускает сразу двоих сверх нормы, остальных ставит в очередь.
  • limit_conn считает не частоту, а одновременность: пять скачиваний при limit_conn 2 дают 2 успеха и 3 отказа.

Если различаешь burst, nodelay и delay= - листай до «Теперь сам».

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

rate=1r/s, burst=3, десять одновременных запросов. Сколько пройдёт и как быстро?

$  команда
for i in $(seq 10); do curl -s -o /dev/null -w '%{http_code} %{time_total}\n' http://localhost/api/x & done; wait | sort -k2 -n
↳  выводfor i in $(seq 10); do curl ... ; done (burst=3, коды и время в секундах)
200 0.006
200 0.995
200 1.985
200 2.983
503 0.001
503 0.001
503 0.001
503 0.001
503 0.001
503 0.003

Четыре успеха: один сразу и трое из очереди - 0.99, 1.98, 2.98 секунды. Три запроса ждали примерно одну, две и три секунды. Ведро углубили, но течёт оно с прежней скоростью, и очередь рассасывается по одному в секунду.

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

Тот же турникет, но перед ним поставили загон на трёх человек. Толпа из десяти: один проходит сразу, трое встают в загон и проходят по очереди, шестерым не хватило места - разворачиваются.

nodelay - это когда охранник пропускает всех, кто поместился в загон, разом, а потом закрывает створку на соответствующее время. Пропускная способность та же, ожидание другое.

Механизм: три режима одной директивы

без burst один проходит, девять сразу получают 503 burst=3 один сразу, трое ждут 1, 2 и 3 секунды клиент видит медленный, но успешный ответ burst=3 nodelay четверо проходят мгновенно но следующий запрос этого клиента упрётся раньше пропущено везде одинаково: burst меняет ожидание, а не пропускную способность
Число прошедших одинаково во всех трёх режимах. Разница в том, ждёт клиент или получает отказ.

Правило

/etc/nginx/conf.d/shop.local.confhttp › server › location /api/
limit_req zone=one burst=20 nodelay;
  • без burst - когда не нужен запас на краткий всплеск: запросы сверх темпа нужно отклонять. Это не ограничение одновременного выполнения;
  • burst без nodelay - когда клиенту лучше подождать, чем получить ошибку: фоновые интеграции, выгрузки по расписанию;
  • burst с nodelay - для живых людей: браузер, открывающий страницу с десятком запросов, не должен ждать секунды на ровном месте.

Для интерфейса почти всегда нужен третий вариант.

Разбор: nodelay не отменяет лимит

↳  выводте же десять одновременных, но burst=3 nodelay
200 0.002
200 0.002
200 0.005
200 0.010
503 0.001
503 0.001
503 0.001
503 0.001
503 0.002
503 0.002

Четыре успеха, как и с обычным burst, но время у всех около нуля - - но без единой секунды ожидания. Пропускная способность не изменилась: ведро всё так же вмещает 1 + burst и течёт со скоростью rate.

Разница в том, когда клиент узнаёт об отказе. С nodelay он получает свои четыре ответа мгновенно и упирается в лимит на пятом; без nodelay он получает все четыре, но последний - через три секунды.

Разбор: delay=N - середина

/etc/nginx/conf.d/shop.local.confhttp › server › location /api/
limit_req zone=one burst=5 delay=2;
↳  выводвосемь одновременных запросов, burst=5 delay=2
200 0.001
200 0.001
200 0.006
200 0.992
200 1.991
200 2.989
503 0.001
503 0.001

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

Разбор: limit_conn - про другое

limit_req считает частоту, limit_conn - сколько соединений клиент держит прямо сейчас:

/etc/nginx/nginx.confhttp
limit_conn_zone $binary_remote_addr zone=conn:10m;
/etc/nginx/conf.d/shop.local.confhttp › server › location /downloads/
limit_conn conn 2;
$  команда
for i in 1 2 3 4 5; do curl -s -o /dev/null -w '%{http_code}\n' http://localhost/downloads/big & done; wait | sort | uniq -c
↳  выводпять одновременных скачиваний при limit_conn 2 (файл отдаётся с limit_rate)
      2 200
      3 503
↳  выводerror.log
[error] limiting connections by zone "conn"

На быстрых ответах эта директива почти не срабатывает: соединение живёт миллисекунды. Смысл появляется там, где ответ долгий - скачивания, выгрузки, потоковые ответы. Пара limit_conn плюс limit_rate из одиннадцатого раздела - типовая защита канала.

В HTTP/2 и HTTP/3 «соединение» - это запрос

Слово «соединение» здесь означает не транспортное соединение, а обрабатываемый запрос: в HTTP/2 и HTTP/3 каждый одновременный запрос считается отдельным соединением. Браузер мультиплексирует десятки запросов в одном соединении - и все они считаются поштучно.

Замер на 1.31.6, limit_conn conn 2, пять запросов в ОДНОМ соединении HTTP/2:

↳  выводпять мультиплексированных запросов, http2 on
      2 200
      3 503

Ровно тот же результат, что у пяти ОТДЕЛЬНЫХ соединений HTTP/1.1. То есть лимит не стал мягче от того, что клиент пришёл по HTTP/2, - и настраивать его надо исходя из числа одновременных ЗАПРОСОВ, а не из числа клиентов. Браузер открывает одно соединение, но может держать в нём шесть-восемь параллельных загрузок, и limit_conn 2 для страницы с картинками окажется слишком жёстким.

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

Лимит на статику. limit_req на location / вместе с картинками и css означает, что открытие страницы с сорока файлами съедает лимит одним человеком. Ограничивают то, что дорого: логин, поиск, отправку форм, тяжёлые отчёты.

Один лимит на всё. Зона одна, а маршруты разные: странице каталога уместен burst=20 nodelay, а /api/login - жёсткий rate=1r/s без burst. Зон делают несколько, по смыслу, и включают в нужных местах.

Лимит применяется на своём уровне. Директива limit_req наследуется по обычным правилам, но, как и allow, замещается целиком: своя строка в location отменяет унаследованные.

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

Практический рецепт для типового сайта:

/etc/nginx/nginx.confhttp
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=login:10m   rate=1r/s;
limit_conn_zone $binary_remote_addr zone=conn:10m;
/etc/nginx/conf.d/shop.local.confhttp › server
limit_req  zone=general burst=50 nodelay;
limit_conn conn 20;

location = /api/login {
    limit_req zone=login burst=3 nodelay;
    limit_req_status 429;
    proxy_pass http://app:8000;
}

Общий щедрый лимит на весь сайт плюс жёсткий на точку входа. И limit_req_status 429 там, где с клиентом разговаривает программа, а не человек.

Теперь сам

Поставили limit_req zone=one burst=100 nodelay; при rate=1r/s - «чтобы никого не отрезать». Через месяц бот перебирает пароли, и лимит его не останавливает. Почему?

Потому что burst=100 разрешает пачку в сто запросов до начала ограничений, а боту этого хватает: он делает сотню попыток мгновенно, потом ждёт и повторяет. Большой burst уместен там, где пачка нормальна (страница с десятками файлов), и вреден на точке входа. На /api/login ставят маленький burst и код 429.

Главное

burst разрешает временное превышение темпа, а nodelay определяет, должны ли разрешённые запросы ждать. Сравни и число принятых запросов, и их задержку. Для одновременных обрабатываемых запросов нужен отдельный limit_conn: ограничение частоты не заменяет ограничение одновременности.

Комментарии

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

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

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