Балансировка Dovecot через director: affinity вместо round-robin для IMAP, POP3 и LMTP
Балансировка почтового кластера — это не про равномерность нагрузки, а про то, чтобы один и тот же пользователь всегда попадал на один и тот же backend. Разбираем, почему обычный round-robin через haproxy ломает индексы Dovecot на общем хранилище, как director держит консистентный хэш и таблицу affinity по кольцу, почему LMTP обязан идти тем же путём, и как сливать ноду на обслуживание без оборванных сессий.
EvilMail Team28 июля 2026 г.14 мин чтения
Три backend'а Dovecot смотрят в один mdbox на CephFS, перед ними haproxy раздаёт IMAP по round-robin. Пользователь открыт одновременно с телефона и с десктопа — два соединения улетают на разные ноды. В логах появляется Corrupted index cache record, dovecot.index.log пухнет, доставка через LMTP начинает подвисать на блокировках. Вы добавляете четвёртую ноду, чтобы «размазать нагрузку», и становится только хуже.
Проблема не в нагрузке. Два процесса Dovecot одновременно пишут в индексы одного почтового ящика, а этого делать нельзя ни при каких обстоятельствах. Нужен не равномерный round-robin, а sticky-роутинг: один пользователь — всегда один backend. Ровно эту задачу решает Dovecot director.
Почему round-robin ломает Dovecot, а affinity — нет
Для каждого mailbox Dovecot держит служебные файлы: dovecot.index (карта сообщений и флагов), dovecot.index.log (журнал транзакций) и dovecot.index.cache (кэш заголовков и тел для быстрого IMAP FETCH). Они рассчитаны на то, что их меняет ровно один процесс в единицу времени
Dovecot director: балансировка IMAP/POP3 с сохранением affinity | evilmail.pro — EvilMail Blog
. Внутренние блокировки Dovecot (fcntl/flock) надёжны в пределах одной машины, но на сетевой ФС — NFS, GlusterFS, CephFS — блокировки либо ненадёжны, либо дороги, либо просто не видны второму хосту вовремя. Как только два backend'а начинают писать в один
dovecot.index.log
, вы получаете гонки, повреждённый журнал и знаменитое
Broken file dovecot.index.log
.
Есть и вторая причина, менее очевидная. Даже без явного повреждения скачки пользователя между нодами обесценивают тёплый кэш. На ноде A уже прогрет mmap индекса и cache-файл, FETCH отдаётся из памяти. Следующее соединение уходит на ноду B, где кэш холодный: она перечитывает всё с хранилища, дописывает свою версию cache, а потом A видит, что файл под ней изменился, и инвалидирует свой. Пинг-понг кэшей превращает быстрый кластер в медленный, даже если формально ничего не «сломалось».
Отсюда вывод: маршрутизировать нужно по пользователю, а не по IP-адресу или TCP-соединению. haproxy с balance source даёт sticky только по клиентскому IP — а мобильный клиент и десктоп сидят за разными адресами, да и NAT всё путает. nginx mail proxy и старый Perdition умеют проксировать почтовые протоколы, но у них нет общей между инстансами таблицы «пользователь → backend» с TTL: каждый прокси решает сам за себя, и два прокси легко отправят одного юзера на две разные ноды. Director решает именно это — держит единую реплицируемую таблицу affinity.
Как director держит кольцо и таблицу affinity
Директор — это тонкий проксирующий слой перед backend'ами. Сам почту не хранит, mailbox не открывает. Его работа: по имени пользователя вычислить целевой backend и проксировать туда аутентифицированное соединение.
Маппинг строится на консистентном хэшировании. Каждый backend получает vhost_count виртуальных узлов (по умолчанию 100), которые раскладываются по хэш-кольцу; имя пользователя хэшируется в ту же окружность и попадает на ближайший vhost. Вес ноды — это число её vhost'ов: мощной ноде можно выдать больше (doveadm director add 10.0.0.21 150), и она заберёт в полтора раза больше пользователей. Ключевой параметр — director_username_hash = %Lu: хэшируется полное имя в нижнем регистре, поэтому [email protected] и [email protected] гарантированно сядут на один backend.
Результат каждого маппинга кладётся в user-table (user → host с TTL director_user_expire, по умолчанию 15 минут) и реплицируется между всеми директорами по TCP-кольцу на порту 9090. Любой директор ответит одинаково, потому что таблица общая. И, в отличие от mod N, при добавлении или удалении ноды переезжает лишь около 1/N пользователей — остальные остаются на своих backend'ах с тёплым кэшем.
Конфигурация: директоры и бэкенды
На директорах правим /etc/dovecot/conf.d/10-director.conf. Тот же самый файл, с идентичным списком director_servers, кладётся на каждый директор — узел обязан видеть себя в этом списке, иначе кольцо не сойдётся.
service director {
unix_listener login/director { mode = 0666 }
fifo_listener login/proxy-notify { mode = 0666 }
unix_listener director-userdb { mode = 0600 }
inet_listener { port = 9090 }
}
service imap-login { executable = imap-login director }
service pop3-login { executable = pop3-login director }
protocol lmtp { auth_socket_path = director-userdb }
director_servers = 203.0.113.10 203.0.113.11
director_mail_servers = 10.0.0.21 10.0.0.22 10.0.0.23
director_user_expire = 15 min
director_username_hash = %Lu
director_servers — публичные адреса всех директоров, по ним они находят друг друга на :9090. director_mail_servers — внутренние IP backend'ов. Здесь задаётся только состав кластера; разный вес между нодами выставляется не в конфиге, а через doveadm director add 10.0.0.21 150 — мощная нода получит 150 vhost'ов вместо дефолтных 100. Перевод imap-login/pop3-login в режим director заставляет login-процесс спрашивать целевой хост у директора вместо локального userdb-lookup.
На стороне backend'ов нужно разрешить проксированную аутентификацию: директор уже проверил пароль, backend должен принять proxy-login и не спрашивать пароль повторно. Обычно это passdb с master-пользователем (master { pass = yes }) либо статический passdb с флагом proxy, доверяющий сети директоров. Директор делает passdb-lookup, добавляет host и port целевого backend'а и передаёт соединение login-процессу.
Файрвол: порт 9090 открывается только между директорами. Если кто-то снаружи достучится до кольца, он сможет вмешаться в таблицу affinity. Между директорами и backend'ами открыты обычные 143/993/110/995 и LMTP.
LMTP тоже обязан идти через director
Это подводный камень, на котором спотыкаются чаще всего. Вы аккуратно завели IMAP и POP3 через director, проверили affinity, всё чисто. А входящую почту MTA (Postfix) продолжает доставлять по LMTP напрямую на случайный backend — например, через свой round-robin по списку транспортов. Доставка снова открывает индексы ящика на «чужой» ноде, пока пользователь читает почту на «правильной». Всё, что вы починили на чтении, ломается на записи.
Строка protocol lmtp { auth_socket_path = director-userdb } заставляет LMTP-процесс спрашивать backend у директора через тот же userdb-сокет, что и login-процессы. Postfix при этом должен слать LMTP не напрямую на backend'ы, а на директоры (или на их VIP) — тогда директор проксирует доставку на тот же backend, где сидит IMAP-сессия пользователя. То же правило действует для managesieve: если пользователи правят Sieve-фильтры, ManageSieve тоже проксируйте через director, иначе получите гонку по .dovecot.sieve и скомпилированным .svbin.
Эксплуатация: doveadm director на каждый день
Весь оперативный контроль — через doveadm director:
bash
doveadm director status # кольцо целиком: backend'ы, vhost, users
doveadm director status [email protected] # на каком backend юзер и когда истечёт запись
doveadm director map # полная таблица user → host
doveadm director ring status # состояние кольца: connected / handshaking
doveadm director add 10.0.0.24 # ввести новый backend в ротацию
doveadm director remove 10.0.0.23 # вывести backend
doveadm director move [email protected] 10.0.0.22 # принудительно переселить юзера
doveadm director flush # сбросить таблицу (переразложит всех)
doveadm director kick [email protected] # разлогинить пользователя
ring status смотрят первым при любой странности: все директоры должны быть connected. Застрявший handshaking или расхождение таблиц между узлами — это split-ring, почти всегда из-за заблокированного 9090.
Правильный вывод ноды на обслуживание — это не про «выключить сервер». Вы делаете doveadm director remove 10.0.0.23 (или выставляете ноде vhost 0), после чего новые пользователи на неё больше не садятся. Дальше ждёте, пока активные сессии разойдутся по мере расхода записей director_user_expire — обычно 15–30 минут — и только потом гасите Dovecot на backend'е. Hard-shutdown под нагрузкой оборвёт живые IMAP IDLE-сессии и вызовет всплеск переподключений. Для массовых переездов есть ограничители director_max_parallel_moves и director_max_parallel_kicks (по 100), чтобы вывод узла не превратился в шторм.
HA самих директоров
Директоры делят одно кольцо и общую таблицу, поэтому любой из них даст консистентный ответ. Значит, перед ними можно поставить VIP на keepalived плюс haproxy или простой DNS round-robin — клиенту всё равно, к какому директору подключиться. Держите минимум два директора: один узел кольца — это не кольцо, а точка отказа. Health-check на балансировщике вешайте на login-порт (143/993), а не на 9090: кольцо может отвечать, пока сервис проксирования уже деградировал.
Мониторинг и типичные сбои
Что держать на дашборде и в алертах:
`doveadm director ring status` — все узлы connected. Появление handshaking дольше пары секунд или расхождение состава кольца между директорами = split-ring, чините 9090.
Строки лога `director: Host 10.0.0.23 is down` — директор потерял backend по health-check. Если хост живой, а строка есть — смотрите сеть и disable_when_needed.
`Corrupted index cache record` / `Broken file dovecot.index.log` на backend'ах — прямой симптом нарушенной affinity: один ящик открыли с двух нод. Проверяйте, не течёт ли LMTP мимо директора.
Перекос по vhost — если один backend несёт заметно больше пользователей, чем его доля vhost, пересмотрите vhost_count/веса.
Быстрая диагностика «почему юзер не на том backend'е»: doveadm director status user@domain покажет текущий host и expiry; если он «не тот», ищите, кто ходит в обход кольца — обычно это прямой LMTP или клиент, минующий VIP.
Чеклист внедрения
Минимум два директора; director_servers идентичен на всех и включает сам узел.
director_username_hash = %Lu — иначе регистр имени ломает affinity.
imap-login и pop3-login переведены в режим director.
LMTP идёт через auth_socket_path = director-userdb, Postfix шлёт на директоры, не на backend'ы. ManageSieve — туда же.
Backend'ы принимают proxy-аутентификацию от сети директоров.
Порт 9090 закрыт файрволом для всех, кроме директоров.
VIP/health-check на login-порт (993/143), не на 9090.
Регламент вывода ноды: director remove → ждать расхода сессий → shutdown. Никакого hard-kill под нагрузкой.
Версии: director в 2.3 против 2.4
Всё описанное — стандарт для Dovecot 2.2/2.3.x (последняя в ветке — 2.3.21) и в 2026 году покрывает подавляющее большинство продакшенов, включая наш кластер на evilmail.pro. В Dovecot 2.4 (релиз 2024, уже под крылом OX) слой проксирования и балансировки существенно переработан — часть директив и сама модель director изменились. Если вы поднимаете кластер на свежей 2.4, не копируйте 10-director.conf из статей по 2.3 дословно: сверьтесь с официальным upgrade-гайдом и документацией по новой модели proxy. Логика affinity никуда не делась — пользователь по-прежнему обязан всегда попадать на один backend, — но конкретный синтаксис берите под свою версию, а не по памяти.