Привет! Меня зовут Максим Иванков, я развиваю школы робототехники и программирования для детей уже 9 лет. Всё это живёт на двух серверах, которые я администрирую сам, и про них я недавно писал в статье «Два сервера дома».

В конце мая я настроил отправку бэкапов в объектное хранилище Яндекса и заранее посчитал, сколько это будет стоить: базы сжимаются, restic дедуплицирует, объём небольшой, так что вышло рублей двадцать пять в месяц. Ну то есть буквально ничего.

А через полтора месяца пришло письмо, что средства заканчиваются, и когда я посмотрел на расход, оказалось, что плачу уже около десяти тысяч в месяц, а по темпу последних трёх суток выходило и вовсе сорок с лишним.

Двадцать пять рублей и десять тысяч отличаются ровно в четыреста раз, и всё это время бэкапы отрабатывали штатно, логи оставались зелёными, а никаких ошибок в почте не приходило.

Сразу стоит сказать, что виноват тут был только я, никаких сюрпризов от провайдера не случилось. Просто система, которую я собирал по частям несколько месяцев, в какой-то момент начала работать против меня, а понял я это далеко не сразу.

Разбор занял почти сутки, и по дороге выяснилось сразу несколько вещей, о которых я даже не подозревал.

Что произошло

Начну с цифры, которая объясняет примерно всё.

Репозиторий с бэкапами раздулся до 291 гигабайта и почти семи тысяч снапшотов, при том что реальных данных, которые действительно нужно хранить, там было в несколько раз меньше.

Что было в репозитории
Из чего состояли 291 ГиБ в облаке

Раскладка получилась такая: 190 гигабайт занимали бэкапы резервного сервера, причём это был чистый дубль того, что уже лежало с основного. То есть я просто платил дважды за одни и те же данные.

Ещё около шести гигабайт съедали журналы транзакций двух баз, накопившиеся в режиме, о котором расскажу ниже, плюс там же лежали снапшоты, сделанные до июньского взлома - они давно потеряли всякий смысл, но никуда при этом не делись.

Оставшиеся двадцать гигабайт были собственно тем, ради чего всё затевалось.

Вот тут стоит показать, как менялись деньги, потому что цифры получаются достаточно занятные:

  • 25 рублей в месяц - столько я насчитал при запуске в конце мая

  • 10 тысяч в месяц - столько выходило по факту к середине июля

  • 42 тысячи в месяц - столько получалось по темпу последних трёх суток перед разбором

Причём между первой и второй цифрой не случилось никакого события, после которого что-то сломалось бы - всё это время система работала ровно так, как я её настроил, просто настроил я её неправильно сразу в двух местах.

Почему ничего не удалялось

Тут нужно короткое объяснение того, как работает restic, если кто-то с ним не сталкивался.

Удаление там делается в два шага: сначала forget помечает снапшоты как ненужные по заданной политике хранения, а потом prune физически вычищает блоки данных, на которые больше никто не ссылается. Без второго шага первый не освобождает вообще ничего - снапшоты пропадают из списка, а данные остаются лежать и спокойно оплачиваться.

И вот тут выяснилась первая неприятность - prune не отрабатывал вообще никогда.

Причин оказалось две, и обе я нашёл далеко не сразу.

Репозиторий был повреждён. Один из предыдущих запусков prune не дожал до конца, оставил после себя зависший лок на пятьдесят шесть часов и успел удалить два пака с данными, при этом индекс и снапшоты на них продолжали ссылаться. В таком состоянии restic check прямо писал, что репозиторий повреждён, а prune падал с ошибкой про несуществующий ключ.

То есть механизм очистки был сломан, а я об этом даже не знал, потому что в логи бэкапов не заглядывал - они же зелёные, дампы создаются.

Шесть виртуалок конкурировали за один лок. А вот это оказалось куда интереснее.

Схема выросла вполне естественным путём: каждая машина, которая делает бэкап, сама же и вызывает forget --prune после загрузки. Логично и удобно, пока машина одна, а у меня их стало шесть, и все они ходили в один репозиторий.

Restic берёт эксклюзивный лок на время prune, и когда шесть процессов независимо пытаются это сделать, репозиторий оказывается залочен практически постоянно. Особенно старался хостинг, который держал лок почти всё время, так что ни один prune не мог взять эксклюзивный доступ, и очистка не происходила никогда.

Получилась замкнутая система: retention исправно помечал старое, а удаления при этом не происходило, так что репозиторий рос без предела, и счёт рос вместе с ним.

Вторая история: платил не за объём

Параллельно вскрылась совсем другая проблема, и вот она кажется мне самой поучительной из всей истории.

За месяц до этого я разбирался, почему счёт подрос, и обнаружил, что журналы транзакций отправляются в облако каждые две минуты, то есть 720 запусков restic в сутки на каждую базу.

Тут важный момент: сами по себе журналы места почти не занимают, потому что restic их дедуплицирует и одинаковые блоки хранятся один раз, то есть по объёму никакого роста не происходило.

Деньги уходили за количество операций. Каждый запуск restic - это десятки, а то и сотни обращений к хранилищу: проверить лок, прочитать индекс, загрузить блоки, записать снапшот. Умножьте это на 720 в сутки, потом ещё на две базы, и получите счёт, в котором объём хранения вообще не главная строка.

Две причины роста счёта
За что на самом деле шли деньги

Тогда я замедлил один таймер до тридцати минут и спокойно посчитал вопрос закрытым.

А на следующий день выяснилось, что таймеров два, и второй, на другой виртуалке, продолжал работать в двухминутном режиме и жёг деньги весь день. Просто я про него забыл, потому что настраивал отдельно и в другое время.

Диагностика, которая тут помогла, оказалась довольно простой: restic snapshots --json и группировка по имени хоста, после чего сразу видно, кто именно частит.

Вот эту грабельку я записал отдельно, крупными буквами: если что-то настраивается по одному экземпляру на сервис, всегда проверяй, сколько экземпляров получилось в итоге.

Что сделал

Разбор состоял из двух частей - разово вычистить и структурно починить, чтобы не наросло заново.

Сначала починил репозиторий. Снял все зависшие локи, пересобрал индекс, потом восстановил снапшоты, и один из них оказался битым - его пересобрало, а данные при этом уцелели.

Отключил бэкапы резервного сервера, те самые 190 гигабайт дубля. Тут была тонкость: на этой же машине лежали бэкапы, для которых она была единственным источником - репозитории Gitea, база паролей и данные VPN-проекта. Их трогать было нельзя, поэтому отключал точечно, по конкретным таймерам, а не всё разом.

Удалил снапшоты. И вот тут поджидала довольно неожиданная штука.

Restic не позволяет удалить все снапшоты одного тега политикой. Пробовал --keep-within 0h - не работает, ругается на отсутствие политики, а удалять можно либо по идентификаторам, либо оставив последние N штук. В итоге я собрал список из более чем тысячи идентификаторов и удалял пачками по ним.

Запустил prune, и он разом забрал около 234 гигабайт.

Итог получился такой: с 291 до 54,7 гигабайта и с 6996 до 1282 снапшотов, то есть минус восемьдесят один процент.

Результат чистки
Что получилось после разбора

Структурный фикс

Разовая чистка ничего не стоит, если причина осталась на месте, поэтому дальше я поменял саму схему.

Убрал --prune из всех скриптов на всех машинах, так что теперь каждая виртуалка делает только две вещи: загружает бэкап и помечает старое через forget. Физическим удалением она не занимается вообще.

Сделал один общий prune - отдельный скрипт на машине с бэкапами, который запускается таймером раз в сутки в половине четвёртого ночи, с флагом наверстывания пропущенного. Один эксклюзивный запуск, никакой конкуренции за лок.

Сжал окна хранения журналов: было четырнадцать и семь дней у разных баз, стало три дня у обеих. Возможность восстановиться на любую секунду в пределах трёх суток меня вполне устраивает, а разница в объёме получается заметная.

Замедлил оба таймера до тридцати минут и добавил в команды ожидание лока, чтобы параллельные запуски не падали, а ждали.

Проверил сейчас, спустя полтора месяца: репозиторий держится на 57,6 гигабайта, снапшотов 848, а ночной prune отработал сегодня с кодом ноль. Схема живёт.

Что я из этого вынес

Зелёный лог не значит, что всё работает. Полтора месяца бэкапы создавались, дампы уезжали, никаких ошибок в почте не приходило - и всё это время счёт рос в четыреста раз относительно расчётного. Очистка не работала, потому что она отдельный процесс, за которым никто не следил. Мониторить надо не только «сделался ли бэкап», но и «удалилось ли старое», а ещё лучше - сколько это стоит.

В облаке платят за операции, а не только за объём. Это самая неочевидная часть всей истории: можно хранить копейки данных и получить при этом приличный счёт, если ходить в хранилище каждые две минуты. Считать надо обе величины.

Схема «каждый сам за себя» ломается при росте. Когда виртуалка одна, она может сама делать всё, включая очистку, но при шести машинах на один репозиторий это уже гарантированный клинч. Операции, требующие эксклюзивного доступа, стоит централизовать с самого начала.

Дубли надо искать первым делом. 190 гигабайт из 291 были копией того, что уже лежало рядом, то есть две трети счёта, которые не давали вообще ничего.

Один сервис - это не значит один экземпляр. Я замедлил таймер, отчитался себе о победе и потерял ещё день, потому что второй точно такой же работал на другой машине. Прежде чем закрывать вопрос, стоит убедиться, что нашёл действительно всё.

И последнее, скорее общее. Вся эта история выросла из того, что систему собирали по частям в разное время и под разные задачи, и каждое отдельное решение было вполне разумным, а вместе они дали счёт в четыреста раз больше расчётного. Периодически смотреть на конструкцию целиком, а не на отдельные её куски, в итоге оказывается дешевле, чем разбираться по факту.

Спасибо, что дочитали. Интересно, у кого какие были сюрпризы со счетами за облако - и удавалось ли поймать причину быстрее, чем за сутки.

Комментарии (1)


  1. onyxmaster
    29.08.2026 11:45

    Первый вывод тут не технический должен быть — надо настраивать бюджеты и алерты на них.