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

Почему мы решили сделать новую версию

Для тех, кто не пошёл читать старую статью

nxs‑backup — это инструмент, решающий такие вопросы как:

  • организация резервного копирования штатными инструментами;

  • доставка копий в различные хранилища;

  • ротация создаваемых копий в хранилищах.

Начнём с того, что освежим в памяти наши основные требования к инструменту при написании предыдущей версии:

  • Бэкапить данные наиболее часто используемого в работе ПО:

    • Файлы (дискретное и инкрементное копирование)

    • MySQL (логические/физические бэкапы)

    • PostgreSQL (логические/физические бэкапы)

    • MongoDB

    • Redis

  • Хранить бэкапы в удалённых хранилищах:

    • S3

    • FTP

    • SSH

    • SMB

    • NFS

    • WebDAV

За прошедшее время у нас появились новые требования к нашему инструменту. Далее остановлюсь на каждом подробнее.

Запуск бинарного файла без пересборки из исходников на любом linux

Со временем список систем, с которыми мы работаем, существенно возрос. Сейчас мы обслуживаем проекты, которые используют кроме стандартных deb и rpm совместимых дистрибутивов, такие как Arch, Suse, Alt и др.

В последних системах были сложности с запуском nxs‑backup, т.к. мы собирали только deb и rpm пакеты и поддерживали ограниченный список версий систем. Где‑то мы пересобирали пакет целиком, где‑то только бинарь, кое‑где нам пришлось просто запускать исходники. А на одном из проектов использовались сервера на ARM процессорах. С ними тоже пришлось повозиться.

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

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

Минимизация docker образа с nxs-backup и поддержка ENV в конфигурационных файлах

В последнее время очень много проектов работает в контейнерной среде. Для таких проектов тоже требуется создавать резервные копии, и мы запускаем nxs‑backup в контейнерах. А для контейнерных сред очень важно минимизировать размер образа и иметь возможность работать с переменными окружения.

Старая версия не предоставляла возможности работать с переменными окружения. Главная проблема — пароли приходится хранить прямо в конфиге. Из‑за этого вместо набора переменных, содержащих только пароли, приходится в переменную складывать конфиг целиком. Редактирование больших переменных окружения требует от инженеров большей концентрации и несколько усложняет поиск и устранение неисправности.

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

Даже используя slim‑версию образа мы получали минимальный размер ~250Mb, что довольно много для одной небольшой утилиты. В некоторых случаях это влияло на время начала сбора бэкапов, т.к. образ долго пулился на ноду. Хотелось получить образ, размер которого не превышал бы 50Mb.

Работа с удалёнными хранилищами без fuse.

Ещё одна проблема для контейнерных окружений — использование fuse для монтирования удалённых хранилищ.

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

Всё становится интереснее, когда вам нужен fuse в контейнере. Без повышения привилегий с прямым доступом к ядру хостовой системы задача не решается, а это существенное понижение уровня безопасности.

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

Безопасней и стабильней работать с удалёнными хранилищами используя их API напрямую.

Мониторинг состояния и отправка уведомлений не только на email

Сегодня команды всё реже используют почту в повседневной работе. Оно и понятно, намного быстрее обсудить вопрос в групповом чате или на созвоне. Отсюда такое широкое распространение получили Telegram, Slack, Mattermost, MS Teams и другие подобные продукты.

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

Кроме того, давно хочется иметь возможность отслеживать статус и видеть подробности работы в реальном времени. Для этого, правда, нужно изменить формат работы приложения, превратив его в демон.

Недостаточная производительность

Ещё одной острой болью была недостаточная производительность в определённых сценариях.

У одного из клиентов есть огромная файлопомойка почти на терабайт и всё мелкими файлами — текст, картинки. Мы собираем инкрементные копии этого добра и имеем следующую проблему — годовая копия собирается ТРИ дня. Да, вот так, старая версия просто не в состоянии переварить этот объём менее чем за сутки.

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

Поиск решения

Все вышеперечисленные проблемы в большей или меньшей степени доставляли вполне ощутимую боль IT‑блоку, заставляя тратить драгоценное время на, безусловно, важные вещи, но этих затрат можно было бы избежать. Более того, в определённых ситуациях создавались определённые риски и для владельцев бизнеса — вероятность остаться без данных за определённый день хоть и крайне низкая, но ненулевая. Мириться с положением дел мы отказались.

Возможно, за 4 года что‑то поменялось и в сети появились новые инструменты, — подумали мы. Нужно было это проверить, и посмотреть, вдруг что изменилось в тех, что мы уже рассматривали?

Итог наших изысканий был неутешителен. Мы провели ревизию и изучили пару новых инструментов, которые не рассматривали раньше. Вот они:

  • restic

  • rubackup

Но, как и рассмотренные ранее, эти нам также не подошли, т.к. не полностью удовлетворяли нашим требованиям.

Миша, всё х…, давай по новой

Выбор у нас был непростой. С одной стороны — уже работающая утилита на Python, с другой — она нас не устраивает по ряду параметров.

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

Конечно, мы выбрали Go!

Во первых, Go из коробки имеет AOT‑компилятор, и даёт собрать универсальный бинарник без зависимостей, который можно запускать на любом дистрибутиве и не испытывать проблем из‑за разных версий библиотек. Приятным бонусом в комплекте шла встроенная кросс‑компиляция под другие архитектуры процессоров.

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

Ну и в третьих, на Go у нас полноценно пишет больше народа, чем на Python.

Nxs-backup 3.0

Итогом нашей работы стала новая версия nxs-backup. 

Ключевые особенности новой версии:

  • Все хранилища и все типы бэкапов реализуют соответствующие интерфейсы. Задания и хранилища инициализируются на старте, а не в процессе выполнения работы. 

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

  • Благодаря мини-фреймворку для приложений go-nxs-appctx, который мы используем в наших проектах, теперь в конфигах можно использовать переменные окружения. 

  • Добавилась возможность рассылать события логов через хуки. Вы можете настроить разные уровни и получать только ошибки или события нужного уровня.

  • Существенно уменьшено время работы и объём потребления ресурсов при работе с большим количеством объектов.

  • Сборка приложения осуществляется без использования библиотек C под разные архитектуры процессоров.

  • Ещё мы изменили формат доставки приложения. Теперь это tar-архив на GitHub или Docker образ с бинарником внутри, у которого нет системных зависимостей. 

Теперь бэкапы просто работают на вашем Linux начиная с ядра 2.6. Это очень упростило работу с нестандартными системами и ускорило сборку образов Docker. Сам образ при этом удалось уменьшить до 23Мб (учитывая дополнительные клиенты mysql и psql).

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

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

Ниже приведу примеры основного конфига для бэкапа и конфиги заданий различных типов.

Пример основного конфига
server_name: localhost
#project_name: My best Project

logfile: stdout
loglevel: debug

notifications:
  mail:
    enabled: false
  webhooks:
  - webhook_url: https://hooks.slack.com/services/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
    message_level: error
    payload_message_key: "text"
storage_connects:
- name: s3
  s3_params:
    bucket_name: my_bucket
    access_key_id: ENV:S3_ACCESS_KEY
    secret_access_key: ENV:S3_SECRET_KEY
    endpoint: my.s3.endpoint
    region: my-s3-region
jobs: []
include_jobs_configs: ["conf.d/*.conf"]

Примеры конфигов заданий
# ./conf.d/mysql.conf - MySQL logical backup job example
job_name: mysql_logical
type: mysql
tmp_dir: /var/nxs-backup/tmp_dump

sources:
- name: mysql57
  connect:
    db_host: 'mysql'
    db_port: '3306'
    db_user: ENV:MSYQL_USER
    db_password: ENV:MYSQL_PASS
  targets:
  - all
  excludes:
  - mysql
  - information_schema
  - performance_schema
  - sys
  gzip: true
  db_extra_keys: '--opt --add-drop-database --routines --comments --create-options --quote-names --order-by-primary --hex-blob --single-transaction'

storages_options:
- storage_name: local
  backup_path: /var/nxs-backup/dump/databases
  retention:
    days: 7
    weeks: 4
    months: 6
# ./conf.d/desc_files.conf - Files backup example
job_name: files
type: desc_files
tmp_dir: /var/nxs-backup/tmp_dump

sources:
- name: "www_data"
  save_abs_path: yes
  targets:
  - /var/www/*/data/
  excludes:
  - */bitrix*
  - log
  - tmp
  gzip: true
- name: "etc_conf"
  save_abs_path: yes
  targets:
  - /etc/nginx
  - /etc/php
  gzip: true

storages_options:
- storage_name: local
  backup_path: /var/nxs-backup/dump/files
  retention:
    days: 3
    weeks: 0
    months: 0
- storage_name: s3
  backup_path: /nxs-backup/files
  retention:
    days: 30
    weeks: 0
    months: 6
# ./conf.d/psql.conf - PSQL logical backup example
job_name: psql_logical
type: postgresql
tmp_dir: /var/nxs-backup/tmp_dump

sources:
- name: psql13
  connect:
    db_host: ENV:PSQL_HOST
    db_port: '6432'
    db_user: 'backup@demo'
    db_password: ENV:PSQL_PASS
    psql_ssl_mode: verify-full
    psql_ssl_root_cert: '/var/secret/psql_root.crt'
  target_dbs:
  - demo
  excludes:
  - demo.information_schema
  - demo.my_schema.excluded_table
  gzip: true
  db_extra_keys: ''

storages_options:
- storage_name: local
  backup_path: /var/nxs-backup/dump/databases
  retention:
    days: 3
    weeks: 0
    months: 0
- storage_name: s3
  backup_path: /nxs-backup/files
  retention:
    days: 30
    weeks: 0
    months: 6

Использовать бэкапы в кубе стало намного удобней. В нашем GitHub вы можете увидеть примеры values файлов для универсального чарта. Сгенерировать манифесты можно следующей командой:

helm template nxs-backup nixys/universal-chart -f https://raw.githubusercontent.com/nixys/go-nxs-backup/main/.helm/mysql.values.yml

Тестирование производительности

Конечно, нас интересовал полученный результат с точки зрения производительности. Тестировались инкрементный и дискретный файловые бэкапы каталога с данными, содержащего 10 миллионов файлов.

Сравнивались:

  • Bash-скрипт с tar под капотом,

  • Python версия nxs-backup (далее обозначена как nb 2),

  • Go-lang версия nxs-backup (далее обозначена как nb 3),

  • Утилита Restic.

Пара слов о Restic

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

Результаты тестирования вы можете видеть ниже.

Диаграмма
Диаграмма
Диаграмма
Диаграмма

Инструмент

Время (сек)

Память (Гб)

Bash (desc)

100

0,08

Bash (inc)

150

0,49

nb 2 (desc)

2267

11,2

nb 2 (inc)

2273

12,1

nb 3 (desc)

155

0,09

nb 3 (inc)

199

0,51

restic (inc)

614

28,1*

Hidden text

* Особенность Restic - он потребляет всю доступную в системе память.

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

Как многие могли догадаться, теперь nxs‑backup всего лишь удобная обертка над привычными инструментами. К сожалению, пока нам пришлось отказаться от собственной реализации архивирования файлов в пользу штатного tar. Подробнее об этом расскажу дальше.

Пара слов про подводные камни

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

Утечка памяти или неоптимальный алгоритм

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

На практике решение оказалось рабочим, хотя и не особо эффективным на большом количестве файлов, как можно было увидеть из тестов. Но тогда мы списывали это на особенности Python и надеялись увидеть существенную разницу при переходе на Go.

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

Мы искали решение. Прочли кучу статей и исследований на эту тему, но все они говорили, что использование «filepath.Walk» и «filepath.WalkDir», является оптимальным вариантом. Причём производительность этих методов только растёт с выходом новых версий языка.

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

В конечном итоге всё упёрлось в количество файлов, которые требуется обработать. Мы тестировали 10 миллионов. Garbage Collector, похоже, просто не успевает очищать такой объём порождаемых переменных.

В итоге, поняв, что можем похоронить тут слишком много времени, решили пока отказаться от своей реализации в пользу проверенного временем и по‑настоящему эффективного решения — использовать GNU tar.

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

Такой разный ftp

Ещё одна неприятность всплыла при работе с ftp. Оказалось, что разные сервера ведут себя по-разному при одних и тех же запросах.

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

Так, нам пришлось отказаться использования библиотеки “prasad83/goftp” в пользу более простой “jlaffaye/ftp”, т.к. первая не могла корректно работать с сервером Selectel. Ошибка заключалась в том, что при подключении первая пытается получить список файлов в рабочем каталоге и получает ошибку прав доступа на вышестоящий каталог. С “jlaffaye/ftp” такой проблемы нет, т.к. она проще и сама никаких запросов к серверу не отправляет.

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

Вишенкой на торте оказалась проблема получения файлов с сервера, вернее, попытка получить несуществующий файл. Одни серверы отдают ошибку при попытке обратиться к такому файлу, другие возвращают корректный объект интерфейса io.Reader, который даже можно читать, только вы получите пустой срез байт.

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

Выводы

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

Что дальше?

У нас ещё от прошлой версии остались не реализованными «хотелки», такие как:

  • Шифрование резервных копий

  • Восстановление из резервной копии средствами nxs‑backup

  • Интеграция с мониторингом

  • Web‑интерфейс для управления

Теперь этот список дополняется новыми:

  • Собственный планировщик запусков

  • Новые типы резервных копий (Clickhouse, Elastic, lvm и пр.)

  • Программная реализация создания бэкапов вместо вызова внешней утилиты

  • Возможность задать лимиты на использование ресурсов

И, конечно, мы будем рады узнать мнение сообщества. Какие ещё возможности для развития вы видите? Какие опции вы бы добавили?

Найти исходный код и закинуть issue можно в нашем репозитории на GitHub. Также есть форма для обратной связи на нашем сайте.

Не забывайте следить за нашими обновлениями на YouTube, Habr и подписывайтесь на наш Telegram‑канал DevOps FM — мы всегда рады новым друзьям.

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


  1. maledog
    00.00.0000 00:00
    +2

    Все бы хорошо, НО. У некоторых серверов(например FTP) есть свойство отдавать локальное время, которое может быть например UTC -8. Так что на time.After я бы не полагался в вопросах удаления устаревших файлов. Рискуете удалить лишнее. Кроме того время на удаленном сервере может просто "слететь"(часто бывает когда удаленный сервер под windows в виртуалке на KVM c неправильно созданным конфигом). Время так же может слететь и на машине проводящей архивацию. ИМХО наши друзья юниксоиды часто сталкивались с этим потому ротация архивов часто по номерам файлов. Еще можно записывать дату в имя файла. Но лучше бы в UTC, чтобы не иметь "граблей" с изменениями во временных зонах. А еще процесс архивации и отправки гигабайтов данных по времени может сильно затянуться, так что время создания файла на удаленном сервере будет иметь мало общего с временем создания бэкапа. Удаление бэкапов вещь ответственная.


    1. r_andreev Автор
      00.00.0000 00:00
      +1

      Хорошее замечание, спасибо. Будем думать в этом направлении тоже.


  1. Samamy
    00.00.0000 00:00

    Очень интересно насколько быстрее будет повторный запуск restic с измененными файлами чем в первый раз, все системы бекапов которые могут в дедупликацию и/или бекап только измененных данных, последующие запуски бекапов выполняется гораздо быстрее.
    Еще бы сравнить сколько займет определенное количество бекапов места на диске.
    То есть сравнивать запуская один раз не совсем корректно.
    зы Еще бы версию restic указали какую тестировали


    1. r_andreev Автор
      00.00.0000 00:00

      Тестировали restic версии 0.12.1.

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

      По поводу объёма итоговых данных.
      Если сравнивать инкрементный бэкап tar и слой/коммит созданный restic, то restic сильно проигрывает. В нашем случае после первого запуска бэкап tar получался порядка 250Мб, у рестик объём полученного каталога сотавил чуть больше 2Гб. Прирост инкремента будет зависеть от объёма данных.

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

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

      Резюмируя, сам по себе инструмент интересный и перспективный, но не в наших сценариях работы.