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

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

Буфер между быстрым и медленным

Коротко

Главный приём nginx - буферизация: он разговаривает с медленной сетью сам, а с приложением только по быстрой локальной.

  • Запрос nginx собирает ЦЕЛИКОМ и лишь потом идёт на бэкенд; ответ забирает целиком и уже сам, не торопясь, скармливает клиенту.
  • Из-за этого воркер приложения занят миллисекунды вместо десятков секунд, и это тот же выигрыш, что в прошлом уроке, только с другой стороны.
  • Цена: ответ начинает приходить клиенту позже, чем его отдал бэкенд. Там, где важна каждая порция (SSE, стриминг, длинный опрос), буферизацию выключают.
  • Один вход для многих сервисов и терминация TLS - следствия того же устройства: клиент видит только nginx.

Если это и так знакомо - листай до «Теперь сам» в конце.

Сколько будет занят воркер

Приложение сформировало страницу каталога размером 500 КБ за 50 миллисекунд. Получатель - телефон в электричке, канал 100 Кбит/с.

Сколько времени рабочий процесс приложения будет занят этим запросом?

Без посредника - всё время передачи. Считаем:

100 Кбит/с ÷ 8 = 12,5 КБ/с
500 КБ ÷ 12,5 КБ/с = 40 секунд

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

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

Курьер привёз посылку в пункт выдачи. Он отдал коробку за минуту и уехал развозить остальные - его рабочий день не зависит от того, когда получатель доберётся до пункта.

А получатель, может быть, придёт через неделю. Может, будет долго искать паспорт. Может, живёт на пятом этаже без лифта и попросит донести. Всё это время коробка лежит на полке пункта выдачи, а не в руках курьера.

Приложение - это курьер. nginx - пункт выдачи с полкой. Полка стоит денег (памяти и места на диске), но освобождает самый дорогой ресурс - того, кто умеет генерировать страницы.

Механизм: две буферизации, а не одна

nginx буферизует обе стороны разговора, и это два разных механизма.

Запрос. Пока клиент по байту досылает форму заказа, nginx копит её у себя и на бэкенд не ходит вовсе. Приложение узнаёт о запросе в тот момент, когда тот уже целиком лежит в памяти nginx.

Ответ. Как только бэкенд начал отдавать ответ, nginx забирает его так быстро, как позволяет локальная сеть, складывает в буфер (а если не влезло - во временный файл на диске) и отпускает соединение с приложением. Дальше он сам, не торопясь, скармливает эти байты клиенту.

без nginx воркер приложения занят все 40 секунд передачи генерация 50 мс с nginx эти 40 секунд ждёт nginx, а воркер уже свободен генерация 50 мс сплошная рамка - занятый воркер приложения, пунктир - соединение, которое держит nginx
Общее время доставки не меняется. Меняется то, чей ресурс им занят.

Правило

Бэкенд должен разговаривать только по быстрой локальной сети. Всё, что связано с медленным каналом, - ожидание запроса, отдача ответа, обрывы, повторы - берёт на себя nginx. Приложение не должно знать, что на свете бывает плохой интернет.

Разбор: во что это обходится по числам

Возьмём тот же ответ 500 КБ и посчитаем обе стороны.

Воркер приложения занят Соединение держит
без nginx 50 мс + 40 с = 40,05 с приложение
с nginx 50 мс + 4 мс на отдачу в локальную сеть = 54 мс nginx

Разница в семьсот раз. Четыре миллисекунды - это 500 КБ по гигабитной сети внутри одной машины или стойки.

Теперь пропускная способность. Восемь воркеров:

без nginx:  8 ÷ 40,05 с ≈ 0,2 страницы в секунду
с nginx:    8 ÷ 0,054 с ≈ 148 страниц в секунду

Железо то же самое. Разница - в том, кто ждёт медленного клиента.

Разбор: где буферизация вредит

Из первого разбора легко унести «буферизация всегда хорошо». Вот случай, где она ломает работу.

Страница показывает прогресс долгой операции: бэкенд каждую секунду отдаёт строку «обработано 10 из 500». Клиент должен видеть их по мере поступления.

Тут всё зависит от того, как устроен ответ. Если бэкенд отдаёт его чанками, ни одна из строк не задержится: nginx перекладывает каждый кусок сразу, и замер даёт первый байт через полторы тысячных секунды. А вот если бэкенд объявил длину ответа заранее или ответ идёт под сжатием, nginx соберёт его целиком, и человек десять минут смотрит на пустой экран, а потом получает всю историю разом.

Лечится отключением буферизации для конкретного места - оно снимает оба случая сразу:

Как читать шапку блока кода

Это первый блок конфига в курсе, и у него есть строка-заголовок. Она не часть настроек, её никуда не копируют - это подпись «куда положить и куда вложить».

Шапка блока состоит из двух частей. file= - путь к файлу, в который пишется настройка; здесь это /etc/nginx/conf.d/Разбирается в разделе 1, глава «Конфиг: где лежит и как не уронить прод»: Где живёт конфиг, каталог для своих кусков конфига. ctx= - место внутри файла, куда вкладывается показанный кусок: ctx=http > server значит «внутри блока http, внутри блока server», а ctx=главный - на самом верху файла, ни во что не вложено. Показывать каждый раз двадцать строк обёртки было бы нечитаемо, поэтому обёртка вынесена в шапку.

/etc/nginx/conf.d/shop.local.confhttp server
location /api/progress {
    proxy_pass http://app:8000;
    proxy_buffering off;
}

proxy_passРазбирается в разделе 7, глава «Слеш в proxy_pass»: Один символ, который меняет путь передаёт запрос приложению, подробно он разобран в разделе про проксирование. app в адресе - имя, а не IP-адрес: так контейнер с приложением называется в Docker Compose, и nginx узнаёт его адрес при запуске: если такого имени нет, nginx -t откажет с host not found in upstream "app". На обычном сервере на этом месте стоит 127.0.0.1 - адрес этой же машины, а 8000 - порт, где слушает приложение.

То же самое бэкенд может попросить сам, заголовкомЗаголовокСлужебная пара, которой стороны сообщают друг другу подробности: какой сайт спрашивают, чем открыли, какого типа содержимое, как долго его хранить. Регистр имени не важен, порядок не важен, одно имя может встретиться дважды. X-Accel-Buffering: no в ответе - это удобнее, когда стриминговых адресов много и все они известны приложению, а не конфигу.

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

Подробный разбор буферов и их размеров будет в разделе про обратный проксиОбратный проксиСтоит перед приложением, разбирает запрос, решает, кому его отдать, и возвращает ответ клиенту. Обратный - потому что защищает сервер, а не клиента.; здесь достаточно знать, что механизм есть и что у него две стороны.

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

«Ответы приходят пачками, а не по мере готовности». Включена буферизация там, где нужен поток. Классика для SSE и логов в реальном времени.

«504 при загрузке больших файлов». Запрос буферизуется на диск, диск медленный или кончился - и таймаут наступает раньше, чем nginx успел собрать тело.

«Кончилось место на диске, хотя логи чистили». Каталог временных файлов (/var/lib/nginx/proxy или похожий) забит буферами ответов. Значит ответы регулярно не влезают в память, и размеры буферов стоит смотреть осознанно.

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

Первый случай приходит от разработчиков: «на локальной машине стрим работает, а через nginx на стенде - нет». Это не «nginx ломает WebSocketРазбирается в разделе 7, глава «WebSocket, таймауты, буферизация»: WebSocket через прокси» - постоянный двусторонний канал - и не «прокси глючит»; это буферизация делает ровно то, для чего её включили, просто не в том месте.

Второй - разбор нагрузочного теста. Тест гоняют из соседнего дата-центра по быстрому каналу и получают отличные цифры, а живые пользователи с телефонов кладут сервис. Без буферизации разница между «быстрый клиент» и «медленный клиент» становится разницей между работающим и лежащим сайтом.

Теперь сам

Бэкенд отдаёт отчёт размером 20 МБ. Половина клиентов - из офиса по гигабиту, половина - с мобильного интернета на 1 Мбит/с. Буфер ответа в nginx настроен на 256 КБ. Что произойдёт с ответом для мобильного клиента и чем это грозит?

Ответ. В память влезет только 256 КБ, поэтому остальные 19,75 МБ nginx запишет во временный файл на диске - и соединение с бэкендом всё равно освободит быстро. Мобильный клиент будет тянуть свои 20 МБ около трёх минут, всё это время файл лежит на диске. Грозит это местом: сто таких клиентов - два гигабайта временных файлов. Отсюда два решения, между которыми выбирают осознанно: поднять буферы в памяти (платим памятью) или отдавать такие отчёты файлом со статики, минуя бэкенд (платим тем, что отчёт надо где-то сохранить).

Главное

nginx - буфер между быстрым бэкендом и медленной сетью. Он собирает запрос целиком до похода на приложение и забирает ответ целиком, освобождая воркер за миллисекунды. Общее время доставки от этого не меняется - меняется то, чей ресурс им занят. Отключают буферизацию там, где важен каждый кусок ответа.

Комментарии

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

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

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