WebSocket, gRPC и стриминг через Nginx

Рейтинг: 67.2% · 18 голосов
Самый подробный курс по Nginx - от первого конфига до production API-gateway. Архитектура и event loop, контексты и location, виртуальные хосты, статика, reverse proxy и upstream-балансировка, кэширование, HTTPS и TLS (post-quantum), HTTP/2 и HTTP/3 QUIC, безопасность и rate limiting, realip, JSON-логи и сквозная трассировка, метрики, тюнинг и траблшутинг. Плюс OpenResty (Lua, фазы, JWT, динамика) и Angie (нативные Prometheus, ACME, dynamic upstream). На основе эталонного gateway и лучших практик. Для новичков и SRE/DevOps. Актуально на 2026 (nginx 1.30/1.31, Angie, ingress-nginx EOL -> Gateway API).
Ответить
Аватара пользователя
Igor_NgINX
Сообщения: 44
Зарегистрирован: 11 май 2026, 05:31

WebSocket, gRPC и стриминг через Nginx

Сообщение Igor_NgINX »

Оглавление курса (44)
  1. Что такое Nginx и почему он захватил веб
  2. Установка Nginx, Angie и OpenResty: пакеты, версии, Docker
  3. Первый конфиг с нуля: минимальный сервер за 5 минут
  4. Архитектура процессов: master, worker и event loop
  5. Анатомия конфигурации: контексты, наследование и модульность
  6. Виртуальные хосты: server и server_name
  7. Директива location: матчинг и приоритеты по шагам
  8. Раздача статики: root, alias, try_files, sendfile
  9. Переменные, map и блок if
  10. rewrite, return и канонизация URL
  11. Работа с заголовками: add_header и proxy_set_header
  12. Модель доверия прокси-заголовков: realip и защита от спуфинга
  13. Сжатие: gzip, brotli и zstd
  14. Reverse proxy: proxy_pass и проброс запроса
  15. upstream и балансировка нагрузки
  16. Таймауты, повторы и устойчивость прокси
  17. FastCGI и PHP: nginx + PHP-FPM (и uwsgi/scgi)
  18. WebSocket, gRPC и стриминг через Nginx (вы здесь)
  19. Кэширование ответов: proxy_cache и микрокэш
  20. HTTPS и TLS: сертификаты, протоколы, шифры (2026)
  21. Let's Encrypt и ACME: certbot, acme.sh, нативный ACME Angie
  22. HTTP/2 и HTTP/3 (QUIC) в 2026
  23. Безопасность: заголовки, скрытие версии, ограничения
  24. Rate limiting и защита от перегрузки
  25. Контроль доступа: allow/deny, auth_basic, auth_request и mTLS
  26. Логирование: access_log, форматы и структурированный JSON
  27. Сквозная трассировка запросов и OpenTelemetry
  28. Метрики и мониторинг Nginx
  29. Производительность и тюнинг под нагрузку
  30. Траблшутинг: 502, 504, 403, 404 и debug-лог
  31. OpenResty: Nginx как платформа на LuaJIT
  32. Фазы обработки запроса и Lua-хуки
  33. Lua API: ngx.*, shared dict, cosocket и lua-resty-core
  34. Практика OpenResty: авторизация, JWT, кэш, Redis
  35. Angie: современный форк Nginx 2026
  36. Динамическая конфигурация и service discovery
  37. Nginx как API-gateway и Kubernetes Gateway API
  38. Динамические модули, njs и WAF
  39. Stream-модуль: проксирование TCP и UDP
  40. Деплой, перезагрузка и конфиг как код
  41. Сценарий: статика, SPA и кэширование за CDN
  42. Сценарий: микросервисный gateway по образцу ngx-trace-gateway
  43. Капстоун: собираем production-gateway с нуля
  44. Чек-лист безопасности, CVE 2026 и аудит конфигурации
Когда HTTP перестаёт быть запрос-ответом

До этого урока ты гонял через nginx классику: пришёл запрос, ушёл ответ, соединение можно закрывать или держать в keepalive. Этой модели хватает для сайтов и REST API. Но как только в проекте появляется чат, биржевой тикер, онлайн-дашборд, нотификации, gRPC между сервисами или поток событий от сервера - привычная схема ломается. Соединение живёт минутами и часами, данные текут в обе стороны, и буфер, который раньше тебя выручал, теперь становится врагом.

Симптомы знакомые: WebSocket валится с ошибкой 400 или просто не апгрейдится, gRPC отдаёт upstream sent invalid response, а Server-Sent Events доходят до браузера пачкой через минуту вместо потока в реальном времени. Девять из десяти таких тикетов - это nginx, который ведёт себя ровно так, как его настроили: режет заголовки апгрейда, буферизует ответ, рубит долгое соединение по короткому таймауту. Разберёмся, как заставить nginx websocket proxy работать честно, как проксировать gRPC и SSE, и почему тут всё держится на трёх-четырёх директивах, которые легко забыть.

Изображение

WebSocket: проблема Upgrade и nginx upgrade connection

WebSocket начинается как обычный HTTP/1.1-запрос, но с особым набором заголовков. Клиент шлёт:

Код: Выделить всё

GET /ws HTTP/1.1
Host: chat.example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Сервер, если согласен, отвечает 101 Switching Protocols, и дальше по тому же TCP-сокету идёт уже не HTTP, а бинарные фреймы WebSocket в обе стороны. Это и есть апгрейд протокола: HTTP-handshake открывает дверь, после чего соединение перестаёт быть запрос-ответом и становится постоянным двунаправленным туннелем.

В чём подвох с nginx. Заголовки Connection и Upgrade - это так называемые hop-by-hop заголовки. По спецификации HTTP они относятся к одному сетевому участку, а не сквозные. Прокси обязан их не пробрасывать как есть, а обрабатывать сам. Поэтому nginx по умолчанию вырезает эти заголовки при проксировании. Бэкенд их не видит, апгрейда не происходит, клиент получает обычный ответ вместо 101 - рукопожатие провалено.

Чтобы nginx proxy websocket заработал, апгрейд надо пробросить руками. И тут есть тонкость: пробрасывать Connection: upgrade в лоб нельзя. Если по этому же location пойдёт обычный HTTP-запрос (а так и будет - тот же бэкенд часто отдаёт и страницы, и сокеты), ему не нужен upgrade, ему нужен close или keepalive. Нужна развилка по значению входящего заголовка. Эту развилку и делает классический map - именно он стоит в http.conf эталонного проекта:

Код: Выделить всё

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
Читается так: если клиент прислал непустой заголовок Upgrade (значит, это WebSocket), то переменная $connection_upgrade получает значение upgrade. Если Upgrade пустой (обычный запрос), то close. Переменная вычисляется лениво, на каждый запрос, и не стоит ничего, пока её не используешь. Блок map ставится в контекст http, один раз на весь сервер, рядом с include mime.types - ровно так это и сделано в эталоне.

Дальше в location, который проксирует сокет, нужно собрать nginx upgrade connection целиком:

Код: Выделить всё

location /ws/ {
    proxy_pass http://chat_backend;

    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header Host       $host;
    proxy_set_header X-Real-IP  $remote_addr;

    proxy_buffering off;
    proxy_read_timeout  3600s;
    proxy_send_timeout  3600s;
}
Разберём каждую строку, потому что лишних тут нет.
  • proxy_http_version 1.1 - обязательно. Механизм Upgrade появился только в HTTP/1.1. Заметь: с nginx 1.29.7 (вошло в stable 1.30) соединение к бэкенду по умолчанию уже HTTP/1.1 с keepalive, раньше был HTTP/1.0 close. Но для совместимости со старыми версиями и для явности эту строку всё равно ставят - и для апгрейда она критична.
  • proxy_set_header Upgrade $http_upgrade - возвращаем вырезанный заголовок Upgrade обратно, беря его из исходного запроса клиента.
  • proxy_set_header Connection $connection_upgrade - та самая переменная из map. Для сокета уйдёт Connection: upgrade, для обычного запроса - Connection: close.
  • proxy_buffering off - выключаем буферизацию ответа. Для WebSocket это не строго обязательно (фреймы и так проходят после апгрейда), но буфер мешает интерактиву и съедает память на долгом соединении. На двунаправленном туннеле он только во вред.
  • proxy_read_timeout / proxy_send_timeout - вот это забывают чаще всего. По умолчанию proxy_read_timeout - 60 секунд. Это значит: если по сокету 60 секунд нет данных от бэкенда, nginx молча закрывает соединение. Для чата, где между сообщениями бывают долгие паузы, это смерть. Поднимаем до часа или больше. Альтернатива - heartbeat-пинги от приложения чаще, чем таймаут, но таймаут всё равно полезно растянуть.
В эталонном проекте проброс Upgrade и Connection $connection_upgrade зашит прямо в global_proxy.conf, вместе с Host, X-Real-IP и трассирующими заголовками. То есть любой location, который делает proxy_pass и подключает этот глобальный набор, автоматически готов к WebSocket - не надо дублировать в каждом блоке. Это правильный паттерн: апгрейд-логика общая, прописана один раз.

nginx grpc: проксирование gRPC поверх HTTP/2

gRPC - это RPC-фреймворк от Google, который технически работает поверх HTTP/2: каждый вызов - это HTTP/2-поток с телом в формате protobuf и трейлерами. Ключевое отличие от WebSocket: gRPC не использует Upgrade. Он требует именно HTTP/2 от начала и до конца, мультиплексирование потоков в одном соединении и поддержку трейлеров (HTTP-заголовки после тела - там лежит grpc-status). Через обычный proxy_pass gRPC проксировать нельзя: модуль proxy не умеет HTTP/2 к бэкенду и потеряет трейлеры. Для этого есть отдельный модуль ngx_http_grpc_module и директива grpc_pass.

Чтобы nginx grpc заработал, нужны три вещи: HTTP/2 на входе, grpc_pass на бэкенд, и аккуратные таймауты. Минимальный рабочий конфиг с TLS-терминацией:

Код: Выделить всё

upstream grpc_backend {
    server 10.0.0.10:50051;
    server 10.0.0.11:50051;
    keepalive 32;
}

server {
    listen 443 ssl;
    http2 on;

    server_name grpc.example.com;
    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    location / {
        grpc_pass grpc://grpc_backend;

        grpc_set_header X-Real-IP $remote_addr;
        grpc_read_timeout  3600s;
        grpc_send_timeout  3600s;
    }
}
Что тут важно и где грабли:
  • http2 on; - именно отдельная директива, а не listen ... http2. С nginx 1.25.1 параметр listen http2 устарел, правильный синтаксис - separate http2 on; в server-блоке. gRPC без HTTP/2 на входе невозможен в принципе - клиент шлёт HTTP/2-фреймы.
  • grpc_pass grpc://... для plaintext-бэкенда (TLS терминируется на nginx, до бэкенда идёт чистый HTTP/2). Если бэкенд сам с TLS - grpc_pass grpcs://. Очень частая ошибка - перепутать схему: написать grpc:// к TLS-бэкенду или наоборот, и получить непонятный сброс соединения.
  • grpc_set_header, а не proxy_set_header. У grpc-модуля свой набор директив-зеркал: grpc_read_timeout, grpc_send_timeout, grpc_connect_timeout, grpc_buffer_size. Директивы proxy_* на grpc_pass не действуют.
  • keepalive в upstream - почти обязателен. gRPC-канал - это долгоживущее HTTP/2-соединение, по которому идёт много вызовов. Без пула keepalive nginx будет открывать новый коннект к бэкенду слишком часто.
Теперь про балансировку - тут зарыта главная засада gRPC. Балансировка nginx работает на уровне соединения, а не запроса. А gRPC-клиент открывает одно длинное HTTP/2-соединение и мультиплексирует в нём тысячи вызовов. Итог: все вызовы одного клиента прилетают на один бэкенд, и least_conn между запросами не размазывает нагрузку так, как ты ждёшь - распределяются только сами соединения, а не RPC внутри них. Это нормально для многих setup-ов, но если у тебя мало клиентов и много вызовов, нагрузка ляжет криво. Решения: балансировать на уровне клиента (client-side load balancing в gRPC), либо ставить sidecar/service mesh, либо принять, что nginx раздаёт по соединениям. Это не баг nginx, это следствие модели HTTP/2 - важно понимать заранее, а не ловить в проде.

Про health checks: активных проверок здоровья в open-source nginx нет, только пассивные max_fails/fail_timeout. Если нужны активные health checks по gRPC и slow_start для прогрева - это бесплатно есть в Angie, форке на базе nginx. Для чистого nginx довольствуйся пассивной логикой.

nginx sse и nginx стриминг: убить буфер

Server-Sent Events (SSE) - это односторонний поток событий от сервера к браузеру поверх обычного HTTP. Никакого Upgrade, никакого HTTP/2 не требуется: сервер держит соединение открытым с Content-Type: text/event-stream и шлёт строки данных по мере событий. Браузерный EventSource их принимает в реальном времени. Сюда же относится любой chunked-стриминг: построчный вывод логов, прогресс длинной операции, потоковая генерация текста от LLM-бэкенда.

И вот здесь nginx стриминг убивает одна включённая по умолчанию настройка - proxy_buffering on. Когда буферизация включена, nginx читает ответ бэкенда в свой буфер и отдаёт клиенту только когда буфер заполнен или ответ закончился. Для статики это хорошо: бэкенд быстро освобождается, клиент тянет в своём темпе. Для потока это катастрофа: события копятся в буфере nginx и доходят до браузера пачкой с задержкой в секунды или минуты. Realtime превращается в not-real-at-all. Это та самая грабля, ради которой написан этот урок.

Лечение - выключить буферизацию на потоковом location:

Код: Выделить всё

location /events {
    proxy_pass http://sse_backend;

    proxy_http_version 1.1;
    proxy_set_header Connection '';

    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;

    add_header X-Accel-Buffering no;
    chunked_transfer_encoding on;
}
  • proxy_buffering off - главное. Теперь nginx отдаёт каждый чанк клиенту сразу, как получил его от бэкенда. Поток течёт в реальном времени.
  • proxy_set_header Connection '' - чистим заголовок Connection, чтобы keepalive к бэкенду работал корректно (актуально для совместимости и при proxy_http_version 1.1 к старым бэкендам).
  • proxy_cache off - кэш и стриминг несовместимы. Кэшировать бесконечный поток событий бессмысленно и опасно.
  • X-Accel-Buffering: no - тонкий и недооценённый инструмент. Это не директива конфига, а заголовок, который сам бэкенд может прислать в ответе, чтобы динамически попросить nginx не буферизовать именно этот ответ. Удобно, когда буферизацию хочется глобально оставить on (для обычных ответов), но для конкретного потокового эндпоинта выключить со стороны приложения, не трогая конфиг nginx. nginx видит X-Accel-Buffering: no и отключает буфер для этого ответа.
Если SSE проходит ещё и через gzip, проверь gzip_buffers и учти, что сжатие тоже может вносить задержку накопления - для чистого SSE сжатие часто отключают на этом location. И помни про таймаут: поток событий с редкими сообщениями упрётся в proxy_read_timeout ровно как WebSocket.

Типичные грабли
  • Забыли map, написали Connection upgrade в лоб. Тогда обычные запросы к тому же location получают Connection: upgrade и ломаются, либо apps жалуются на странное поведение keepalive. Всегда через переменную $connection_upgrade.
  • Variable connection_upgrade is not defined. nginx -t падает, потому что блок map забыли положить в контекст http (а сунули в server или вообще пропустили). map живёт только в http.
  • WebSocket рвётся ровно через 60 секунд. Дефолтный proxy_read_timeout. Лечится длинным таймаутом плюс heartbeat от приложения.
  • SSE доходит пачкой. proxy_buffering on. Выключить на потоковом location или прислать X-Accel-Buffering: no с бэкенда.
  • gRPC: upstream sent invalid response. Чаще всего на входе нет http2 on, либо использован proxy_pass вместо grpc_pass, либо перепутаны grpc:// и grpcs://.
  • gRPC балансируется не как ждали. Помни: распределяются соединения, а не RPC внутри мультиплексированного HTTP/2-канала.
  • Apple/корпоративный прокси режет апгрейд. Если перед nginx стоит ещё один прокси или CDN, апгрейд надо пробросить на каждом хопе цепочки. Один забытый прокси посередине - и сокет не поднимется.
Мини-лаба: проксируем WebSocket-бэкенд руками

Поднимем эхо-сокет и пропустим его через nginx. Нужен любой бэкенд, отдающий WebSocket - возьмём готовый тестовый.
  • Шаг 1. Запусти простой ws-эхо-сервер. Если есть Node: создай файл и поставь websocket-библиотеку, либо возьми любой готовый ws-echo на порту 9001. Проверь локально wscat -c ws://127.0.0.1:9001 - должно соединяться и эхать твои строки.
  • Шаг 2. В http-контексте nginx добавь блок map (если его ещё нет):

    Код: Выделить всё

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }
    
  • Шаг 3. Добавь server с location для сокета:

    Код: Выделить всё

    server {
        listen 8080;
        location /ws/ {
            proxy_pass http://127.0.0.1:9001/;
            proxy_http_version 1.1;
            proxy_set_header Upgrade    $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            proxy_set_header Host       $host;
            proxy_read_timeout 3600s;
        }
    }
    
  • Шаг 4. Проверь конфиг и перезагрузи: nginx -t и nginx -s reload. Вывод nginx -t должен сказать syntax is ok и test is successful.
  • Шаг 5. Подключись через nginx: wscat -c ws://127.0.0.1:8080/ws/. Введи строку - должно вернуться эхо. Сокет работает через прокси.
  • Шаг 6. Сломай специально: закомментируй строку proxy_set_header Upgrade, сделай reload, подключись снова. Получишь ошибку рукопожатия - наглядно видно, что апгрейд вырезается без явного проброса. Верни строку обратно.
  • Шаг 7. Проверь таймаут: поставь proxy_read_timeout 10s, подключись и молчи 11 секунд - nginx разорвёт сокет. Это та самая грабля коротких таймаутов. Верни 3600s.
Контрольные вопросы
  • Почему заголовки Connection и Upgrade нельзя просто пробросить как есть, и зачем для этого нужен блок map $http_upgrade $connection_upgrade?
  • Чем проксирование gRPC отличается от обычного proxy_pass и почему для него обязателен http2 on на входе?
  • Какая директива чаще всего ломает SSE и потоковую отдачу, и какими двумя способами (со стороны конфига и со стороны бэкенда) это лечится?
  • Почему WebSocket-соединение может стабильно рваться примерно раз в минуту, даже если конфиг апгрейда корректен?
Итог

Не-обычный HTTP в nginx держится на коротком списке директив, которые легко забыть. WebSocket - это map $http_upgrade $connection_upgrade плюс проброс Upgrade/Connection, proxy_http_version 1.1 и длинные таймауты; в эталонном проекте это вынесено в global_proxy.conf один раз на всё. gRPC - это grpc_pass поверх обязательного http2 on, со своими grpc_set_header и пониманием, что балансируются соединения, а не RPC. SSE и стриминг - это в первую очередь proxy_buffering off и X-Accel-Buffering: no, иначе realtime превращается в задержку. Запомни главную мысль урока: буферизация и короткие таймауты - твои друзья на обычном HTTP и твои враги на всём, что живёт долго и течёт потоком.
👍1 ❤️4 🔥1 😄 🤔2
Аватара пользователя
Ianc
Сообщения: 1
Зарегистрирован: 30 май 2026, 22:34

Re: WebSocket, gRPC и стриминг через Nginx

Сообщение Ianc »

Сидел два дня на 400 при апгрейде, оказалось map просто лежал не в http а в server. Спасибо что прямо на этом акцент сделал, теперь буду первым делом nginx -t смотреть.
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
peggyl
Сообщения: 1
Зарегистрирован: 28 май 2026, 12:44

Re: WebSocket, gRPC и стриминг через Nginx

Сообщение peggyl »

А по gRPC вопрос: если балансируются соединения а не вызовы, то least_conn в апстриме вообще смысл имеет при паре жирных клиентов? Похоже что нет и надо клиентскую балансировку, правильно понял?
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
FastCGI и PHP: nginx + PHP-FPM (и uwsgi/scgi)
Следующая глава →
Кэширование ответов: proxy_cache и микрокэш

Все главы курса «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Поделиться темой: ✈ Telegram VK
Похожие запросы: WebSocket и gRPC через nginx

Вернуться в «Nginx профессионально: от первого конфига до API-gateway на OpenResty и Angie»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя