# теория · шаг 2 из 5
Таймауты и буферизация
Коротко
Таймауты меряют не длительность ответа, а паузу. Буферизация освобождает бэкенд от медленного клиента, и в этом главный смысл прокси.
proxy_read_timeoutсчитает промежуток между двумя чтениями. Ответ, идущий восемь секунд по кусочку, проходит при таймауте в три.- Чанкованный поток (ответ кусками без объявленной длины) буферизация не
задерживает. Задерживает она ответ с объявленной длиной и ответ под
gzip- там замер даёт 4 секунды вместо 0,0015. - Буферизация помогает потому, что умеет слить ответ на диск.
proxy_max_temp_file_size 0делает её бесполезной. - Keepalive к бэкенду работает только через блок
upstream. С литеральным адресом вproxy_passкаждое соединение новое.
Если знаешь, что таймаут меряет паузу, и не путаешь буферизацию со сжатием - листай до «Теперь сам».
Сначала ответь сам
proxy_read_timeout 3s;. Два запроса. Первый идёт к обработчику, который
восемь секунд отдаёт ответ по кусочку раз в секунду. Второй - к обработчику,
который молчит пять секунд и отвечает целиком.
Какой из них упрётся в таймаут?
200, всего 8.004266s
504, всего 3.004419s
Восьмисекундный прошёл, пятисекундный упал. Таймаут не про длительность ответа, он про молчание.
На что это похоже
Ты ждёшь человека у метро и договорился: «если пропадёшь больше чем на три минуты - ухожу». Он может добираться час, но каждые две минуты пишет «еду». Ты дождёшься. А может выйти из дома через пять минут после разговора - и не застать тебя, хотя опоздал всего на пару минут.
Отсчёт идёт от последней весточки, а не от начала истории.
Механизм: четыре таймаута на разных участках
Практический вывод из «таймаут меряет паузу»: тяжёлую операцию лучше сделать потоковой или асинхронной задачей со статусом (приложение сразу отвечает «принято, номер такой-то», а результат отдаёт по отдельному запросу), чем поднимать таймаут до получаса. Потоковая проходит при любом разумном таймауте, а получасовой таймаут означает, что зависший запрос будет занимать соединение полчаса.
Правило
Буферизация включена по умолчанию, и работает она так: nginx читает ответ так быстро, как бэкенд отдаёт, складывает в буферы, а что не влезло - во временный файл на диске, и освобождает бэкенд. Медленным клиентом дальше занимается он сам.
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 МБ - в четыреста раз больше. Запретили временный файл - и польза исчезла.
Отсюда же цена решения: приложение с десятком воркеров (своих рабочих процессов, как у 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 с |
Первая строка ломает привычное объяснение: чанкованный поток буферизация не задерживает вовсе. nginx отдаёт клиенту каждый кусок, как только его прочитал, и лишь читает вперёд, когда клиент не успевает.
Задержка появляется там, где nginx не может отдать кусок отдельно: у ответа с объявленной длиной он собирает тело, а сжатие вынуждено накопить данные, прежде чем их сжать. Поэтому «пачками» приходят именно эти два случая.
Отсюда и лечение. proxy_buffering off снимает оба - в таблице при нём и
ответ с длиной, и сжатый поток отдают первый байт за 0,0015 с, - но вместе с
ними снимает и защиту бэкенда от медленного клиента, поэтому его ставят
точечно, на потоковом маршруте (SSE, серверные события, - именно такой
маршрут):
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; - кто-то
поставил, «чтобы файл шёл сразу». Файлы большие, клиенты разные, приложение
периодически упирается в нехватку воркеров. Что происходит и что делать?
Выключенная буферизация держит воркер приложения всё время скачивания: клиент на мобильной связи занимает его минутами. Помогает ли она «идти сразу» - зависит от ответа: чанкованный пошёл бы сразу и с буферизацией, а вот выгрузка с объявленной длиной действительно копилась бы в буфере. Разумный ход - вернуть умолчание и отдавать отчёт чанками, тогда работают обе стороны сразу.
Главное
Таймауты меряют паузу, а не длительность: поток по кусочку проходит при
маленьком таймауте, а «молчал и ответил» не проходит. Буферизация освобождает
бэкенд от медленного клиента, и работает это за счёт временного файла - запрет
файла делает её бесполезной. Чанкованный поток она не задерживает; задерживают
её объявленная длина ответа и gzip, и лечится это proxy_buffering off на
потоковом маршруте или заголовком X-Accel-Buffering: no от приложения. proxy_buffer_size - это про 502 с too big header. Keepalive к
бэкенду с 1.29.7 работает сам, но только когда бэкенд записан блоком
upstream.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий