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

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

Таймауты и буферизация

Коротко

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

  • proxy_read_timeout считает промежуток между двумя чтениями. Ответ, идущий восемь секунд по кусочку, проходит при таймауте в три.
  • В стенде чанкованный поток без сжатия приходил без задержки; мелкие порции с объявленной длиной и под gzip ожидали до четырёх секунд. Это результат данного режима передачи, а не гарантия для любого потока.

Если знаешь, что таймаут меряет паузу, и не путаешь буферизацию со сжатием - листай до «Теперь сам».

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

proxy_read_timeout 3s;. Два запроса. Первый идёт к обработчику, который восемь секунд отдаёт ответ по кусочку раз в секунду. Второй - к обработчику, который молчит пять секунд и отвечает целиком.

Какой из них упрётся в таймаут?

↳  выводcurl -s -o /dev/null -w '%{http_code}, всего %{time_total}s\n' http://to.local/s/stream/8
200, всего 8.004266s
↳  выводcurl -s -o /dev/null -w '%{http_code}, всего %{time_total}s\n' http://to.local/a/slow/5
504, всего 3.004419s

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

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

Ты ждёшь человека у метро и договорился: «если пропадёшь больше чем на три минуты - ухожу». Он может добираться час, но каждые две минуты пишет «еду». Ты дождёшься. А может выйти из дома через пять минут после разговора - и не застать тебя, хотя опоздал всего на пару минут.

Отсчёт идёт от последней весточки, а не от начала истории.

Механизм: четыре таймаута на разных участках

клиент nginx бэкенд keepalive_timeout 75s простой соединения с клиентом proxy_connect_timeout 60s установка соединения proxy_send_timeout 60s пауза при передаче запроса proxy_read_timeout 60s пауза при чтении ответа
Три таймаута отмеряют паузы на участке до бэкенда, четвёртый - простой соединения с клиентом.

Практический вывод из «таймаут меряет паузу»: тяжёлую операцию лучше сделать потоковой или асинхронной задачей со статусом (приложение сразу отвечает «принято, номер такой-то», а результат отдаёт по отдельному запросу), чем поднимать таймаут до получаса. Потоковая проходит при любом разумном таймауте, а получасовой таймаут означает, что зависший запрос будет занимать соединение полчаса.

Правило

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

/etc/nginx/conf.d/shop.local.confhttp › server › location /api/
proxy_buffers     8 16k;    # восемь буферов по 16 КБ на соединение (умолчание - 8 4k или 8 8k)
proxy_buffer_size 16k;      # отдельный буфер под заголовки ответа (умолчание - 4k или 8k)

proxy_buffer_size стоит помнить отдельно: это он даёт 502 со строкой upstream sent too big header, когда бэкенд отвечает раздутой кукой сессии (разбирали в шестом разделе).

Разбор: буферизация против медленного клиента

Ответ 50 МБ, клиент читает со скоростью 50 КБ/с и уходит через восемь секунд. Бэкенд печатает, за сколько он отдал ответ:

настройка что стало с бэкендом
proxy_buffering on отдал за 0,26 c и свободен
proxy_buffering off держал соединение, пока клиент не ушёл
on + proxy_max_temp_file_size 0 держал соединение, как при off

Третья строка объясняет механизм. На большом ответе буферизация выигрывает не потому, что «есть буферы», а потому, что умеет слить ответ на диск: буферов всего восемь по 16 КБ, то есть 128 КБ, а ответ в 50 МБ - в четыреста раз больше. Запретили временный файл - и для такого ответа польза исчезла. Для маленького она остаётся: на стенде ответ в 20 КБ освободил бэкенд мгновенно и с файлом, и без него, а ответ в 40 МБ при клиенте на 4 МБ/с занял бэкенд на 0,13 с с файлом и на 5 с без него.

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

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

«События приходят пачками, выключи proxy_buffering» - самый частый совет. Замер показывает, что он верный, но причина не та, о которой думают. Ответ из четырёх кусков по секунде, время до первого байта:

как устроен ответ buffering on buffering off
Transfer-Encoding: chunked 0,0015 с 0,0014 с
объявлен Content-Length, тело пишется по частям 4,005 с 0,0015 с
chunked плюс gzip 4,003 с 0,0015 с

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

Задержка появляется там, где nginx не отдаёт кусок сразу: у ответа с объявленной длиной он копит данные, пока не заполнится буфер или не кончится ответ, а сжатие вынуждено накопить данные, прежде чем их сжать. Копится именно мелочь: ответ в 300 КБ с объявленной длиной, идущий кусками по 50 КБ, на стенде начал приходить клиенту через 0,002 с - буферы заполняются сразу, и чтение идёт одновременно с отправкой. В таблице эти два режима дали заметную задержку.

Отсюда и лечение. proxy_buffering off снимает оба - в таблице при нём и ответ с длиной, и сжатый поток отдают первый байт за 0,0015 с, - но вместе с ними снимает и защиту бэкенда от медленного клиента, поэтому его ставят точечно, на потоковом маршруте (SSE, серверные события, - именно такой маршрут):

/etc/nginx/conf.d/shop.local.confhttp › server › location /events/
proxy_pass http://app:8000;
proxy_buffering off;
proxy_read_timeout 3600s;

gzip off сюда добавлять незачем: при выключенной буферизации сжатие поток уже не держит, это третья строка таблицы.

Есть и путь со стороны приложения, без правки nginx вовсе: заголовок X-Accel-Buffering: no в ответе. Проверено на том же стенде - при включённом gzip он вернул первый байт на 0,0016 секунды. Приложение решает это для конкретного ответа само.

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

Симптом «через curl мгновенно, через браузер с задержкой» при включённой буферизации означает сжатие (gzipРазбирается в разделе 11, глава «gzip и brotli»: Что сжимать и чем - nginx пережимает ответ, если клиент сказал, что умеет его распаковывать): curl по умолчанию не просит Accept-Encoding, а браузер просит всегда. Проверяется одной парой команд:

$  команда
curl -s -o /dev/null -w '%{time_starttransfer}\n' https://shop.local/events/
curl -s -o /dev/null -w '%{time_starttransfer}\n' -H 'Accept-Encoding: gzip' https://shop.local/events/

Разница в секунды - виновато сжатие. Одинаково медленно в обоих - смотри, как устроен ответ: если приложение объявляет Content-Length, поток собирается в буфере целиком. Одинаково медленно и при proxy_buffering off - буферизует само приложение, и nginx тут ни при чём.

Второе, что ломается тихо, - соединения с бэкендом. Замер портов на бэкенде для шести запросов подряд:

Порт - номер, с которого nginx открыл соединение к бэкенду: новое соединение - новый порт. Бэкенд видит его в $remote_port.

как записан бэкенд порты со стороны nginx
proxy_pass http://app:8000/ шесть разных
upstream app { server app:8000; } один и тот же

Первое app - имя группы (его и пишут в proxy_pass http://app;), второе - имя хоста в Docker; совпадать им не обязательно.

С версии 1.29.7 keepalive к бэкенду включён по умолчанию, но включён он в группе. Литеральный адрес в proxy_pass группой не является, и каждый запрос открывает новое TCP-соединение, а на HTTPS - ещё и новое рукопожатие. Одна строка upstream вокруг того же адреса убирает это целиком; подробнее - в следующем разделе.

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

Три вопроса, которые стоит задать конфигу до того, как его выложат:

Есть ли маршрут, где ответ идёт дольше минуты? Если да, он должен быть либо потоковым, либо асинхронным - таймаут поднимают только когда деваться некуда.

Есть ли потоковый маршрут? Если да, на нём выключают сжатие и буферизацию и поднимают proxy_read_timeout - но именно на нём, а не глобально.

Записан ли бэкенд группой? Если нет, ты платишь по соединению на запрос, причём в логах это никак не видно.

Теперь сам

На маршруте выгрузки отчёта в CSV стоит proxy_buffering off; - кто-то поставил, «чтобы файл шёл сразу». Файлы большие, клиенты разные, приложение периодически упирается в нехватку воркеров. Что происходит и что делать?

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

Главное

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

Комментарии

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

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

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