Новинка Виртуальный VDS сервер в Финляндии от 390р
Выберите продукт

Nginx stream как TCP/UDP балансировщик: базы, TLS passthrough и health‑checks

Разбираем Nginx stream как L4‑балансировщик: когда он уместен вместо HTTP, как настроить TLS passthrough и маршрутизацию по SNI, передавать клиентский IP через PROXY protocol, работать с таймаутами и логами, организовать health‑checks и избежать ловушек.
Nginx stream как TCP/UDP балансировщик: базы, TLS passthrough и health‑checks

Когда упираешься в ограничения 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;
}
FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды Финляндия

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 в Nginx stream

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.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

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 придётся обойтись внешними средствами. Типовой приём:

  1. Заводим include‑файл с составом upstream, например /etc/nginx/stream.d/app_upstream.conf.
  2. Периодический скрипт проверяет бэкенды и помечает неуспешные как down (генерирует новый include).
  3. Атомарно подменяем 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;
}

Балансировка DNS по UDP в Nginx stream

Производительность и системные настройки

Пара общественных рекомендаций для высоконагруженных 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‑балансировщик для продуктивной среды и обеспечить предсказуемое поведение при сбоях бэкендов.

Поделиться статьей

Вам будет интересно

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...