1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

Что такое stream и чем он не HTTP

Коротко

stream - второй корневой блок рядом с http, работающий на уровне соединения: принял, выбрал сервер, отдал бэкенду и дальше перекладывает байты. Ни location, ни URI, ни заголовков - и это не «так не принято», а отказ загрузиться. listen 53 и listen 53 udp - два разных сокета. У UDP-сессии нет конца, поэтому длину назначаешь ты: proxy_responses закрывает её сразу после нужного числа ответов, без него она висит до proxy_timeout.

Уже гонял базы через nginx - листай до «Теперь сам». Разборы про коды в логе stream и про длину UDP-сессии.

Два блока на одном порту, оба без имени. Что скажет nginx -t?

/etc/nginx/nginx.confstream
server { listen 7100; proxy_pass pg-primary:5432; }
server { listen 7100; proxy_pass pg-standby:5432; }

Половина руководств обещает отказ при старте: «в stream два сервера на порту - ошибка конфигурации». На 1.31.5 конфиг принимается:

↳  выводnginx -t
nginx: [warn] conflicting server name "" on 0.0.0.0:7100, ignored
nginx: configuration file /etc/nginx/nginx.conf test is successful

Второй блок выброшен, весь трафик идёт в первый. Предупреждение уезжает в вывод nginx -t, которого при systemctl reload никто не видит.

На что это похоже

http-блок - это приёмная: конверт вскрывают, читают адресата на первой странице и по нему решают, кому нести. Правил там много, потому что содержимое конверта понятно.

stream-блок - патч-панель: провод из розетки 5432 воткнут в порт базы. Что бежит по проводу, панель не знает и знать не должна. Поэтому в ней нет ни одной ручки, которая требовала бы понимания содержимого, - и именно поэтому через неё проходит что угодно: PostgreSQL, Redis, SMTP, gRPC поверх TLS, игровой протокол собственного сочинения.

Механизм: путь соединения

соединение принято preread: первые байты, не съедая выбор server: адрес, порт, udp/tcp allow, deny, limit_conn proxy_pass: байты в обе стороны ssl_preread отказ = обрыв
Фаз мало, и ни одна из них не заглядывает в протокол глубже первых байтов

Правило

Блок выбирается по паре «адрес и порт» плюс транспорт, а с 1.25.5 ещё и по имени из SNI. Дальше запрос проходит доступ и уходит бэкенду целиком. Всё, что в http относится к содержимому - location, add_header, кеш, сжатие, try_files, - в stream отсутствует, и проверяется это при старте, а не молча.

↳  выводnginx -t с location внутри stream
nginx: [emerg] "location" directive is not allowed here in /etc/nginx/nginx.conf:4

Разбор: коды ответа, которых клиент не видит

Расхожая формулировка «в stream нет кодов ответа» верна ровно наполовину. Клиент их действительно не видит: у TCP нет места, куда положить код, поэтому отказ выглядит как оборванное соединение. А вот в логе код есть - переменная $status в stream существует и означает исход сессии.

Четыре сессии одного прогона стенда, формат $remote_addr $protocol $status $bytes_sent/$bytes_received $session_time ups=$upstream_addr:

↳  вывод/var/log/nginx/stream.log
172.25.0.4 TCP 200 13/1 0.001 ups=172.25.0.2:6001
172.25.0.4 TCP 403 0/0 0.000 ups=-
172.25.0.4 TCP 503 0/0 0.000 ups=-
172.25.0.4 TCP 502 0/0 0.000 ups=172.25.0.2:6999
что случилось $status что увидел клиент
сессия прошла нормально 200 данные бэкенда
deny не пустил 403 connection reset by peer
limit_conn не пустил 503 connection reset by peer
бэкенд не отозвался 502 connection reset by peer

Три разные причины дают клиенту один и тот же симптом. Различить их можно только со своей стороны, и только если лог включён.

Разбор: сколько живёт UDP-сессия

У TCP конец сессии определяет сам протокол. У UDP его нет: пришла датаграмма, ушла датаграмма, а сколько ещё ждать - вопрос к конфигу. Стенд: бэкенд отвечает дважды, первый ответ сразу, второй через секунду.

/etc/nginx/nginx.confstream
server {
    listen 7061 udp;
    proxy_responses 1;
    proxy_timeout 5s;
    proxy_pass 10.0.0.53:53;
}
настройка что получил клиент сколько жила сессия
proxy_responses 1 только первый ответ 0,001 с
proxy_responses 2 оба ответа 1,003 с
без proxy_responses оба ответа 6,003 с

Последняя строка складывается из времени последнего ответа и proxy_timeout: 1 + 5 = 6. То есть без proxy_responses каждая запись в кеше nginx о клиенте занимает память пять лишних секунд после того, как всё уже сказано. На DNS с тысячей запросов в секунду это тысячи мёртвых сессий одновременно.

Обратная сторона видна в первой строке: proxy_responses 1 обрубает вторую датаграмму. Для DNS это верно (ответ один), для протокола, отвечающего пачкой, - потеря данных без единой записи в логе.

Что ломается без этого

Лог молчит, потому что его никто не включал. Три сессии в stream и один запрос в http на одном и том же стенде дали в access.log ровно одну строку - от http. В stream логирование по умолчанию выключено, и «в логах ничего нет» означает не «трафика не было», а «ты не написал access_log».

Формат лога из http не грузится. Половина переменных, к которым ты привык, в stream не существует:

есть нет
$status, $protocol, $session_time $request, $uri, $host
$server_port, $connection $request_time, $args
$bytes_sent, $upstream_bytes_sent всё семейство $http_*

Скопированный log_format роняет старт с unknown "request" variable - и это хорошая новость: ошибка громкая. Хуже с $host: строку map $host $backend; переносят из http-конфига чаще всего, а в stream её нет вовсе.

Зачем это в работе

Три задачи, где stream - правильный ответ. Пара «основной и резервный» для базы: приложение знает один адрес, переключение живёт в конфиге nginx. Единственный вход в контур: наружу торчит один хост, за ним Redis, SMTP и всё остальное без своих балансировщиков. TLS-passthrough: соединение доходит до бэкенда шифрованным, и сертификат остаётся там, где ему место.

И одна, где stream - неправильный: HTTP. Соблазн «просто пробросить порт» велик, но за него платишь всем сразу - кешем, заголовками о клиенте, повтором запроса на другой бэкенд, разбором по URL. Для HTTP всегда http-блок.

Теперь сам

Нужно пустить через nginx DNS-резолвер 10.0.0.53. DNS живёт и на UDP, и на TCP. Напиши блоки и объясни, почему proxy_responses стоит только у одного.

Ответ: нужны два server - listen 53 udp; и listen 53;, потому что это разные сокеты, и блок с udp не примет TCP-соединение. proxy_responses 1 ставится только в UDP-блоке: у обычного DNS-ответа одна датаграмма, и без этой строки сессия проживёт весь proxy_timeout. В TCP-блоке директива не нужна - конец обмена там определяет сам протокол.

Главное

stream работает на уровне соединения: выбрал сервер по адресу, порту и транспорту, проверил доступ, отдал бэкенду байты. location, заголовки и всё остальное «про содержимое» отсутствуют - конфиг с ними не загрузится. Два блока на одном порту без имени дают предупреждение, а не отказ: второй молча игнорируется. $status в stream есть и различает 200, 403, 503 и 502, но клиент видит один симптом - оборванное соединение; лог по умолчанию выключен, а переменных $request, $uri и $host в stream нет. listen ... udp - отдельный сокет, и длину UDP-сессии задаёт proxy_responses: без него она живёт до proxy_timeout уже после последнего ответа.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.