Maildir проти mdbox у Dovecot: швидкість, бекапи та тиск на inode
Вибір формату зберігання в Dovecot — це не «що краще», а компроміс між тиском на inode/IOPS і ризиком пошкодження одного великого файлу. Розбираємо Maildir і mdbox по операціях, з реальними командами, порогами й планом міграції без простою.
EvilMail Team11 липня 2026 р.12 хв читання
Проблема: коли `df -i` вбиває сервер раніше за `df -h`
Класичний нічний виклик: MTA раптом повертає 452 4.3.1 Insufficient system storage, клієнти ловлять No space left on device, а df -h показує вільних 210 ГБ на розділі з поштою. Диск не заповнений. Заповнена таблиця inode.
Maildir vs mdbox у Dovecot: порівняння форматів зберігання | evilmail.pro — EvilMail Blog
Причина стає очевидною, щойно розкладеш арифметику. ext4 за замовчуванням виділяє один inode на кожні 16384 байти простору (bytes-per-inode=16384). На розділі 500 ГБ це трохи більше ніж 33 млн inode. У Maildir кожен лист — це окремий файл, тобто окремий inode, плюс індексні файли Dovecot. Скринька на 40 тис. листів з'їдає 40 тис. inode. Кілька сотень активних скриньок середнього розміру — і сервер упирається в стелю inode при 55–60% фактично зайнятого місця.
Змінити bytes-per-inode після mkfs не можна — тільки переформатувати. Тож питання не косметичне: або ви заздалегідь ллєте ФС під мільйони дрібних файлів, або міняєте формат зберігання так, щоб один лист перестав коштувати один inode. Саме тут і починається розмова про mdbox.
Як насправді влаштований Maildir на диску
Maildir — це три підкаталоги на кожну папку IMAP: tmp/, new/, cur/. Лист спершу пишеться у tmp/ під унікальним іменем, потім атомарно переноситься через rename() у new/. Коли клієнт його побачив — Dovecot переміщує файл у cur/ і дописує флаги в ім'я. Ніяких блокувань на всю скриньку: rename() у межах однієї ФС атомарний, і два процеси не можуть зіпсувати один одному доставку.
Ім'я файлу несе метадані прямо в собі:
text
1720080000.M12345P6789.mail01,S=2048,W=2080:2,S
└─timestamp─┘ └─unique─┘ └host┘ │ │ └flags
│ └W = розмір з CRLF
└S = розмір у байтах
Флаги після :2, записуються в ASCII-алфавітному порядку: D=Draft, F=Flagged, P=Passed, R=Replied, S=Seen, T=Trashed. Тому RS завжди означає «Replied + Seen», а не навпаки — порядок фіксований самим форматом. Поле S= дає Dovecot розмір листа без stat(), а зміна флага — це просто rename() файлу. Звідси і головна перевага, і головна вада формату: усе прозоро й людиночитабельно, але кожна дрібна операція б'є по метаданих ФС.
Як влаштований mdbox (і чим він не mbox)
mdbox — сучасний формат родини dbox. Багато листів пакуються в один файл storage/m.N, а окремий індекс storage/dovecot.map.index тримає карту «лист → файл + зсув». Це принципово інша річ, ніж старий mbox: немає глобального локу на всю скриньку. Листи дописуються в кінець поточного m.N (append-only), нові приходять паралельно, а видалення — це не переписування файлу, а просто позначка в map-індексі.
Місце реально звільняється окремим кроком doveadm purge, коли частка «мертвих» листів у файлі перевищує поріг. Нарізкою керують два параметри:
mdbox_rotate_size = 2M # default; починаємо новий m.N після цього розміру
mdbox_rotate_interval = 0 # default; ротація за часом вимкнена
Дефолтні 2 МБ — надто дрібно для завантаженого сервера: ви отримуєте забагато файлів m.N і частіший purge. Для скриньок із десятками тисяч листів ставимо mdbox_rotate_size = 10M (іноді до 20M) — це баланс між кількістю inode і обсягом, який доведеться перечитувати під час purge.
Швидкість: IOPS, метадані й де хто програє
Розкладемо по конкретних операціях, бо «швидше/повільніше» без операції — порожнє слово.
Доставка одного листа. Maildir: create у tmp/ + rename у new/ — дві метаоперації. mdbox: один append до відкритого m.N плюс оновлення map-індексу. На NVMe різниця мала; на мережевому сховищі (NFS, Ceph) mdbox помітно легший — менше окремих метаданих-запитів.
Відкриття великої скриньки. Тут провалля. Maildir мусить прочитати вміст каталогу й зробити stat() фактично на кожен файл, якого ще немає в кеші індексу. Скринька 20 тис. листів на HDD після скидання кешу відкривається секундами; на NVMe — сотні мілісекунд, але все одно це тисячі метазапитів. mdbox читає dovecot.map.index і бере листи за зсувом — десятки IOPS замість тисяч.
Пошук і сортування. Обидва спираються на індекси Dovecot, але коли індекс треба перебудувати, mdbox виграє: послідовне читання кількох великих файлів проти random-обходу дерева з тисяч дрібних.
Індекси в обох форматах критично важливо тримати на швидкому носії. Винесення їх на окремий SSD або tmpfs знімає весь random-IOPS від метаданих з основного стореджу:
Чесний контраргумент за Maildir: за дуже конкурентного доступу до однієї скриньки багатьма незалежними процесами (IMAP-сесія плюс локальний скрипт, що читає файли напряму) Maildir не має спільної точки серіалізації, тоді як mdbox упирається в map-індекс. Для класичного mail-хостингу це рідкість, але для нестандартних pipeline — реальний фактор.
Бекапи: чому це вирішальний, а не другорядний фактор
Це та частина, де інтуїція часто підводить.
Maildir + rsync виглядає ідеально: кожен лист — окремий файл, інкрементальний бекап тривіальний, відновлюється поштучно. Пастка в тому, що rsync мусить обійти й stat()кожен файл, щоб вирішити, чи він змінився. Мільйони дрібних файлів перетворюють нічний бекап на багатогодинний обхід дерева, а бекап-сторедж теж упирається у власний ліміт inode.
mdbox + rsync швидко обходить дерево (кілька великих файлів), АЛЕ будь-яка зміна всередині m.N змушує rsync перекачати весь файл цілком, а не один лист. І пошкодження одного m.N тягне за собою всі листи в ньому.
Правильний інструмент для mdbox — не файловий rsync, а dsync/doveadm backup, який працює на рівні листів, а не файлів:
bash
# інкрементальний бекап скриньки у mdbox на бекап-хості
doveadm backup -u user@domain \
mdbox:/backup/vhosts/domain/user/mdbox
doveadm backup синхронізує рівно ті листи й флаги, що змінилися, зберігає структуру IMAP і не залежить від того, як формат розкладає байти по файлах. Для mdbox це різниця між коректним бекапом і регулярним перекачуванням гігабайтів через зміну одного листа.
Надійність і відновлення після збою
Тут Maildir чесно виграє в одному вимірі — гранулярність шкоди. Пошкоджений файл у Maildir означає втрату рівно одного листа, індекс перебудовується без драми:
bash
doveadm force-resync -u user@domain '*'
У mdbox пошкодження m.N або dovecot.map.index зачіпає групу листів у цьому файлі — десятки чи сотні. Dovecot тримає резерв storage/dovecot.map.index.backup, і відновлення так само йде через force-resync, часто по всіх користувачах одразу:
bash
doveadm force-resync -A '*'
Але будьмо відверті: m.N — це бінарний контейнер, його не «розгребеш очима» через less, як окремий .eml-файл Maildir. Якщо вам важлива можливість руками витягти конкретний лист із пошкодженого сховища сторонніми засобами — Maildir дає це безкоштовно, mdbox ні. Для більшості інсталяцій регулярний doveadm backup це перекриває, але рішення має бути свідомим.
Компресія й дедуплікація: козир mdbox
mdbox прозоро дружить із компресією й single-instance зберіганням вкладень, чого Maildir нормально не вміє:
У Maildir zlib_save теж працює на запис, але без прозорого переупакування вже наявних листів і без нормальної дедуплікації вкладень. Це саме те, де dbox-архітектура з окремим attachment-сховищем показує економію 30–50% на типовому потоці з великими PDF та зображеннями.
Міграція Maildir → mdbox без простою
dsync дозволяє перелити скриньку в mdbox, поки Maildir ще обслуговує клієнтів, і зробити фінальний догін уже після перемикання. Ключове правило: не змішуйте два формати в одному namespace наживо і не забувайте про права 5000:5000 у нашій інфраструктурі.
bash
# 1. Первинна конвертація (Maildir читаємо, у mdbox пишемо)
doveadm -o mail_location=mdbox:/var/mail/vhosts/domain/user/mdbox \
backup -u user@domain maildir:/var/mail/vhosts/domain/user/Maildir
# 2. Перемикаємо mail_location у конфізі на mdbox, релоуд:
# mail_location = mdbox:/var/mail/vhosts/%d/%n/mdbox
doveadm reload
# 3. Фінальний догін змін, що прилетіли під час кроку 1
doveadm -o mail_location=mdbox:/var/mail/vhosts/domain/user/mdbox \
backup -u user@domain maildir:/var/mail/vhosts/domain/user/Maildir
# 4. Права + перша збірка місця
chown -R 5000:5000 /var/mail/vhosts/domain/user/mdbox
doveadm purge -u user@domain
Після перемикання дайте Dovecot перебудувати індекси — перше відкриття кожної скриньки буде повільнішим, це нормально. Старий Maildir/ не видаляйте кілька днів, поки не переконаєтеся, що лічильники листів збіглися:
bash
doveadm mailbox status -u user@domain 'messages' '*'
Практичний чеклист вибору
Беріть Maildir, якщо:
потрібна сумісність зі сторонніми файловими інструментами (rsync поштучно, mairix, offlineimap, ручний grep по .eml);
скриньки малі (одиниці тисяч листів) і inode не в дефіциті;
критична поштучна атомарність і людиночитабельне сховище для ручного розбору інцидентів.
Беріть mdbox, якщо:
скриньки на десятки+ тисяч листів і сервер упирається в df -i раніше за df -h;
потрібна прозора компресія zlib і single-instance дедуплікація вкладень;
бекап будується на doveadm backup/dsync, а не на файловому rsync;
багато дрібних листів створюють неприйнятне метаданих-навантаження на ext4/XFS.
П'ять налаштувань, які виставляємо одразу під mdbox:
Підсумкове інженерне рішення для нових інсталяцій на ext4/XFS із багатьма скриньками: mdbox за замовчуванням, Maildir — усвідомлений виняток там, де переважує сумісність зі сторонніми тулзами або потреба у поштучному ручному доступі. І в будь-якому разі — бекап через doveadm backup, а індекси на швидкому диску. Ці два рішення дають більше приросту, ніж сам вибір формату.