Дедупликация в ZFS: почему её почти всегда не стоит включать
Функция дедупликации в ZFS выглядит как подарок: включаете один параметр — и файловая система сама находит повторяющиеся блоки данных, экономя место без всяких усилий. На практике за этим обещанием стоит структура, которая должна почти целиком жить в оперативной памяти, а требования к этой памяти растут не с текущим объёмом данных на диске, а с историческим объёмом всего уникального, что вы когда-либо записали. Если это осознать заранее — решение принимается легко. Если нет — вы узнаёте об этом в момент, когда сервер перестаёт отвечать на запись.
Содержание
- Как ZFS дедуплицирует данные: блоки, хеши и таблица DDT
- Почему DDT должна помещаться в оперативную память
- Что происходит, если памяти не хватает: типичный постмортем
- Сколько RAM реально нужно и как это прикинуть заранее
- Сжатие как разумная и почти всегда более дешёвая альтернатива
- Когда дедупликация всё-таки оправдана
Как ZFS дедуплицирует данные: блоки, хеши и таблица DDT
Первое, что нужно понять: дедупликация в ZFS работает на уровне блоков, а не файлов целиком. Если у вас два одинаковых файла по 10 МБ, ZFS не сравнивает их как объекты — она сравнивает блоки, из которых они состоят (размер блока задаётся параметром recordsize, по умолчанию 128 КБ, но может быть от 4 КБ до 1 МБ в зависимости от датасета).
Когда дедупликация включена для датасета (zfs set dedup=on pool/dataset), при каждой записи нового блока ZFS считает его криптографический хеш (по умолчанию SHA-256, можно указать dedup=sha256,verify или другой алгоритм) и сверяет этот хеш с таблицей дедупликации — DDT (Dedup Table). В DDT хранится запись на каждый уникальный блок, который когда-либо был записан в датасет с включённым dedup: хеш, счётчик ссылок (сколько раз этот блок используется) и физическое расположение блока на диске.
Если хеш нового блока уже есть в таблице — ZFS не пишет данные заново, а просто увеличивает счётчик ссылок и создаёт указатель на существующий блок. Если хеша нет — блок записывается на диск как обычно, и в DDT добавляется новая запись. Логика простая и по-своему элегантная. Проблема не в логике, а в том, что происходит с этой таблицей по мере роста хранилища.
Важный нюанс: включить дедупликацию можно точечно, для конкретного датасета, а не для всего пула целиком:
zfs create tank/vm-images
zfs set dedup=on tank/vm-images
zfs set dedup=off tank/backups
Это не отменяет проблему с памятью (см. ниже), но хотя бы ограничивает её масштаб одним датасетом, а не всем пулом.
Почему DDT должна помещаться в оперативную память
DDT — это структура, к которой обращаются на каждую операцию записи в датасет с включённым dedup, и в меньшей степени — при удалении данных (нужно уменьшать счётчики ссылок). Чтобы такое обращение было быстрым, вся таблица или её подавляющая часть должна находиться в ARC — адаптивном кеше ZFS в оперативной памяти. Если запись DDT нужно каждый раз поднимать с диска — вы получаете произвольное чтение с диска (случайный I/O) на каждую операцию записи, что для механических дисков и даже для многих SSD-сценариев является одним из самых медленных типов доступа к данным.
Ключевая деталь, которая обычно упускается при первом знакомстве с фичей: размер DDT растёт пропорционально общему объёму уникальных блоков, когда-либо записанных в датасет с dedup=on — а не текущему объёму данных на диске. Если вы записали 50 ТБ уникальных данных, а потом удалили 40 ТБ из них, записи в DDT для удалённых блоков освобождаются (счётчик ссылок падает до нуля), но пока эти данные лежали в датасете, таблица успела вырасти до размера, соответствующего пиковому объёму уникальных данных, а не текущему. То есть DDT — это в некотором смысле накопленная история, а не отражение текущего состояния диска.
По разным источникам в сообществе ZFS в качестве грубого ориентира называют цифры порядка 200–320 байт оперативной памяти на одну запись в DDT (точное число зависит от версии OpenZFS, размера блока и внутренних структур метаданных — воспринимайте это как ориентир для прикидки, а не как измеренный бенчмарк). При recordsize=128K и, скажем, 10 ТБ уникальных данных это около 80 миллионов блоков — и уже в районе 16–25 ГБ только на саму таблицу дедупликации, сверх обычного объёма RAM, который ZFS и так резервирует под ARC для нормального кеширования чтения. Если вы уже читали нашу статью про то, сколько оперативной памяти закладывать с запасом на сервере под типовые задачи — дедупликация добавляет к этой цифре отдельный, быстро растущий довесок, который никак не связан с нагрузкой приложений, а зависит только от объёма уникальных данных.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверЧто происходит, если памяти не хватает: типичный постмортем
Вот тут начинается самая болезненная часть истории, и это один из самых частых сценариев, которые всплывают на форумах и в багтрекерах ZFS: «включил дедупликацию, первое время всё было нормально, а через несколько недель или месяцев запись почти встала».
Механика проста. Пока пул почти пустой, DDT маленькая и целиком помещается в ARC — дедупликация работает быстро и незаметно. По мере роста объёма уникальных данных таблица растёт, и в какой-то момент перестаёт помещаться в доступную память целиком. С этого момента часть обращений к DDT начинает уходить на диск. Каждая запись — это теперь потенциальный поиск записи таблицы на медленном носителе, причём поиск случайный, не последовательный. Пропускная способность записи на пул может упасть на порядок и больше, iowait улетает в потолок, простые операции вроде rsync, cp или даже rm начинают выполняться минутами вместо секунд.
Хуже того: осознав проблему, администратор часто пытается просто отключить дедупликацию — zfs set dedup=off pool/dataset — и с удивлением обнаруживает, что легче не становится. Это ожидаемо: dedup=off останавливает дедупликацию новых записей, но все уже записанные ранее дедуплицированные блоки остаются привязаны к записям в DDT, и операции над ними (в первую очередь удаление и работа со снапшотами) по-прежнему требуют обращения к таблице, пока эти блоки не будут удалены или перезаписаны. Реального избавления от проблемы это не даёт — придётся либо ждать, пока данные постепенно вымоются, либо перенести данные (zfs send / zfs receive) в новый датасет без дедупликации, что само по себе требует времени, свободного места и ресурсов ввода-вывода, которых на деградировавшем пуле и так не хватает.
Отдельно стоит сказать про удаление снапшотов и целых датасетов с дедуплицированными данными: операция zfs destroy на таком датасете может потребовать пересчёта счётчиков ссылок для миллионов записей DDT и занимать часы там, где на обычном датасете это была бы секундная операция.
Сколько RAM реально нужно и как это прикинуть заранее
Обычные рекомендации по памяти для ZFS и так не самые скромные — файловая система активно использует ARC для кеширования чтения, и мы разбирали в статье про ECC-память на арендованном сервере, почему для ZFS в принципе желательно иметь ECC RAM: повреждённый в памяти бит на пути к диску ZFS не всегда способна поймать так же надёжно, как повреждение непосредственно на носителе. Дедупликация добавляет к этому отдельное, растущее требование, которое не зависит от того, сколько именно памяти вы готовы выделить под кеш чтения приложений.
Практический ориентир, который встречается в документации и обсуждениях сообщества: планировать по несколько гигабайт оперативной памяти на каждый терабайт уникальных данных, предполагаемых к дедупликации — конкретная цифра сильно зависит от recordsize, версии OpenZFS и реального коэффициента дублирования, поэтому это именно ориентир для грубой прикидки, а не гарантированное число.
Проверить состояние таблицы дедупликации на уже работающем пуле можно командой:
zpool status -D poolname
Она покажет статистику DDT: количество записей, распределение по коэффициентам ссылок, оценку занимаемой памяти. Команда zdb -D poolname даёт более подробную раскладку, но учтите — на большом пуле с большой DDT сама эта команда может выполняться долго и создавать дополнительную нагрузку, поэтому не стоит гонять её бездумно на проде. Общий коэффициент дедупликации по пулу виден в выводе zpool list в столбце DEDUP. Полезно также посматривать в arc_summary — утилиту, показывающую текущий размер ARC и долю попаданий в кеш; резкое падение hit ratio при активной дедупликации — ранний тревожный звоночек, что таблица начинает вытеснять из кеша полезные данные или сама перестаёт помещаться.
Прежде чем включать dedup на проде, разумно протестировать сценарий на копии реальных данных в тестовом окружении — записать сопоставимый объём, замерить фактический размер DDT через zpool status -D, и только после этого закладывать память в боевой конфигурации с запасом.
Сжатие как разумная и почти всегда более дешёвая альтернатива
Если цель — сэкономить место на диске, в подавляющем большинстве случаев обычное сжатие даёт сравнимый или лучший практический эффект при несопоставимо меньшей цене по ресурсам. Встроенное сжатие ZFS работает независимо для каждого блока — оно не требует общей таблицы, растущей вместе с историей всех уникальных данных в датасете, и поэтому не создаёт накопительного требования к памяти, как дедупликация.
zfs set compression=lz4 pool/dataset
# либо, где важнее степень сжатия, чем скорость:
zfs set compression=zstd pool/dataset
zfs set compression=zstd-9 pool/dataset
Алгоритм lz4 в современных версиях ZFS — фактический дефолт не просто так: он достаточно быстрый, чтобы почти не нагружать CPU, а на многих рабочих нагрузках даже ускоряет ввод-вывод — с диска физически считывается меньше данных, чем занимает распакованный блок. zstd даёт более высокую степень сжатия ценой заметно большей нагрузки на CPU, что может быть оправдано для холодных архивных данных и неоправданно для активной базы данных.
Ключевое практическое соображение: реальная доля дублирующихся данных у типичного пользователя (сайты, базы данных, обычные файловые хранилища, бэкапы без специфической структуры) обычно скромная — выигрыш от дедупликации в процентах редко оказывается настолько большим, чтобы оправдать риск и стоимость памяти, которую под неё придётся выделить. Сжатие же почти всегда даёт заметный эффект почти бесплатно.
| Критерий | Сжатие (lz4/zstd) | Дедупликация |
|---|---|---|
| Требования к RAM | Минимальные, не растут с историей данных | Пропорциональны накопленному объёму уникальных данных |
| Нагрузка на CPU | Низкая (lz4) — умеренная (zstd) | Хеширование каждого блока при записи |
| Обратимость | Можно включить/выключить в любой момент без побочных эффектов | Отключение не освобождает память сразу, старые блоки остаются в DDT |
| Типичный выигрыш | Стабильный, предсказуемый | Сильно зависит от реальной доли дублей, часто переоценивается |
| Риск деградации при росте данных | Практически отсутствует | Высокий — известный сценарий обвала производительности записи |
Когда дедупликация всё-таки оправдана
Дедупликация не бесполезна как класс — она оправдана в узком, но реальном наборе сценариев, где доля дублирующихся данных не «скромная», а по-настоящему огромная. Классический пример — хранилище множества почти идентичных образов виртуальных машин или VDI-дисков, где десятки и сотни виртуалок построены на одном и том же базовом образе ОС с минимальными различиями: доля повторяющихся блоков там измеряется не процентами, а кратностями.
В таких сценариях включать дедупликацию стоит только при выполнении двух условий одновременно: объём реальных дублей заведомо огромен (не «наверное сэкономим», а измеримо и предсказуемо), и объём RAM под DDT просчитан и заложен заранее, а не «попробуем и посмотрим, как пойдёт». Это решение, которое принимается на этапе проектирования хранилища, а не тумблер, который включают из любопытства на уже работающем проде.
Стоит также иметь в виду, что в новых версиях OpenZFS появились улучшения механизма дедупликации (в сообществе их иногда называют «fast dedup»), которые снижают часть накладных расходов на обслуживание таблицы — но они не отменяют исходную зависимость размера DDT от объёма уникальных данных, а лишь несколько смягчают её. Если задача — дедупликация конкретно бэкапов, часто разумнее решать её не на уровне ZFS, а на уровне самого инструмента бэкапа: программы вроде Borgbackup или Restic используют дедупликацию на основе контентно-зависимого разбиения на чанки, которая для сценария бэкапов обычно требует существенно меньше памяти, чем ZFS DDT, и не завязана на всю файловую систему целиком.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Дедупликация ZFS работает на уровне файлов или блоков?
На уровне блоков, размер которых задаётся параметром recordsize датасета (обычно 128 КБ по умолчанию). Два одинаковых файла дедуплицируются только если совпадают их блоки побайтово, с учётом выравнивания.
Можно ли включить дедупликацию только для части пула?
Да, параметр dedup задаётся на уровне датасета, а не всего пула — можно включить его для одного датасета (например, с образами VM) и оставить выключенным для остальных.
Если выключить dedup, память сразу освободится?
Нет. dedup=off останавливает дедупликацию новых записей, но уже существующие записи в DDT для ранее записанных блоков остаются, пока эти блоки не будут удалены или перезаписаны — облегчение наступает постепенно, а не мгновенно.
Можно ли заранее прикинуть, сколько RAM понадобится под DDT?
Точно — только протестировав на представительном объёме данных и посмотрев фактический размер таблицы через zpool status -D или zdb -D. Готовые формулы дают лишь грубый ориентир, реальное число зависит от версии ZFS, размера блока и структуры данных.
Чем дедупликация ZFS отличается от дедупликации в инструментах бэкапа вроде Borgbackup или Restic?
Инструменты бэкапа обычно используют контентно-зависимое разбиение на чанки и хранят метаданные дедупликации отдельно от основной файловой системы, что для сценария бэкапов часто требует заметно меньше памяти, чем таблица DDT в ZFS, привязанная ко всему датасету.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →