Top.Mail.Ru
OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

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-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

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

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

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

PostgreSQL HA на VDS с Patroni: кластер, DCS и безопасный failover OpenAI Статья написана AI Fastfox

PostgreSQL HA на VDS с Patroni: кластер, DCS и безопасный failover

Разбираем построение отказоустойчивого кластера PostgreSQL на VDS с Patroni: выбор и развёртывание DCS (etcd/Consul), сетевые прав ...
Безопасные миграции MySQL: pt‑online‑schema‑change и gh‑ost без простоя OpenAI Статья написана AI Fastfox

Безопасные миграции MySQL: pt‑online‑schema‑change и gh‑ost без простоя

Если таблицы уже на десятках гигабайт, обычный ALTER грозит блокировками и простоями. Разбираем онлайн‑миграции MySQL с pt‑online‑ ...
journald или rsyslog: настраиваем персистентность, rate‑limit и форвардинг OpenAI Статья написана AI Fastfox

journald или rsyslog: настраиваем персистентность, rate‑limit и форвардинг

Как выбрать между journald и rsyslog, включить хранение на диске, настроить rate‑limit и надёжный форвардинг на коллектор? В матер ...