Симптом был такой: приложение отдаёт ответ кусками по мере готовности, локально всё приходит сразу, а через прокси клиент ждёт несколько секунд и получает всё разом. Классика, но интересны детали — выключить буферизацию одной директивой оказалось недостаточно.
У прокси два потока: запрос от клиента к приложению и ответ обратно. Директивы разные, и выключение одной не влияет на вторую.
| Директива | Что делает |
|---|---|
proxy_buffering off | Ответ идёт клиенту по мере поступления, а не копится в буфере |
proxy_request_buffering off | Тело запроса передаётся приложению потоком, nginx не ждёт его целиком |
Для загрузок важна вторая: без неё nginx сначала примет весь файл на диск и только потом начнёт разговор с приложением. На больших файлах это выглядит как зависание в начале.
Даже с выключенной буферизацией ответа остаётся размер буфера, в который nginx читает данные из соединения. Пока он не заполнен или не закрыт поток, клиенту может ничего не уходить. Помогает уменьшение:
proxy_buffer_size 4k;
proxy_buffers 8 4k;
Плюс явное отключение сжатия на лету для таких маршрутов — gzip off. Сжатие по своей природе требует накопления, иначе экономии не получится, и это ещё один источник задержки.
По умолчанию nginx общается с апстримом по HTTP/1.0, где нет постоянных соединений и chunked-передачи. Для стриминга это неприемлемо:
proxy_http_version 1.1;
proxy_set_header Connection "";
Пустой заголовок Connection нужен, чтобы не пробрасывать клиентский keep-alive или close в апстрим и не рвать соединение раньше времени.
Проверять глазами бесполезно, нужны цифры. У curl есть готовые переменные:
curl -o /dev/null -s -w \
"connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
https://example.test/stream
Ключевой показатель — time_starttransfer, время до первого байта. Если оно почти равно time_total, значит ответ пришёл одним куском в конце, и буферизация где-то осталась. Если между ними заметный разрыв — поток идёт как задумано.
Отдельно стоит проверить proxy_read_timeout. Для долгих потоков дефолтные шестьдесят секунд означают обрыв ровно посередине, причём в логе это выглядит как ошибка апстрима, хотя апстрим ни при чём.
location /stream/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 300s;
gzip off;
}
После этого время до первого байта упало до сотни миллисекунд, а данные пошли ровным потоком.Полдня на поиск, четыре строки в конфиге — обычная пропорция для таких историй.