13g10n
Напишите мне
На главную

Настраиваем стриминговые ответы для сервера NGINX + uWSGI

NGINX2 минуты

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

Для наглядности я использую Python + Flask, но uWSGI поддерживает и другие языки, а часть связанная с NGINX применима для любых серверов.

Обрисуем нашего условного преступника, который делает очень важную работу, считая от 1 до 10 с промежутком в полсекунды:

app.py
@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 — запрос сначала зависает, а после выдаёт все данные разом. К счастью, это поведение давно известно и рецепт прост:

nginx.conf
proxy_buffering off;

Локально в связке Docker + NGINX + Python никаких проблем не возникнет. Однако на практике в продакшене умные программисты не используют стандартный Flask сервер. В нашем мнимом проекте-считалочке на продакшене мы используем uWSGI.

Я не рекомендую использовать uWSGI для ваших проектов. На мой взгляд, Gunicorn или Uvicorn намного лучше в качестве сервера.

Что бы uWSGI верно отдавал ответ нам нужно добавить всего 2 строки в настройки:

uwsgi.ini
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.py
@app.route('/stream')
def stream():
    ...

    response = app.response_class(generate(), mimetype='text')
    response.headers['X-Accel-Buffering'] = 'no'
    return response

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

Python
Flask
NGINX
uWSGI