Планирование ёмкости почтового сервера: письма/сек, IOPS и память под Dovecot и rspamd
Почтовый сервер убивает не число ящиков, а пиковая секунда доставки. Разбираем, как честно посчитать письма/сек из логов, вывести требуемые sync-write IOPS и параллелизм rspamd, и только потом закладывать ядра, RAM и класс NVMe.
EvilMail Team27 июля 2026 г.12 мин чтения
Почему счёт «по числу ящиков» врёт
Хостер даёт калькулятор: 10 000 ящиков — берите вот такой тариф. Вы берёте, запускаете, а в 09:20 утра сервер лежит с iowait под 40%, LMTP таймаутит, Postfix копит очередь, и вы в SSH пытаетесь понять, почему «маленький» сервер не тянет то, что по бумаге тянуть обязан.
Проблема в метрике. Ящик, лежащий на диске, почти ничего не стоит: пара килобайт индекса и место под письма. Сервер убивает не хранение, а пиковая секунда доставки — когда на LMTP валится шторм fsync-ов, а rspamd параллельно сканирует десятки писем. И этот пик почти не связан с числом ящиков напрямую.
Разведём три величины, которые все путают:
Суточный объём — сколько писем в сутки. Красивое число для презентации, бесполезное для железа.
Средний msg/s — суточный объём делённый на 86 400. Тоже врёт: почта не размазана ровно по суткам.
Пиковый msg/s — сколько писем приходит в самую загруженную секунду. Вот под это и планируют.
Нагрузка сконцентрирована. На busiest hour приходится 10–15% суточного объёма, а внутри часа есть минутные всплески от корпоративных рассылок, утреннего прилива и ретраев. Средний RPS может быть 6 писем/сек, а пиковая минута — 80. Планируем под 80, а не под 6.
Считаем письма в секунду честно
Формула, от которой отталкиваюсь на старте, пока нет своих логов:
Из практики: hour_peak_factor ≈ 0.12–0.18 (доля суточного объёма на busiest hour), burst_multiplier ≈ 3–5 (насколько минутный всплеск выше среднего по этому часу). Для 500k писем/сутки при факторе 0.15 и мультипликаторе 4:
(500000 × 0.15 / 3600) × 4 = 20.8 × 4 ≈ 83 msg/s
Это оценка на салфетке. Как только сервер поработал хотя бы неделю, гадать больше не нужно — реальный пик достаётся из логов Postfix. Не средний, а именно поминутный максимум:
pflogsumm /var/log/mail.log даст сводку по часам, но для capacity planning важнее минутная гранулярность — именно минутный залп переполняет очередь LMTP, а не ровный часовой поток.
Отдельная статья расходов — ретрай-шторм. Упал ваш сервер или входящий канал на час — очереди соседних MTA никуда не делись, они накопились и ударят залпом, когда вы поднимитесь. Плюс сам Postfix отдавал 4xx, и теперь всё это возвращается на повторную доставку. Закладываю сверху ещё ×2 к расчётному пику: сервер должен переваривать не только штатный час пик, но и разгребание очереди после инцидента, иначе после каждого чиха вы входите в спираль таймаутов.
Для temp-mail вроде evilmail есть свой нюанс, которого нет у «обычной» корпоративной почты: короткоживущие ящики дают высокий create/expunge rate. Это не только доставка — это постоянное создание почтовых каталогов, запись индексов и массовое удаление с последующей чисткой. Файловую систему и индексы это нагружает порой сильнее, чем сама входящая доставка, и в расчёт IOPS это надо закладывать отдельной строкой.
Дисковые IOPS: где реально болит
Первое, что нужно вбить в голову: болит sync-write latency, а не throughput. Ваш стор может спокойно тянуть гигабайты в секунду линейно и при этом захлёбываться на почте, потому что каждая доставка через LMTP делает fsync() — блокирующую, latency-критичную операцию. Пропускная способность тут ни при чём.
Дальше решает формат хранения:
maildir — один файл на письмо, fsync на файл плюс операции с inode и переименование из tmp/ в new/. Простой и надёжный, но на всплесках даёт заметный write amplification и много мелких синхронных операций. На HDD это приговор, на consumer-SSD — боль.
mdbox/sdbox — Dovecot батчит несколько писем в один файл и режет число sync-write в разы. Расплата — обязательное обслуживание: doveadm purge -A по расписанию, иначе удалённые письма не освобождают место.
# Формат хранения в conf.d/10-mail.conf
mail_location = mdbox:~/mdbox
Считаем IOPS на одну входящую доставку честно, со всеми участниками. Записывается не только письмо:
сам mail-файл (write + fsync);
dovecot.index.log (журнал индекса);
dovecot.index.cache (кэш заголовков);
обновление bayes в Redis (если статистический классификатор пишет per-message);
строка в maillog.
Итого 4–8 write-операций на письмо, часть из них синхронные. Умножаем на пиковый msg/s. При 83 msg/s это грубо 350–650 sync-write IOPS в пике, а с temp-mail create/expunge и ретрай-запасом смело закладывайте 500–700.
Измеряется всё это в пике штатными инструментами, без экзотики:
bash
iostat -x 1 # смотрим w/s, w_await, %util на mail-разделе
pidstat -d 1 # какой процесс генерит запись
doveadm stats dump # что делает сам Dovecot
Ключевой вывод про железо. Consumer-NVMe в маркетинговом бенче показывает 100 000 IOPS — на буферизованной записи. Дайте ему O_DSYNC (а именно так пишет почтовый стор), и без конденсатора защиты питания он проваливается в разы, потому что честно ждёт сброса на флеш. Нужен enterprise-класс с PLP (Power Loss Protection): Samsung PM9A3, Intel/Solidigm D7-P5520 и родня. Проверяется до закупки, а не после инцидента:
Смотрите не на «максимум IOPS», а на устойчивый результат при --fsync=1 --iodepth=1. HDD с его ~150 IOPS для активного mail store не рассматриваем вообще — только под холодный архив.
rspamd: память и параллелизм под пик
rspamd — вторая половина пиковой нагрузки, и она CPU-bound. Модель воркеров, которая работает:
normal-воркеры делают сканирование, их count = числу ядер;
отдельный controller (веб-интерфейс, обучение, статистика);
отдельный rspamd_proxy в режиме milter — точка входа от Postfix.
Бюджет времени на скан — 50–200 мс на письмо. Отсюда считается параллелизм: чтобы переварить пик без роста очереди в rspamd_proxy, нужно параллельных сканов не меньше, чем peak_msg_s × scan_time. При 83 msg/s и 150 мс это ≈ 13 одновременных сканов — то есть 12–16 ядер под rspamd в пике, иначе очередь растёт и латентность доставки ползёт вверх.
Память: воркер со скомпилированным hyperscan держит 150–300 МБ RSS. Умножаем на count. Плюс Redis под bayes, ratelimit и fuzzy — его размер растёт с числом токенов. Neural-модуль и крупные regexp-правила отдельно раздувают RSS, за этим надо следить:
bash
rspamadm control stat # аптайм, learned, scanned, среднее время
rspamc stat # статистика классификатора и правил
# scan-time p95 держим < 250 мс — выше значит не хватает ядер
Redis настраиваем осознанно. Для bayes нельзя терять данные при переполнении — noeviction; для ratelimit/fuzzy эвикшн допустим. Держите разные логические БД или инстансы и следите за вытеснением:
bash
redis-cli info stats | grep evicted_keys # для bayes должно быть 0
Dovecot: процессы, индексы и что съедает RAM
Первое и обязательное — high-performance login. По умолчанию Dovecot форкает процесс на каждое соединение, и на пике коннектов это заметный расход. Переключаем imap-login в режим постоянных процессов:
service imap-login {
service_count = 0
process_min_avail = 4
}
default_vsz_limit = 256M — классическая ловушка OOM. На большом ящике с гигабайтами почты и жирными индексами процесс упирается в лимит виртуальной памяти и падает, а пользователь видит обрывы IMAP. Проверьте значение на самом большом реальном ящике, а не на тестовом.
Главный потребитель RAM в Dovecot — вовсе не процессы. Это page cache под индексы. Вы хотите, чтобы горячие dovecot.index активных ящиков жили в оперативке, иначе каждый SEARCH/SORT превращается в random read с диска, и вы снова в i/o-wait. Отсюда правило по RAM:
Средний msg/s: 500000 / 86400 ≈ 6 msg/s (для железа бесполезно).
Пик по формуле: (500000 × 0.15 / 3600) × 4 ≈ 83 msg/s. С ретрай-запасом планируем ~150 msg/s как потолок выживания.
Sync-write IOPS в пике: 83 × (4–8) ≈ 350–650 IOPS, с temp-mail create/expunge округляем вверх до ~700. Для enterprise-NVMe — единицы процентов ресурса. Для HDD — пятикратное переполнение и смерть.
Параллелизм rspamd под 83 msg/s при 150 мс: ~13 одновременных сканов → 12–16 ядер под rspamd.
Итог в железе:
CPU: нагрузка rspamd-bound — 16 ядер, из них ~12–14 под normal-воркеры.
RAM: 32–64 ГБ. Воркеры rspamd (~4–5 ГБ), Redis (задать maxmemory по числу токенов), остальное — под page cache индексов.
Диск: enterprise-NVMe с PLP, mdbox. RAID1/RAID10 из двух-четырёх дисков — не ради IOPS (их с запасом), а ради переживания смерти диска без простоя.
Пороги алертов: w_await на mail-разделе, %util диска, rspamd scan-time p95, глубина очереди Postfix (mailq | tail -1). Алерт должен звонить до того, как очередь начнёт расти, а не после.
Чеклист перед запуском в прод
Пиковый msg/s измерен из логов (awk по maillog с группировкой по минутам), а не взят с потолка.
Выбран mdbox, если профиль IOPS-bound; настроен doveadm purge -A по cron.
NVMe с PLP подтверждён тестом fio --fsync=1 --iodepth=1, а не по строчке в даташите.
imap-login переведён в high-performance (service_count = 0, process_min_avail).
default_vsz_limit проверен на самом большом ящике — нет OOM.
rspamd worker "normal" { count = <ядра> }, отдельные controller и rspamd_proxy; scan p95 < 250 мс под нагрузкой.
Redis: maxmemory и maxmemory-policy заданы; для bayes noeviction, evicted_keys = 0.
Индексы активных ящиков влезают в RAM (page cache не вымывается под нагрузкой).
Алерты на w_await, %util, scan p95 и глубину очереди подняты и проверены.
Прогнан нагрузочный тест на пиковом msg/s до открытия трафика:
Считайте пик, а не ящики. Всё остальное — ядра, память, класс диска — выводится из одного честно измеренного числа: сколько писем приходит в вашу самую тяжёлую минуту.