Войти
Главная→Документация→Балансировка

Балансировка SMTP-нагрузки

SMTP-балансировщик принимает входящую почту на одном адресе и распределяет SMTP-сессии между backend-серверами SPZGate: поровну, по весам, с основными и резервными серверами, а также по статическим маршрутам — отдельно для отправителей и получателей.

Балансировщик ставится отдельной командой установщика (--balancer) на собственный сервер и не затрагивает установленные шлюзы. Он построен на HAProxy с Lua-модулем и работает в контейнере Docker.

Как это работает

  1. Клиент подключается к балансировщику и ведёт обычный SMTP-диалог: EHLO, при необходимости STARTTLS, MAIL FROM.
  2. На первой команде RCPT TO балансировщик знает отправителя и получателя и выбирает маршрут (см. статические маршруты), а из него — упорядоченный список backend-серверов.
  3. Балансировщик подключается к первому серверу списка: проверяет приветствие, выполняет EHLO, при TLS-сессии STARTTLS, передаёт реальный IP клиента командой XCLIENT, затем MAIL FROM и RCPT TO. Если сервер недоступен или не ответил за заданное время, берётся следующий.
  4. Дальше письмо пересылается построчно, а ответы backend-сервера передаются клиенту без изменений. Решения backend-сервера (например, отказ 5xx на получателя или письмо) не считаются сбоем и не вызывают переключения на другой сервер.
Маршрут выбирается один раз за сессию, по первому RCPT TO. Следующие получатели того же домена идут на тот же сервер. Получатель другого домена в той же сессии получает временный отказ 450 4.3.0 cannot route recipient to a different domain in this session, и отправляющий сервер повторяет его отдельной сессией — так ведут себя все стандартные MTA.

Установка

Требования к серверу балансировщика:

1

Включите XCLIENT на backend-серверах

Балансировщик передаёт backend-серверу реальный IP отправителя командой XCLIENT — иначе проверки по спам-листам, greylisting и репутация видели бы только IP балансировщика. На каждом backend-сервере SPZGate создайте в каталоге control почтового сервера файл xclient со значением 1 (внутри контейнера qmail — /var/qmail/control/xclient) и перезапустите почтовую часть:

echo 1 > /var/qmail/control/xclient
spzctl restart qmail
Команда XCLIENT принимается от любого клиента, а не только от балансировщика. Разрешите доступ к порту 25 backend-серверов только с IP балансировщика (файрвол) или оставляйте их во внутренней сети.
2

Подготовьте имя балансировщика

Балансировщик представляется backend-серверам в EHLO и в приветствии именем, которое задаётся параметром --hostname. Если backend-сервер проверяет HELO, у этого имени должна быть A-запись в DNS на IP балансировщика — иначе приём письма отклоняется ответом вида 550 Invalid HELO, address … not resolved.

3

Запустите установщик

На сервере балансировщика:

curl -fsSL https://spamorez.ru/spzgate/install.sh | sudo bash -s -- --balancer \
  --servers 10.0.0.11,10.0.0.12,10.0.0.13 --hostname bl.example.com

Установщик:

  • устанавливает Docker и плагин Docker Compose, если их нет;
  • скачивает пакет балансировщика и проверяет его контрольную сумму SHA-256;
  • создаёт каталог установки (по умолчанию /opt/spzgate-balancer) и записывает конфигурацию маршрутов conf/smtp_balancer.conf: серверы из --servers становятся серверами по умолчанию с равномерным распределением;
  • при указании --hostname задаёт имя сервера и прописывает его в /etc/hosts;
  • собирает и запускает контейнер spz-smtp-balancer (режим network_mode: host, автозапуск unless-stopped);
  • настраивает ротацию журналов (/etc/logrotate.d/spzgate-balancer) и ставит команду spzctl;
  • пишет ход установки в журнал (по умолчанию /var/log/spzgate-balancer-install.log).

Проверка после установки:

spzctl status
nc 127.0.0.1 25
ответ220 bl.example.com ESMTP

Структура каталога установки:

ПутьНазначение
conf/smtp_balancer.confмаршруты и параметры балансировки — основной файл настройки
conf/haproxy.confпорт приёма, таймауты сессии, путь к TLS-сертификату
conf/servercert.pemсертификат и ключ для STARTTLS со стороны клиентов (поставьте свой, затем пересоберите образ)
logs/журналы: connections.log, backend-error.log, backend-status.log
docker-compose.yml, Dockerfile, bin/, src/описание контейнера, собранный HAProxy и служебные скрипты
Свой сертификат для STARTTLS. Положите сертификат и закрытый ключ одним PEM-файлом в conf/servercert.pem и выполните в каталоге установки docker compose up -d --build.
Обновление. Запустите ту же команду установщика ещё раз. Файл conf/smtp_balancer.conf и каталог logs/ при этом сохраняются, параметр --servers повторно указывать не нужно.

Удаление:

cd /opt/spzgate-balancer && docker compose down
rm -rf /opt/spzgate-balancer /etc/logrotate.d/spzgate-balancer

Параметры установки

Параметры указываются после --balancer:

ПараметрПо умолчаниюОписание
--balancer—включает установку только балансировщика (без шлюза SPZGate); обязателен
--servers IP1,IP2,…—backend-серверы по умолчанию (адреса или имена через запятую). Обязателен при первой установке; при обновлении можно не указывать, если конфигурация уже есть
--backend-port25порт backend-серверов по умолчанию (default_port в конфигурации)
--listen-port25порт, на котором балансировщик принимает почту
--hostname FQDNне меняетсяимя сервера: hostname ОС, имя в приветствии 220 и в EHLO к backend-серверам
--dir ПУТЬ/opt/spzgate-balancerкаталог установки
--log ФАЙЛ/var/log/spzgate-balancer-install.logжурнал самой установки
--log-keep ДНИ14сколько суток хранить журналы балансировщика (ротация ежедневная, архивы сжимаются)
--pkg URL|ФАЙЛhttps://spamorez.ru/spzgate/balancer/spzgate-balancer.tar.gzпакет балансировщика: адрес или локальный файл. Контрольная сумма берётся из файла .sha256 рядом с пакетом

Пример с нестандартными портами и локальным пакетом:

sudo bash install.sh --balancer --servers mx1.example.com,mx2.example.com \
  --backend-port 2525 --listen-port 25 --hostname bl.example.com \
  --log /home/admin/balancer-install.log --log-keep 30 --pkg ./spzgate-balancer.tar.gz

Настройка балансировки backend-серверов

Вся настройка маршрутов — в файле conf/smtp_balancer.conf. Это текстовый файл с разделами в квадратных скобках, строками ключ = значение и комментариями, начинающимися с #. Файл читается при запуске контейнера, поэтому после правок выполните:

docker restart spz-smtp-balancer

Результат разбора виден в docker logs spz-smtp-balancer: итоговая строка routing loaded: … default_backends=N source_routes=N destination_routes=N и, если есть ошибки, сообщения с номером строки (smtp_balancer.conf:64: cannot parse line (line skipped)). Некорректная строка пропускается, остальные действуют.

Серверы по умолчанию и общие параметры задаются в разделе [routing]. Именно эти серверы получают сессии, для которых не нашлось ни одного статического маршрута:

[routing]
order = destination, source, default
mode = round_robin
default_port = 25
connect_timeout = 5
health_check_interval = 60
backend = 10.0.0.11 priority=1 weight=1
backend = 10.0.0.12 priority=1 weight=1
КлючПо умолчаниюОписание
orderdestination, source, defaultпорядок проверки источников маршрута: по получателю, по отправителю, серверы по умолчанию
moderound_robinround_robin — сессии поочерёдно распределяются внутри группы одного приоритета с учётом весов; failover — всегда первый сервер группы, следующий только при его недоступности
default_port25порт для серверов, у которых порт не указан
connect_timeout5время в секундах на подключение и SMTP-диалог до RCPT TO; если сервер не уложился, он считается недоступным и берётся следующий
health_check_interval60период активной проверки всех серверов в секундах; 0 — отключить
backend—сервер по умолчанию; одна строка — один сервер, строк может быть сколько угодно

Формат описания сервера (одинаков в [routing], [source] и [destination]):

адрес[:порт] [priority=N] [weight=N]

Равномерное распределение

Все серверы с одинаковыми приоритетом и весом получают сессии по кругу:

backend = 10.0.0.11 priority=1 weight=1
backend = 10.0.0.12 priority=1 weight=1
backend = 10.0.0.13 priority=1 weight=1

Распределение по весам

Более мощному серверу можно отдать большую долю. Ниже первый сервер получает две сессии из трёх:

backend = 10.0.0.11 priority=1 weight=2
backend = 10.0.0.12 priority=1 weight=1

Основные и резервные серверы

Резервный сервер получает почту, только когда недоступны все основные:

backend = 10.0.0.11 priority=1
backend = 10.0.0.12 priority=1
backend = 10.0.0.99 priority=2

Что происходит при недоступности сервера

Если сервер не принял соединение, не ответил приветствием или EHLO за connect_timeout секунд, сессия переключается на следующий сервер списка — клиент этого не замечает. Каждое переключение записывается в журналы (см. журналы) с причиной: отказ в соединении, сеть или хост недоступны, таймаут и т. д.

Кроме реальных сессий, каждые health_check_interval секунд все серверы всех маршрутов проверяются активно: подключение, приветствие и EHLO (если сервер не знает EHLO — HELO), после первого же ответа балансировщик отправляет QUIT и письмо не передаёт. Результат обновляет статус серверов (spzctl status routes) и журнал ошибок; порядок обхода серверов в новых сессиях он не меняет — недоступный сервер пропускается за время не более connect_timeout.

Статические маршруты

Статические маршруты направляют сессии на выбранные серверы по адресу отправителя или получателя. Каждый маршрут — одна строка вида:

route = ШАБЛОН -> сервер1 [priority=N] [weight=N]; сервер2 …

Слева от -> шаблон адреса, справа — один или несколько серверов через ; (формат тот же, что для серверов по умолчанию, включая приоритеты, веса и балансировку по кругу).

Шаблоны, регистр букв не учитывается:

ШаблонЧто совпадает
user@example.comточный адрес
@example.comлюбой адрес домена example.com
*.example.comлюбой поддомен (a.example.com, x.y.example.com), но не сам example.com
*любой адрес, включая пустой адрес отправителя <> (отчёты о недоставке)

Как выбирается маршрут:

  1. Источники проверяются в порядке, заданном order в [routing] (по умолчанию: получатель, отправитель, серверы по умолчанию).
  2. Внутри раздела [source] или [destination] побеждает первый подошедший маршрут — конкретные шаблоны ставьте выше общих.
  3. Если ни один маршрут не подошёл, используются серверы по умолчанию из [routing].

Маршруты по отправителю — раздел [source]

Маршрут выбирается по адресу из команды MAIL FROM (отправитель в конверте). Подходит, когда почту определённых отправителей нужно обрабатывать на выделенных серверах: партнёрский домен, сервис рассылок, внутренние системы, отчёты о недоставке.

[source]
# все письма партнёра — на два сервера по кругу
route = @partner.example -> 10.0.1.10; 10.0.1.11
# письма одного адреса — на отдельный порт
route = billing@example.com -> 10.0.1.20:2525
# все поддомены .net — основной и резервный сервер
route = *.net -> 10.0.1.30 priority=1; 10.0.1.31 priority=2
Отчёты о недоставке приходят с пустым отправителем <>; перехватить их можно только шаблоном *. Маршрут * в разделе [source] захватит всю почту, если до него не сработал маршрут по получателю, — используйте его осознанно.

Маршруты по получателю — раздел [destination]

Маршрут выбирается по получателю из первой команды RCPT TO. Основной сценарий — домены, почта которых обрабатывается разными узлами: один адрес приёма для внешнего мира, а внутри — каждому домену свои серверы.

[destination]
# домен example.com: два основных сервера (первый вдвое мощнее) и резервный
route = @example.com -> 10.0.2.10 priority=1 weight=2; 10.0.2.11 priority=1 weight=1; 10.0.2.20 priority=2
# все поддомены example.org — один сервер
route = *.example.org -> 10.0.2.30
# отдельный ящик — на выделенный сервер (расположите выше общего маршрута домена)
route = ceo@example.com -> 10.0.2.40
Маршрут выбирается по первому получателю сессии. Если один домен нужно разделить по ящикам, ставьте точные адреса выше общего шаблона домена; получатели других доменов в той же сессии получают 450 и приходят отдельной сессией.

Чтобы убедиться, что маршрут сработал, посмотрите журнал: для каждой сессии записывается событие route с разделом, шаблоном и порядком серверов:

route section=destination pattern=@example.com backends=10.0.2.10:25,10.0.2.11:25,10.0.2.20:25

Журналы и статус

Журналы лежат в каталоге logs/ установки. Ротация ежедневная (или при размере больше 200 МБ), срок хранения задаётся параметром --log-keep.

ФайлСодержимое
connections.logвсе подключения: команды клиента целиком (кроме тела письма), выбранный маршрут, попытки подключения к серверам, ответы серверов, переключения. При недоступности сервера пишется строка error: Server адрес: причина и событие failover
backend-error.logошибки подключения к backend-серверам: маршрут, сервер, стадия, причина (refused, unreachable, timeout, reset, smtp_protocol…), затраченное время
backend-status.logтекущий статус каждого маршрута и каждого сервера; перезаписывается при каждом новом подключении и после каждой проверки

Быстрый просмотр статуса:

spzctl status routes
spzctl status backends

Первая команда показывает по строке на маршрут со статусом UP, DEGRADED (часть серверов недоступна), DOWN (недоступны все) или UNKNOWN (серверы ещё не проверялись), вторая — полный файл статуса с временем последней проверки, счётчиками и текстом последней ошибки.

Частые вопросы

ПроблемаЧто проверить
Backend отвечает 550 Invalid HELO … not resolvedимя из --hostname должно резолвиться в IP балансировщика; задайте имя с A-записью и перезапустите установщик с этим параметром
Порт 25 занятостановите другой почтовый сервер на этой машине или установите балансировщик с другим --listen-port
На backend-сервере не виден реальный IP клиентавключён ли control/xclient на этом сервере
Почта идёт не на тот серверсобытие route в connections.log: какой раздел и шаблон сработали; порядок строк (первый подошедший побеждает) и значение order
Изменения в smtp_balancer.conf не действуютвыполните docker restart spz-smtp-balancer; ошибки разбора — в docker logs spz-smtp-balancer
Сервер в статусе DOWNпричина в backend-error.log: refused — на порту никто не слушает, unreachable — нет маршрута до хоста, timeout — пакеты не проходят (файрвол)