Дано
PBS с датастором на 1.93 ТБ — страйп двух зеркал из четырёх SSD по терабайту. На него бэкапятся 22 виртуалки: суммарно около двух терабайт виртуальных дисков, внутри которых занято ~1.5. Открываю в PVE свойства PBS-стораджа, вкладка Backup Retention: шесть полей Keep, все пустые, снизу жёлтая плашка. Вопрос выглядит просто — что вписать, чтобы история была подлиннее, а диск не кончился в пятницу вечером. Ответ оказался не про «какие цифры», а про «как считать и где вписывать». Сел, посчитал на живом датасторе, делюсь.Почему «сколько копий влезет» — неправильный вопрос
Первый рефлекс — делить в лоб: диск 2 ТБ, гости весят 500 ГБ, значит, влезут четыре копии. PBS так не работает.PBS режет данные на чанки (~4 МБ), жмёт zstd и хранит каждый уникальный чанк ровно один раз. Снапшот — это индекс со ссылками на чанки. Второй бэкап той же VM стоит не 500 ГБ, а столько, сколько чанков реально изменилось с прошлого раза. За полный объём платишь один раз, дальше — только за дельту. Бонусом однотипные гости (десяток машин на одной Ubuntu) дедуплицируются ещё и между собой: системные чанки общие.
Поэтому правильная постановка: базовый footprint + скорость прироста.
Формула
Код:
Занято ≈ D × c × (1 + Nd×rd + Nw×rw + Nm×rm)
- D — суммарно занятые данные внутри гостей (не размер выделенных дисков);
- c — коэффициент сжатия zstd, для типичной смеси ОС+сервисы ~0.6;
- rd / rw / rm — уникальный churn между соседними daily/weekly/monthly снапшотами; стартовые прикидки — 2% / 5% / 12%;
- Nd / Nw / Nm — сколько таких храним.
Потолки для 1.93 ТБ
Бюджет — 80% ёмкости, ~1.55 ТБ. Забивать PBS под крышку нельзя: GC и очередному бэкапу нужен запас, а при 100% PBS не выселяет старое — новые бэкапы просто падают с ошибкой.| Политика | Снапшотов на VM | Потолок исходных данных D |
|---|---|---|
| 7d + 4w | ~11 | ~1.9 ТБ |
| 7d + 4w + 3m | ~14 | ~1.5 ТБ |
| 7d + 4w + 6m | ~17 | ~1.2 ТБ |
| 14d + 8w + 12m | ~34 | ~0.8 ТБ |
Теперь без коэффициентов: что говорит дашборд
Седьмой день работы, суточные бэкапы, prune ещё не включён:
Код:
Использование: 51.98% (1.00 ТБ из 1.93)
Виртуальные машины: 22 группы, 158 снимков → ~7.2 поколения на VM
Фактор дедупликации: 14.30
Фактор 14.30 при семи поколениях — это не только история: PBS схлопнул одинаковые системные чанки между машинами и воздух в образах. Полезнее другое: терабайт на диске — это уникальная база плюс шесть суточных дельт. Снимаю дельты по модельным 2%: база ≈ 1.00 / 1.12 ≈ 0.89 ТБ.
И вот тут план «7 daily + 4 weekly + 6 monthly» разбился о факты: плато = 0.89 × 2.06 ≈ 1.84 ТБ — 95% сырой ёмкости, на 0.3 выше бюджета. Нынешние комфортные 52% обманчивы: дешёвая часть политики (daily-бакеты) уже набрана, вся дорогая (monthly — 0.72 из 1.06 надбавки) целиком впереди. Поэтому стартую с 7/4/3: плато ≈ 1.51 ТБ, ровно по кромке бюджета. Monthly — единственная ручка, как её крутить дальше — ниже, в динамике.
Ловушка, которая уже была настроена
На датасторе обнаружился prune job с невинной комбинацией: last=3, daily=3, weekly=1, monthly=1. Выглядит как «три дня + неделя + месяц». Работает как «три дня, точка».Механика по живым датам (в сторе снимки за 5–11 июля). last=3 забирает 11, 10 и 9 июля. daily=3 смотрит на три самых свежих календарных дня — 11, 10, 9 — их бакеты уже заняты теми же снимками, ноль добавлений. weekly=1 — один бакет, текущая ISO-неделя, её новейший снимок уже сохранён. monthly=1 — бакет «июль», та же история. Итог прогона: три снимка остаются, четыре под нож, и глубже трёх дней истории не будет никогда. Единица в weekly и monthly при живом daily всегда декоративна — она дублирует бакет новейшего снимка. Счётчики keep-* не суммируются в лоб.
Настройка на стороне PBS
Retention живёт здесь, и только здесь. Datastore → Prune & GC → Prune Job:
Код:
Пространство имён: Root, глубина — полная
Расписание: 05:00 (ночное окно бэкапов к этому времени закрыто)
Хранить последние: 3
Хранить ежедневные: 7
Хранить еженедельные: 4
Хранить ежемесячные: 3
Почасовые и ежегодные: пусто
Неделя ежедневных — оперативная глубина («вчера же работало»). Месяц еженедельных — на случай, когда проблему заметили не сразу. Ежемесячные — квартал вместо желанных шести: арифметика выше решила за меня. keep-last=3 — страховка для ручных бэкапов: daily-бакет держит один снапшот на календарный день, и без keep-last бэкап, сделанный руками перед рискованными работами, вылетит при первом же prune после ночного планового. При суточном расписании last полностью перекрывается daily-бакетами и не стоит ни байта.
Следом на той же вкладке — GC, ежедневно в 06:00. И verify-job: раз в неделю, re-verify after 30 days — иначе о bit rot узнаёте в самый интересный момент, при ресторе.
Настройка на стороне PVE
А здесь самое трудное — не трогать. В свойствах PBS-стораджа (та самая вкладка Backup Retention) все шесть полей остаются пустыми. Жёлтая плашка снизу описывает fallback честно: без keep-опций PVE сам ничего не прунит, вся власть у PBS. Если задать keep-* с обеих сторон, получаются два конкурирующих механизма: PVE режет сразу после бэкапа, PBS — по своему расписанию, и выяснять, кто из двоих удалил нужный снимок, будете без удовольствия. Один источник истины.Prune ничего не удаляет
Тонкость, о которую спотыкаются: prune только помечает. Место освобождает garbage collector — отдельный job, потому он и стоит в расписании следом за prune. Причём чанк живёт минимум 24 часа 5 минут после последнего обращения (grace period по atime), так что место возвращается с лагом в сутки. Это не баг, а защита от гонки между GC и идущим в этот момент бэкапом.Hourly и yearly — края спектра
Два поля из шести остались пустыми, и не по забывчивости.Keep Hourly. Календарная ловушка: hourly-бакет — это «новейший бэкап в каждом из N последних часов, где бэкапы были». При суточном расписании каждый бэкап попадает в свой уникальный часовой бакет, и keep-hourly=24 молча превращается в 24 суточные копии: hourly обрабатывается раньше daily и съедает его роль. Легитимный сценарий один — отдельный частый job для критичных гостей с RPO меньше суток: бэкап каждые 2–4 часа, keep-hourly=12–24. По месту это копейки (часовой churn 0.3–1%), по I/O для VM тоже: dirty-bitmap читает только изменённые блоки. Контейнерам почасовое противопоказано — битмапа у CT нет, каждый прогон пересканирует всю ФС. И будем честны: point-in-time для MySQL делается binlog'ом, а не молотилкой снапшотов.
Keep Yearly. Самая дорогая опция из шести. Годовой churn — 40–80% (апгрейды ОС, естественный рост данных), каждый yearly тащит примерно половину полного объёма уникальными чанками. Рестор двухлетнего образа нужен чуть чаще, чем никогда; yearly — история про комплаенс. У меня комплаенса нет, поле пустое.
Не забьётся ли диск раньше срока
Нет — при стабильном источнике заполнение асимптотическое, а не линейное. Retention создаёт равновесие: новый daily выталкивает старый за окно, prune помечает, GC освобождает. Датастор растёт до плато и останавливается: приход равен расходу.Прогноз для моей базы считается уже не на коэффициентах, а по наклону графика: Datastore → Summary, usage за неделю. Наклон s — реальная суточная цена в ГБ. К текущему терабайту политика 7/4/3 доберёт неделю daily-дельт, четыре weekly (по ~2.5 суточных цены) и три monthly (по ~6):
Код:
плато ≈ U₇ + 28 × s (для 7/4/6 было бы U₇ + 46 × s)
| Наклон s, ГБ/сут | Решение | Плато |
|---|---|---|
| < 12 | monthly 3 → 6, история — полгода | ≤ 1.55 ТБ |
| 12–19 | остаёмся на 7/4/3 | 1.35–1.55 ТБ |
| > 19 | monthly → 2 или выкидывать жирные диски из бэкапа | — |
Модельные 2% для базы 0.89 дают s ≈ 18 — впритык. Асимметрия, из-за которой стартую консервативно: расширение retention бесплатно и включается в любой момент — бакеты доберутся сами за пару месяцев; урезание срабатывает первым же прогоном и назад ничего не возвращает.
Подвал: датастор на ZFS
Под датастором — четыре SSD по терабайту, собранные в страйп двух зеркал: из 4 ТБ raw остаётся 1.93 полезных. Система живёт отдельно, на своём диске с LVM, и в пул не лезет. Для этой нагрузки зеркала — правильная геометрия: GC и verify — случайное чтение по миллионам файлов, которое зеркала раскидывают на оба диска пары, а resilver — простое копирование, а не пересчёт чётности. Формулу выше эта деталь не меняет — весь расчёт идёт на уровне PBS, — но добавляет свои правила эксплуатации.- 80% — теперь закон вдвойне. CoW-аллокатор ZFS на заполненном пуле переключается в best-fit и начинает тормозить, фрагментация растёт. Бюджет 1.55 ТБ совпадает с классическим правилом для ZFS — совпадение приятное, но не случайное.
- FRAG в zpool list — не про данные. Мои 46% — фрагментация свободного места, метрика аллокатора; дефрага в ZFS нет, и он не нужен. На SSD она бьёт не по сикам, а по скорости выделения блоков ближе к заполнению — ещё один голос за 80%-потолок.
- compressratio ≈ 1.00 — это норма. Чанки приезжают уже сжатыми: zstd отрабатывает на стороне клиента, тот самый c=0.6 из формулы. lz4 на датасете оставляю — early abort делает его бесплатным.
- ZFS dedup не включать. Никогда. PBS дедуплицирует сам, контент-адресацией по SHA-256 — двух одинаковых чанков в сторе не бывает по построению. DDT сожрёт гигабайты RAM и не найдёт ничего.
- recordsize=1M на датасет с датастором: чанки — неизменяемые файлы до 4 МБ, крупная запись режет объём метаданных и ускоряет GC с verify, которые ходят по миллионам файлов. Действует только на новые файлы, поэтому ставится до первого бэкапа — потом поздно.
- TRIM. GC освобождает место, удаляя сотни тысяч файлов, — SSD должны об этом узнавать, иначе через полгода равновесной работы приедет деградация записи.
zpool set autotrim=on, а Debian заодно кладёт в cron помесячный zpool trim (первое воскресенье) и scrub (второе) — проверить, что оба живы. Мой пул на этой проверке встретил меня скрабом двухлетней давности: дата в zpool status честнее любой веры в автоматику.
Scrub при зеркалах сам чинит bit rot на носителе, verify ловит повреждения уровнем выше — вместе это пояс и подтяжки, а не дубляж. И на всякий случай про atime, вокруг которого на форумах традиционная паника: GC обновляет метки явно, через utimensat, так что дефолты ZFS трогать не нужно.
Что реально забивает диск раньше срока
Политика сама диск не убьёт. Убивают три вещи.- Растущий источник. Плато существует только при стабильном D. Если в бэкап попадает то, что растёт само по себе — каталог торрентов, хранилище картинок, — retention бессилен: потолок съедается со скоростью роста источника. Такие диски исключаю из бэкапа (галка Backup на диске VM в PVE) или закладываю рост в бюджет.
- Мёртвый GC. Prune помечает — GC освобождает. Нет GC-job или он молча падает — датастор превращается в храповик: только вверх. Фактор дедупликации на дашборде доказывает лишь, что GC бегал хотя бы раз; разовый прогон и расписание — разные вещи.
- Мёртвые группы. Prune считает бакеты по бэкапам самой группы, а не по календарю. Убрали VM из job — её последние снапшоты навечно останутся в своих бакетах и не устареют никогда. Чистится руками: Datastore → Content → Forget по группе.
Предохранители
Триггер в Zabbix на 85% датастора: при 100% PBS не начинает выселять старое, новые бэкапы просто падают — худший способ узнать о проблеме. Туда же zpool health и возраст последнего scrub, раз уж тот умеет теряться на годы.Аварийный клапан один, и он уже назван: monthly. Срез 3 → 2 опускает плато с 1.51 до 1.4 ТБ; глубже — weekly 4 → 2, но это уже сигнал чистить источник, а не резать retention.
Проверка без прода
Какие снапшоты выживут при вашей комбинации keep-*, видно заранее: у Proxmox есть prune simulator. Задаёте расписание и опции — он раскрашивает, что останется. Нюанс, который там хорошо виден и который я поймал на своём сторе: один бэкап может закрывать несколько бакетов сразу (последний бэкап декабря — одновременно и monthly, и yearly), поэтому счётчики keep-* не суммируются в лоб.Итог
Код:
PVE, storage → Backup Retention: все шесть полей пустые — прунит только PBS
PBS Prune Job: Root, полная глубина, 05:00; keep-last=3 (сохранять последние), daily=7 (ежедневно), weekly=4 (еженедельно), monthly=3 (месячные)
PBS GC: ежедневно, 06:00
PBS Verify: еженедельно, re-verify after 30d
ZFS: recordsize=1M, autotrim=on, trim+scrub в cron, dedup — никогда
Zabbix: 85% датастора, zpool health, возраст последнего scrub
Через неделю: наклон графика usage → monthly 3→6 (s<12) или 3→2 (s>19)