Это и есть nginx stream - отдельный контекст
Код: Выделить всё
stream {}Код: Выделить всё
http {}Контекст stream {} - это не про tcp_nopush из http
Сразу убираем главную путаницу, на которой спотыкаются даже опытные люди. В эталонном проекте ngx-trace-gateway есть файл globals/global_tcp.conf со строками
Код: Выделить всё
sendfile on; tcp_nopush on; tcp_nodelay on;Код: Выделить всё
http {}Настоящий stream - это отдельный блок верхнего уровня, рядом с http, а не внутри него:
Код: Выделить всё
# nginx.conf
user nginx;
worker_processes auto;
events { worker_connections 10240; }
http {
# ... весь твой HTTP-мир: server {}, location {}, proxy_pass на http://...
}
stream {
# ... совершенно другой мир: TCP/UDP L4-проксирование
upstream postgres_backend {
server 10.0.0.11:5432 max_fails=2 fail_timeout=10s;
server 10.0.0.12:5432 max_fails=2 fail_timeout=10s;
}
server {
listen 5432;
proxy_pass postgres_backend;
}
}
Код: Выделить всё
location
nginx tcp proxy: балансировка реплик базы и очередей
Самый частый сценарий - nginx tcp балансировка для не-HTTP сервисов. Реплики PostgreSQL, кластер Redis, ноды RabbitMQ, пул SMTP-релеев. Клиент коннектится на один адрес nginx, а тот раскидывает соединения по живым бэкендам. Соберём боевой конфиг для read-реплик базы:
Код: Выделить всё
stream {
log_format stream_log '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time "$upstream_addr" '
'$upstream_connect_time $upstream_bytes_sent';
upstream db_read_replicas {
least_conn; # метод балансировки для долгих соединений
zone db_read 64k; # shared-зона для состояния пиров между воркерами
server 10.0.0.21:5432 weight=2 max_fails=3 fail_timeout=30s;
server 10.0.0.22:5432 max_fails=3 fail_timeout=30s;
server 10.0.0.23:5432 max_fails=3 fail_timeout=30s backup;
}
server {
listen 5432;
proxy_pass db_read_replicas;
proxy_connect_timeout 3s; # сколько ждём установки коннекта к бэкенду
proxy_timeout 30m; # таймаут БЕЗДЕЙСТВИЯ соединения (не всей сессии!)
proxy_next_upstream on; # пробовать следующий пир при ошибке коннекта
proxy_next_upstream_tries 2;
access_log /var/log/nginx/db_stream.log stream_log;
}
}
Код: Выделить всё
least_connКод: Выделить всё
hash $remote_addr consistentКод: Выделить всё
random two least_connКод: Выделить всё
ip_hashКод: Выделить всё
hash $remote_addrproxy_timeout - главная грабля новичка. Это не лимит длительности сессии, а таймаут простоя: если по соединению в обе стороны N времени не идёт байтов, nginx его рвёт. Для БД с длинными idle-транзакциями ставь минуты, иначе клиент будет получать внезапные обрывы соединения. По умолчанию там 10 минут.
Health checks: в open-source nginx активных проверок здоровья для stream нет - только пассивные через
Код: Выделить всё
max_failsКод: Выделить всё
fail_timeoutКод: Выделить всё
health_checknginx udp: DNS, syslog и почему это сложнее
TCP - это установленное соединение с понятным жизненным циклом. nginx udp - совсем другая история, потому что UDP не имеет соединений вообще: это разрозненные датаграммы без состояния. nginx эмулирует "сессию" по паре адрес+порт клиента, но это искусственная конструкция. Балансируем DNS-резолверы:
Код: Выделить всё
stream {
upstream dns_servers {
server 10.0.0.31:53;
server 10.0.0.32:53;
}
server {
listen 53 udp; # ОБЯЗАТЕЛЕН параметр udp
proxy_pass dns_servers;
proxy_responses 1; # ждём ровно 1 датаграмму-ответ, потом закрываем сессию
proxy_timeout 3s; # короткий таймаут - DNS быстрый
}
# А вот syslog - односторонний, ответа НЕТ:
server {
listen 514 udp;
proxy_pass log_collectors;
proxy_responses 0; # ответов не ждём вообще, сессия закрывается сразу
}
}
Код: Выделить всё
proxy_timeoutГрабли UDP, которые надо знать заранее:
- UDP stateless - "сессия" в nginx это фикция по адресу клиента. За NAT десятки клиентов могут слиться в одну сессию, если у них совпал внешний адрес+порт. Для тяжёлого UDP это реальная проблема.
- Если протокол шлёт несколько датаграмм в ответ (а ты поставил proxy_responses 1), nginx закроет сессию после первой и потеряет остальные. Знай ответную семантику своего протокола.
- Балансировка UDP по умолчанию round-robin раскидывает каждую датаграмму потенциально на разный бэкенд. Для stateful-протоколов поверх UDP (тот же QUIC, игровые серверы) это ломает всё - нужен , чтобы прибить клиента к одному бэкенду.
Код: Выделить всё
hash $remote_addr - Параметр на listen раскидывает UDP-датаграммы по воркерам ядром, снимая узкое место единого сокета - для высоконагруженного UDP почти обязателен.
Код: Выделить всё
reuseport
Теперь самое мощное. Представь: на одном внешнем IP и порту 443 у тебя несколько разных бэкендов, и каждый сам владеет своим TLS-сертификатом. Ты не хочешь терминировать TLS на nginx (private key должен остаться на бэкенде - например, по требованиям безопасности, или это чужой managed-сервис). Как раскидать трафик по домену, если он зашифрован?
Ответ - nginx ssl_preread. Модуль ngx_stream_ssl_preread читает самое начало TLS-рукопожатия - сообщение ClientHello, которое клиент шлёт открытым текстом до шифрования. В ClientHello лежит расширение SNI (Server Name Indication) с запрашиваемым доменом. nginx подсматривает имя, не расшифровывая ничего, и роутит соединение целиком на нужный бэкенд. End-to-end TLS сохраняется: nginx вообще не видит расшифрованный трафик, это TLS passthrough.
Код: Выделить всё
stream {
map $ssl_preread_server_name $tls_upstream {
api.example.com api_pool;
grafana.example.com grafana_pool;
~^.*\.internal\.lan$ internal_pool; # по regex-маске поддоменов
default fallback_pool;
}
upstream api_pool { server 10.0.0.41:8443; }
upstream grafana_pool { server 10.0.0.42:3000; }
upstream internal_pool { server 10.0.0.43:8443; }
upstream fallback_pool { server 10.0.0.44:8443; }
server {
listen 443;
ssl_preread on; # включаем чтение ClientHello БЕЗ терминации
proxy_pass $tls_upstream; # бэкенд из map по SNI
proxy_protocol on; # отдадим бэкенду реальный IP клиента (см. ниже)
}
}
Код: Выделить всё
ssl_preread onКод: Выделить всё
$ssl_preread_server_nameКод: Выделить всё
proxy_pass $tls_upstreamКод: Выделить всё
$ssl_preread_protocolКод: Выделить всё
$ssl_preread_alpn_protocolsВажное ограничение: ssl_preread видит только то, что в открытом ClientHello. Если клиент использует ECH (Encrypted Client Hello, шифрование SNI - появляется массово в 2026), то имя домена внутри зашифровано, и preread его не достанет. Это by design ECH - спрятать SNI от промежуточных узлов. Учитывай при проектировании passthrough-роутинга.
nginx proxy_protocol: реальный IP клиента через L4
Вот фундаментальная проблема L4-проксирования. Когда nginx проксирует TCP, бэкенд видит в качестве источника соединения адрес nginx, а не клиента - на уровне TCP так и должно быть, соединение-то новое. В HTTP мы решаем это заголовком
Код: Выделить всё
X-Forwarded-ForОтвет - nginx proxy_protocol: бинарный (v2) или текстовый (v1) префикс, который nginx дописывает в самое начало TCP-соединения к бэкенду. В этом префиксе - настоящие адрес и порт клиента. Бэкенд (PostgreSQL с расширением, HAProxy, Envoy, другой nginx, многие приложения) парсит этот заголовок и узнаёт клиента. Это L4-аналог X-Forwarded-For.
Связь с realip работает в обе стороны. Исходящая - nginx добавляет proxy_protocol к бэкенду через
Код: Выделить всё
proxy_protocol on;Код: Выделить всё
stream {
server {
listen 5432 proxy_protocol; # ПРИНИМАЕМ proxy_protocol от вышестоящего LB
# модуль ngx_stream_realip восстанавливает $remote_addr из заголовка:
set_real_ip_from 10.0.0.0/8; # доверяем ТОЛЬКО известной сети LB
set_real_ip_from 172.16.0.0/12;
proxy_pass db_read_replicas;
proxy_protocol on; # и дальше пробрасываем настоящий IP бэкенду
}
}
Код: Выделить всё
set_real_ip_fromЕщё одна мина: если ты поставил
Код: Выделить всё
listen ... proxy_protocolЛимиты, логи и TLS-терминация в stream
Защита соединениями. В stream работает
Код: Выделить всё
limit_connКод: Выделить всё
stream {
limit_conn_zone $binary_remote_addr zone=perip:10m;
server {
listen 6379; # Redis
limit_conn perip 10; # не более 10 коннектов с одного IP
proxy_pass redis_backend;
}
}
Код: Выделить всё
listen 5432 ssl;Код: Выделить всё
ssl_certificateКод: Выделить всё
ssl_certificate_keyКод: Выделить всё
ssl_protocols TLSv1.2 TLSv1.3;Код: Выделить всё
ssl_conf_command Ciphersuites ...Логирование в stream обязательно настраивать вручную - дефолтного access_log тут нет, и без него ты слеп. Свой
Код: Выделить всё
log_formatКод: Выделить всё
$protocolКод: Выделить всё
$statusКод: Выделить всё
$bytes_sentКод: Выделить всё
$bytes_receivedКод: Выделить всё
$session_timeКод: Выделить всё
$upstream_addrКод: Выделить всё
$upstream_connect_timeКод: Выделить всё
$ssl_preread_server_nameМини-лаба: TCP-балансировка и SNI-роутинг руками
Повторяем по шагам на тестовой машине. Понадобятся nginx с модулями stream (в официальных пакетах и docker они уже собраны).
- Шаг 1. Проверь, что stream-модуль есть: . Должны увидеть
Код: Выделить всё
nginx -V 2>&1 | tr ' ' '\n' | grep stream,Код: Выделить всё
--with-stream,Код: Выделить всё
--with-stream_ssl_preread_module.Код: Выделить всё
--with-stream_realip_module - Шаг 2. Подними два простых TCP-бэкенда в разных терминалах: и
Код: Выделить всё
ncat -lk 9001 --sh-exec 'echo BACKEND-1'.Код: Выделить всё
ncat -lk 9002 --sh-exec 'echo BACKEND-2' - Шаг 3. Создай stream-конфиг с upstream из 9001 и 9002 (round-robin) и , как в примере с db_read_replicas, но порты 9001/9002.
Код: Выделить всё
listen 9000; - Шаг 4. Проверь синтаксис и перечитай: . Помни - healthcheck в эталонном docker как раз и есть
Код: Выделить всё
nginx -t && nginx -s reload.Код: Выделить всё
nginx -t - Шаг 5. Постучись несколько раз: . Увидишь, что ответы чередуются BACKEND-1 / BACKEND-2 - это round-robin в действии.
Код: Выделить всё
for i in 1 2 3 4; do ncat 127.0.0.1 9000; done - Шаг 6. Останови ncat на 9002 и снова постучись - после пары ошибок (max_fails) nginx выведет мёртвый пир и пойдёт только на живой. Это пассивный health check.
- Шаг 7. Для SNI: добавь ,
Код: Выделить всё
map $ssl_preread_server_nameиКод: Выделить всё
ssl_preread on;. Проверь роутинг:Код: Выделить всё
listen 8443;противКод: Выделить всё
openssl s_client -connect 127.0.0.1:8443 -servername api.example.comи убедись по access_log, что попал на разные upstream.Код: Выделить всё
-servername grafana.example.com
- Чем директивы из global_tcp.conf эталона (sendfile, tcp_nopush) отличаются от контекста ? Где живёт каждая?
Код: Выделить всё
stream {} - Зачем нужен в UDP-прокси и какое значение поставишь для DNS, а какое для одностороннего syslog?
Код: Выделить всё
proxy_responses - Как ssl_preread роутит TLS-трафик по домену, ничего не расшифровывая, и почему ECH ломает этот подход?
- Почему нельзя открывать на 0.0.0.0/0 при приёме proxy_protocol? Что сможет сделать злоумышленник?
Код: Выделить всё
set_real_ip_from
stream-модуль превращает nginx из HTTP-прокси в универсальный L4-шлюз для любого TCP и UDP. Запомни четыре опоры: это отдельный контекст stream {} (не путать с tcp_nopush из http), балансировка не-HTTP сервисов с пассивными health checks (активные - в Angie), UDP требует осторожности с proxy_responses и stateless-сессиями, а ssl_preread плюс proxy_protocol дают SNI-роутинг без расшифровки TLS и передачу реального IP клиента сквозь L4. Там, где за одним входом стоят базы, очереди, DNS и зашифрованные домены, stream незаменим - HTTP-блок тут просто не применим.