Maildir против mdbox в Dovecot: производительность и миграция тысяч ящиков
Maildir не устарел — он просто перестаёт масштабироваться там, где вы этого не ждёте: не в байтах, а в inode и метаданных. Разбираем механику обоих форматов по syscall и IOPS и даём рабочий рецепт миграции на mdbox через dsync почти без простоя, который прогоняется на тысячах ящиков параллельно.
EvilMail Team11 июля 2026 г.14 мин чтения
Где Maildir упирается в потолок
На одном ящике Maildir хорош ровно потому, что «одно письмо — один файл». Атомарный rename() из tmp/ в new/, никаких блокировок, доставка не спорит с чтением, повредить можно максимум одно письмо. За это его и любят двадцать лет. Проблема начинается не с формата, а с масштаба.
Возьмём типичный прод: 8000 ящиков, в среднем по 12 тысяч писем. Это ~96 миллионов файлов, и каждый — отдельный inode.
bash
find /var/mail/vhosts -type f | wc -l # ≈ число писем во всей системе
df -i /var/mail/vhosts # утилизация inode, а НЕ байтов
На практике df -h покажет 40% занятого места, а df -i
— 85% inode. Кончится раньше именно второе, и когда кончится, доставка встанет с
No space left on device
на разделе, где терабайты свободны.
Второй удар — метаданные. IMAP-клиент делает SELECT большой папки, у которой не прогрет dovecot.index (перезагрузка сервера, сброшенный page cache, редко открываемый ящик) — и Dovecot идёт stat()-ить тысячи файлов подряд, чтобы восстановить флаги и порядок. Это не про пропускную способность диска, это про IOPS на мелких операциях с метаданными. На NVMe вы это переживёте, на сетевом хранилище или переподписанном SSD — почувствуете как секундные подвисания на открытии Inbox.
Третий — бэкап. Ночной rsync или снапшот обходит дерево и stat-ит каждый из 96 миллионов файлов, чтобы понять, что изменилось. Само копирование дельты — минуты, обход дерева — часы. Окно бэкапа растёт линейно от числа писем, а не от их объёма.
mdbox бьёт ровно в эти три точки. Но у него есть цена, о которой в туториалах молчат, и к ней мы вернёмся. Сначала — как оба формата лежат на диске.
Как оба формата лежат на диске
Maildir — это дерево каталогов. У каждого ящика cur/, new/, tmp/. Флаги письма закодированы прямо в имени файла: суффикс :2,S — прочитано (Seen), :2,RS — Replied+Seen. Рядом Dovecot держит dovecot.index и dovecot.index.cache для быстрого доступа, но источником истины остаётся файловая система.
mdbox переворачивает модель. Письма упаковываются в крупные storage-файлы m.1, m.2, m.3 по несколько мегабайт каждый. Метаданные и структура папок живут отдельно в mailboxes/, а глобальная нумерация писем — в dovecot.map.index. Индекс здесь не ускоритель, а полноправный источник истины: без него storage — просто склеенные байты.
Помимо меньшего числа файлов, mdbox умеет single-instance storage: при включённом mail_attachment_dir одинаковое вложение, разосланное сотне пользователей, физически хранится один раз, а не сто. Но главный выигрыш проще — на порядок-два меньше файлов, а значит меньше inode и меньше метаданных, которые надо обходить.
Что реально меняется в производительности
Механика, а не маркетинг. Открытие большой папки в Maildir с холодным кэшем — это сотни и тысячи open()/stat() на мелкие файлы. В mdbox то же открытие — это чтение нескольких m.N крупными последовательными блоками плюс один индекс. Число syscall на операцию падает драматически, а последовательное чтение диску даётся легче случайного.
Где выигрыш заметен на глаз:
Большие папки. Ящики-архивы на десятки тысяч писем открываются с прогретым индексом мгновенно, а не «думают» секунду на каждый SELECT.
Пересчёт квоты.doveadm quota recalc по Maildir складывает размеры тысяч файлов; по mdbox читает размеры из индекса.
Бэкап и снапшоты. Обход дерева перестаёт быть узким местом — файлов на несколько порядков меньше, окно бэкапа схлопывается.
Отдельно про NFS и Dovecot director. Maildir прощает распределённый доступ: атомарный rename() при доставке делает конкурентную запись безопасной даже с нескольких хостов. mdbox так не умеет — у storage-файлов и dovecot.map.index должен быть один писатель. На кластере это означает Dovecot director (или proxy с consistent hashing в 2.4), который прибивает все сессии одного пользователя к одному backend-у. Если у вас NFS и вы не готовы поднимать director — это серьёзный аргумент остаться на Maildir.
Обратная сторона mdbox, о которой молчат
Формат dbox не удаляет данные сразу при expunge. Когда пользователь стирает письмо, оно помечается удалённым в индексе, но байты остаются в m.N до тех пор, пока вы явно не запустите уборку. Без регулярного doveadm purge storage будет пухнуть, а квота — расходиться с реальным размером на диске. Это не баг, это дизайн: purge переупаковывает живые письма в новые файлы и выкидывает мёртвые.
«Радиус поражения» тоже меняется. Битый сектор или обрыв записи в Maildir уносит одно письмо — остальные лежат отдельными файлами и целы. Битый m.N в mdbox уносит все письма, упакованные в этот файл — десятки, а то и сотни. Чинится это force-resync, который пересобирает индекс по storage, но письма из повреждённого куска вы не вернёте. Отсюда прямое следствие: под mdbox нужен нормальный RAID/ZFS с контролем целостности и честный бэкап, а не «ну оно же на диске лежит».
И бытовое: mdbox не поковыряешь руками. Нет grep -r по каталогу писем, нет «скопировать один .eml», нет привычного find. Всё через doveadm. Для команды, которая привыкла лечить почту напильником прямо в файловой системе, это смена привычек.
Вывод трезвый: mdbox — это не «лучше», это «быстрее в обмен на дисциплину обслуживания». Готовы держать cron с purge, мониторить целостность и понимать force-resync — берите. Не готовы — Maildir честно доработает ещё долго.
Подготовка к миграции
Сначала версия Dovecot — от неё зависит синтаксис конфига.
bash
dovecot --version
В 2.3 хранилище задаётся одной строкой:
mail_location = maildir:/var/mail/vhosts/%d/%n/Maildir # было
mail_location = mdbox:/var/mail/vhosts/%d/%n/mdbox # станет
В 2.4 (актуально на 2026, mail_location объявлен устаревшим) это два параметра:
%d — домен, %n — имя пользователя; doveadm подставляет их из -u, так что один шаблон работает на все ящики.
Дальше — чек-лист pre-flight, который стоит пройти до того, как трогать прод:
Свежий бэкап индексов и текущего Maildir. Миграция читает источник, но правило «сначала бэкап» не отменяется.
Запас места ~2x на пик. Во время миграции живут оба хранилища — исходный Maildir и новый mdbox. Проверьте df -h, иначе упрётесь в середине прогона.
Тест на одном ящике с проверкой счётчиков (см. ниже). Не начинайте массовый прогон, пока один ящик не сошёлся по messages и vsize.
Понимание направления.doveadm backup — односторонний: он делает приёмник точным зеркалом источника и удаляет в приёмнике всё, чего нет в источнике. Приёмный mdbox обязан быть пустым. Нужна двусторонняя синхронизация — это sync, но для миграции нужен именно backup.
mail_fsync=never на время миграции отключает лишние fsync — вы копируете во временное хранилище, надёжность отдельного письма здесь неважна, а скорость вырастает заметно.
Массовый прогон на тысячах ящиков. doveadm user '*' перечисляет всех, xargs -P8 гонит по 8 параллельно, шаблон %d/%n подставляется автоматически, ошибки уходят в лог, а не теряются:
Степень параллелизма подбирайте по диску: на NVMe спокойно -P8…-P16, на сетевом хранилище начните с -P4 и смотрите на iowait. После первого, «онлайнового» прогона:
1.Блокируем вход — nologin/deny в userdb, чтобы во время финальной дельты никто не писал в старый Maildir.
2.Инкрементальный backup — та же команда повторно. dsync копирует только то, что появилось с первого прогона; на ящик уходят секунды.
3.Правим конфиг — mail_location (2.3) или mail_driver+mail_path (2.4).
4.`doveadm reload` — без рестарта демона, существующие сессии не рвём жёстко.
Единственный реальный простой — этапы 2–4, и он измеряется секундами на ящик, а не часами на всю систему.
После переключения: обслуживание mdbox
Миграция закончилась там, где начинается рутина. Первое и обязательное — cron на уборку, иначе storage будет расти на удалённых письмах:
Старый Maildir не удаляйте сразу. Переведите его в read-only и подержите ~7 дней как страховку — если всплывёт расхождение, у вас есть куда откатиться. Убедились, что почта ходит и счётчики сошлись — тогда rm -rf.