Настраиваем стриминговые ответы для сервера NGINX + uWSGI
В некоторых особых случаях возникает необходимость использовать стриминговый ответ с сервера, передавая данные чанками. По собственному опыту скажу, что примером такого взаимодействия может стать стриминг логов или кадров камеры (хотя на деле это не лучший способ достижения результата).
Обрисуем нашего условного преступника, который делает очень важную работу, считая от 1 до 10 с промежутком в полсекунды:
@app.route('/stream')
def stream():
def generate():
for i in range(1, 11):
yield str(i)
time.sleep(0.5)
return app.response_class(generate(), mimetype='text')
Работу с этим гениальным эндпоинтом на фронте я описывать не стану, т.к. это не тема заметки (можете смотреть ReadableStream и ReadableStreamDefaultReader).
В стандартном виде где-нибудь у разработчика на ноутбуке это конечно же работает прекрасно, проблемы начинаются, когда мы начинаем использовать NGINX — запрос сначала зависает, а после выдаёт все данные разом. К счастью, это поведение давно известно и рецепт прост:
proxy_buffering off;
Локально в связке Docker + NGINX + Python никаких проблем не возникнет. Однако на практике в продакшене умные программисты не используют стандартный Flask сервер. В нашем мнимом проекте-считалочке на продакшене мы используем uWSGI.
Что бы uWSGI верно отдавал ответ нам нужно добавить всего 2 строки в настройки:
http-auto-chunked = true
add-header = X-Accel-Buffering: no
Подробнее:
- http-auto-chunked — автоматически трансформирует ответ в чанки в процессе HTTP 1.1 keepalive (если необходимо)
- X-Accel-Buffering: no — заголовок указывает NGINX, что данный ответ нужно обрабатывать с выключенной буферизацией
После этого мы можем убрать proxy_buffering off из конфига NGINX и тем самым немного улучшить производительность в местах, где не используется uWSGI.
Но самое интересное, что исходя из этого мы можем пойти дальше и использовать заголовок для точечного управления только в тех эндпоинтах, где это действительно необходимо, не теряя в других местах!
@app.route('/stream')
def stream():
...
response = app.response_class(generate(), mimetype='text')
response.headers['X-Accel-Buffering'] = 'no'
return response
Какой именно способ конфигурации выбрать зависит от конкретного случая, я же надеюсь, что сэкономил вам немного времени и нервов.