Когда упираешься в ограничения L7‑прокси (HTTP), на сцену выходит модуль Nginx stream
: он работает на уровне L4 и умеет балансировать произвольные TCP/UDP протоколы — от баз данных и SMTP/IMAP до DNS, RADIUS и собственных бинарных протоколов. В этой статье я покажу, как собрать устойчивый балансировщик на nginx stream
, включить TLS passthrough с маршрутизацией по SNI, аккуратно настроить таймауты и логирование, а также организовать health‑checks без «магии» и простоя.
Когда выбирать Nginx stream (L4) вместо HTTP (L7)
Балансировка на уровне TCP/UDP нужна в случаях, когда:
- протокол не HTTP и его нельзя проксировать через
http{}
; - нужно передавать шифрованный трафик без расшифровки на балансировщике (TLS passthrough);
- важна максимальная прозрачность и минимальный overhead;
- приложение требует одинаковое исходное соединение на протяжении всей сессии (stateful TCP);
- UDP‑службы (DNS, syslog, RADIUS, VoIP‑сигналинг) нуждаются в простом распределении нагрузки.
Цена за простоту L4: нет доступа к заголовкам/телу HTTP, нельзя применять фильтры контента, компрессию, WAF и т. п. Всё это — задачи L7.
Базовая архитектура: upstream, server, listen
Конфигурация stream
похожа на http
, но с меньшим набором директив. Минимальный шаблон для TCP:
stream {
# Формат логов для TCP/UDP с полезными полями
log_format stream '$remote_addr:$remote_port $server_addr:$server_port '
'$protocol $upstream_addr $session_time '
'$bytes_sent/$bytes_received $ssl_preread_server_name';
access_log /var/log/nginx/stream_access.log stream buffer=128k flush=1s;
# Лимит одновременных сессий на IP
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
upstream app_tcp {
# Метод балансировки: по умолчанию round-robin, можно включить least_conn/hash
# least_conn;
server 10.0.0.11:9000 max_fails=3 fail_timeout=30s;
server 10.0.0.12:9000 max_fails=3 fail_timeout=30s;
}
server {
listen 9000 reuseport so_keepalive=on backlog=65535;
proxy_connect_timeout 3s;
proxy_timeout 60s;
proxy_pass app_tcp;
limit_conn conn_per_ip 50;
}
}
Где запускать такой балансировщик: чаще всего это отдельный узел. Удобно поднять его на VDS, оставив бэкенды за приватной сетью.
Ключевые моменты:
reuseport
повышает параллелизм при большом количестве соединений;so_keepalive=on
полезен для «зависших» TCP‑сессий;proxy_connect_timeout
иproxy_timeout
— основа грума для пассивных health‑checks и устойчивости;max_fails
иfail_timeout
реализуют пассивный health‑check (см. ниже).
Методы балансировки и “липкость” соединений
В stream
доступны:
- round-robin по умолчанию;
- least_conn — полезен для долгих TCP‑сессий;
- hash — детерминированное распределение, есть модификатор
consistent
для минимизации перетасовок.
Пример консистентного хеша по клиентскому адресу, когда важно, чтобы один клиент попадал на один и тот же backend (stateful‑сервисы, UDP):
upstream kv_udp {
hash $remote_addr consistent;
server 10.0.0.21:11211;
server 10.0.0.22:11211;
}

UDP: специфика и типовые настройки
UDP не имеет «соединений» в привычном смысле, поэтому часть TCP‑директив не работает. Для UDP обратите внимание на:
listen ... udp
— явное включение режима UDP;proxy_timeout
— таймаут бездействия между пакетами;proxy_responses
— сколько ответов ожидается от upstream на один запрос (для DNS это обычно1
);reuseport
— обязателен при интенсивном UDP.
stream {
upstream dns_udp {
hash $remote_addr consistent;
server 10.0.0.31:53;
server 10.0.0.32:53;
}
server {
listen 53 udp reuseport;
proxy_timeout 2s;
proxy_responses 1;
proxy_pass dns_udp;
}
}
Для UDP‑сервисов полезно ограничение по IP с помощью limit_conn
и фильтрация доступа (allow/deny
) — так можно отсечь случайные сканы и DDoS на раннем этапе.
TLS passthrough и маршрутизация по SNI
Иногда нужно принимать TLS на 443 и не расшифровывать его на балансировщике, а отправлять дальше как есть: это и есть TLS passthrough. В stream
это делается директивой ssl_preread on;
, которая позволяет считать SNI и ALPN без завершения рукопожатия.
Типичный кейс — на одном IP:443 живут разные сервисы, и вы хотите:
- часть хостов отправлять напрямую к backend’ам (passthrough);
- часть — завернуть на локальный Nginx для терминации TLS и L7‑логики.
stream {
map $ssl_preread_server_name $upstream_name {
default tls_default; # Фолбэк
api.example.com local_https; # Терминируем TLS локально
db.example.com mysql_ro; # Passthrough к TCP:3306
mail.example.com imaps_pool; # Passthrough к IMAPS:993
}
upstream tls_default { server 10.0.0.40:443; }
upstream local_https { server 127.0.0.1:8443; }
upstream mysql_ro { server 10.0.0.41:3306; }
upstream imaps_pool { server 10.0.0.51:993; server 10.0.0.52:993; }
server {
listen 443 reuseport;
ssl_preread on;
proxy_timeout 180s;
proxy_pass $upstream_name;
}
}
Далее в секции http{}
поднимем локальный HTTPS на 127.0.0.1:8443 — это позволит терминировать TLS на балансировщике только для выбранных имён:
http {
server {
listen 127.0.0.1:8443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/nginx/certs/api.crt;
ssl_certificate_key /etc/nginx/certs/api.key;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:8080;
}
}
}
Важно: порт 443 не может одновременно слушать и http{}
, и stream{}
в одном процессе Nginx. Поэтому мы слушаем 443 в stream{}
, а для терминации используем локальный вспомогательный порт (например, 8443). Для локальной терминации потребуются валидные SSL-сертификаты. Если у вас много поддоменов, пригодится автоматизация выпуска wildcard‑сертификатов по DNS‑01.

ALPN‑маршрутизация
В некоторых сценариях стоит учитывать ALPN (например, разделять h2 и http/1.1). Переменная $ssl_preread_alpn_protocols
доступна вместе с ssl_preread on;
. Её можно задействовать в map
для более тонкой маршрутизации. Например, трафик gRPC (h2) направлять в отдельный upstream.
Передача реального IP: PROXY protocol
При TLS passthrough backend не увидит исходный IP, если не использовать PROXY protocol. Включаем его на входе и на выходе из балансировщика, а также на бэкенде.
stream {
map $ssl_preread_server_name $u { default be; }
upstream be { server 10.0.0.60:443; }
server {
listen 443 proxy_protocol reuseport;
ssl_preread on;
proxy_protocol on; # Передать PROXY протокол к бэкенду
proxy_pass $u;
}
}
На стороне backend (если это Nginx с терминацией TLS) он должен ожидать PROXY‑заголовок и уметь извлечь реальный IP:
http {
server {
listen 443 ssl http2 proxy_protocol;
real_ip_header proxy_protocol;
set_real_ip_from 10.0.0.10; # IP балансировщика
server_name app.example.com;
ssl_certificate /etc/nginx/certs/app.crt;
ssl_certificate_key /etc/nginx/certs/app.key;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8081;
}
}
}
Если backend не ожидает PROXY‑протокол, он посчитает первые байты рукопожатия «мусором» и соединение не установится. Проверьте согласованность настроек.
Доступ, ACL и защита
В stream
доступны простые ACL:
server {
listen 3306 reuseport;
allow 192.0.2.0/24;
allow 198.51.100.10;
deny all;
proxy_pass mysql_pool;
}
Для TCP/UDP‑сервисов с ограниченным кругом клиентов (админские базы, RDP, SMTP‑релеи) это часто самый быстрый и эффективный способ снизить риски ещё до бэкендов.
Логи и метрики stream
Логирование в stream
— это лог сессий, а не HTTP‑запросов. Рекомендуемые поля: адреса клиента и сервера, протокол, адрес upstream, время сессии, объём трафика, SNI.
stream {
log_format stream '$remote_addr:$remote_port $server_addr:$server_port '
'$protocol $upstream_addr $session_time '
'$bytes_sent/$bytes_received $ssl_preread_server_name';
access_log /var/log/nginx/stream_access.log stream;
}
Для UDP логи покажут длительность «квазисессии» между первым и последним пакетом по ключу. Для полноценного мониторинга TCP/UDP пригодятся экспорт метрик из логов и внешний пробывальщик портов.
Таймауты, ретраи и пассивные health‑checks
В open‑source версии Nginx активные health‑checks для stream
недоступны. Но пассивные проверки работают надёжно:
max_fails
— число ошибок на бэкенде перед тем, как признать его временно «плохим»;fail_timeout
— окно времени ошибок и период, на который сервер исключается;proxy_connect_timeout
— таймаут установки TCP‑соединения к бэкенду;proxy_timeout
— таймаут неактивности сессии.
upstream app_tcp {
least_conn;
server 10.0.0.11:9000 max_fails=2 fail_timeout=20s;
server 10.0.0.12:9000 max_fails=2 fail_timeout=20s;
}
server {
listen 9000 reuseport;
proxy_connect_timeout 2s;
proxy_timeout 45s;
proxy_pass app_tcp;
}
Для повышения живучести добавьте ретраи на уровне балансировщика (при ошибке подключения к одному узлу попробовать другой):
server {
listen 9000;
proxy_next_upstream on;
proxy_next_upstream_tries 2;
proxy_pass app_tcp;
}
Нюанс: ретраи уместны для идемпотентных протоколов. Для stateful TCP‑протоколов (долгие сессии) повторное подключение может не иметь смысла — лучше доверьтесь пассивным checks и аккуратным таймаутам.
Активные health‑checks: реалистичные варианты
Если нужны активные проверочные подключения (port‑probe, баннер‑чек баз данных, SMTP/EHLO и т. п.), в open‑source Nginx придётся обойтись внешними средствами. Типовой приём:
- Заводим include‑файл с составом upstream, например
/etc/nginx/stream.d/app_upstream.conf
. - Периодический скрипт проверяет бэкенды и помечает неуспешные как
down
(генерирует новый include). - Атомарно подменяем include и делаем
nginx -s reload
без простоя.
# /etc/nginx/nginx.conf (фрагмент)
stream {
upstream app_tcp {
zone app:64k;
include /etc/nginx/stream.d/app_upstream.conf;
}
server { listen 9000; proxy_pass app_tcp; }
}
# /usr/local/bin/check-app.sh (упрощённо)
BACKENDS="10.0.0.11:9000 10.0.0.12:9000 10.0.0.13:9000"
OUT=/etc/nginx/stream.d/app_upstream.conf.new
: > "$OUT"
for b in $BACKENDS; do
host=$(echo $b | cut -d: -f1)
port=$(echo $b | cut -d: -f2)
if nc -z -w1 "$host" "$port"; then
echo "server $b;" >> "$OUT"
else
echo "server $b down;" >> "$OUT"
fi
done
mv "$OUT" /etc/nginx/stream.d/app_upstream.conf
nginx -t && nginx -s reload
Такой подход хорошо сочетается с DNS‑имёнами и resolve
, если IP‑адреса меняются динамически:
upstream app_tcp {
zone app:64k;
server app1.internal:9000 resolve max_fails=2 fail_timeout=20s;
server app2.internal:9000 resolve max_fails=2 fail_timeout=20s;
resolver 127.0.0.1 valid=30s ipv6=off;
}
Производительность и системные настройки
Пара общественных рекомендаций для высоконагруженных TCP/UDP‑балансировщиков:
worker_processes auto;
,worker_rlimit_nofile
и щедрыйworker_connections
;listen ... reuseport
на всех «горячих» портах;backlog
увеличьте до десятков тысяч, если есть всплески входящих;so_keepalive
включайте для TCP сессий, где важна детекция обрывов;- подберите
proxy_timeout
под характер протокола (короткий для UDP/DNS, длиннее для IMAP/DB).
worker_processes auto;
worker_rlimit_nofile 200000;
events {
worker_connections 65535;
multi_accept on;
}
Частые ошибки и диагностика
- Конфликт порта 443 между http и stream. Один процесс Nginx не может слушать один и тот же IP:порт в двух контекстах. Решение: слушайте 443 в
stream{}
и маршрутизируйте часть хостов на локальный HTTPS‑порт; либо используйте разные IP. - Несогласованный PROXY protocol. Если включили на балансировщике, включайте и на бэкенде. Иначе будут «битые» рукопожатия.
- Нет
resolver
для именных upstream. Без него Nginx резолвит имена только при старте/перезагрузке. Добавьтеresolver
и флагresolve
у серверов. - Слишком малые таймауты. Преждевременные обрывы TCP выглядят как «рандомные» ошибки приложения. Подберите
proxy_timeout
иproxy_connect_timeout
под протокол. - UDP без
proxy_responses
. Для DNS обязательно ограничивайте количеством ответов, иначе возможны утечки «залипших» псевдосессий. - TLS passthrough без SNI. Старые клиенты без SNI попадут в
default
. Продумайте фолбэк или выделите отдельный IP.
Пошаговые рецепты
1) Балансировка MySQL (TCP, пассивные checks)
stream {
upstream mysql_pool {
least_conn;
server 10.0.1.11:3306 max_fails=2 fail_timeout=20s;
server 10.0.1.12:3306 max_fails=2 fail_timeout=20s;
}
server {
listen 3306 reuseport so_keepalive=on;
proxy_connect_timeout 2s;
proxy_timeout 300s; # длинные транзакции
proxy_pass mysql_pool;
allow 203.0.113.0/24;
deny all;
}
}
2) Балансировка DNS (UDP, консистентный хеш)
stream {
upstream dns_udp {
hash $remote_addr consistent;
server 10.0.2.21:53;
server 10.0.2.22:53;
}
server {
listen 53 udp reuseport;
proxy_timeout 2s;
proxy_responses 1;
proxy_pass dns_udp;
}
}
3) Единый 443: TLS passthrough + локальная терминация для части хостов
stream {
map $ssl_preread_server_name $route {
default be_default;
app.example.com local_https;
imap.example.com imaps_be;
}
upstream be_default { server 10.0.3.30:443; }
upstream local_https { server 127.0.0.1:8443; }
upstream imaps_be { server 10.0.3.40:993; }
server {
listen 443 reuseport;
ssl_preread on;
proxy_pass $route;
}
}
http {
server {
listen 127.0.0.1:8443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/nginx/certs/app.crt;
ssl_certificate_key /etc/nginx/certs/app.key;
location / { proxy_pass http://127.0.0.1:8080; }
}
}
Чек‑лист внедрения
- Определите протоколы и требования: TCP/UDP, нужен ли TLS passthrough, требуется ли видеть клиентский IP на бэкендах.
- Выберите метод балансировки:
least_conn
для длинных TCP‑сессий,hash
для stateful/UDP. - Настройте таймауты и пассивные health‑checks; при необходимости организуйте внешние активные проверки и генерацию include‑файлов.
- Включите логирование и определите формат, пригодный для алертинга.
- Проверьте ACL, лимиты соединений и системные параметры (
reuseport
,backlog
,worker_connections
). - Протестируйте SNI‑маршрутизацию и PROXY protocol на стенде, имитируйте падение бэкендов.
Главная мысль: Nginx stream — отличный инструмент там, где требуется простая и быстрая L4‑балансировка, прозрачный TLS passthrough и минимальный overhead. За глубокую инспекцию и сложные политики отвечает слой L7.
Итог
Мы разобрали фундаментальные сценарии nginx stream
для TCP/UDP, настроили TLS passthrough c маршрутизацией по SNI и передачей реального IP через PROXY protocol, обсудили пассивные и внешние health‑checks, логи и типовые ловушки. Этого достаточно, чтобы построить надёжный L4‑балансировщик для продуктивной среды и обеспечить предсказуемое поведение при сбоях бэкендов.