Несколько лет назад перед нами встала встала проблема скорости бекапов с наших серверов баз данных. Проблема была нетиповая, поэтому и с решением возникли сложности. Тем не менее спустя пару лет после внедрения можно сказать, что свою проблему мы успешно решили. Может быть, это поможет кому-то еще.
Совсем необязательное предисловие
Я долго колебался, стоит ли вообще в 2024 году писать статью про Оракл на русском языке и будет ли это кому-то нужно или интересно? Тем не менее желание поделится результатом своих трудов у меня осталось, так что в сторону рефлексию и к делу.
А теперь обязательное предисловие
Наша компания занимается разработкой и поддержкой продуктов на базе OracleDB и у нас есть сервер, на котором крутится около 2000 схем, а если быть точнее - 1905 (все числа буду приводить на момент написания статьи) с суммарным объемом данных 3448 ГБ. Примерно половина схем содержит бинарные медиаданные (изображения, айдиофайлы, видеофайлы и прочее) в виде BLOB-объектов, занимают они 1854 ГБ и их можно вообще не бекапить. А оставшаяся половина схем содержит обычные таблицы с текстовыми данными, датами, числами и т.д. они занимают 1594 ГБ. Схемы друг от друга независимы, изменения в них вносятся разными людьми, в разное время и периодически требуется восстановить какую-то схему за какое-то число. А иногда за разные числа. Поэтому нужно иметь бекап всех этих схем за каждый день.
Что предлагает Оракл из коробки?
Оракл предлагает два типа бекапа (в общих чертах):
Бекап базы данных. Это полный бекап всех файлов БД из которого можно провести восстановление базы при ее повреждении либо полностью
восстановить базу на другой сервер. Этот вариант нам совсем не подходит по следующим причинам:нам придется ежедневно бекапить 3,5 терабайта данных, из которых половина нам вообще не нужна;
в случае, если нам нужны данные из какой-то схемы за какое-то число - нам нужно восстанавливать всю базу. В общем, понятно, тут ненужные объемы, впустую затраченное время и дисковое пространство.
Бекап данных. Бекапятся только данные и метаданные. Возможно бекапить отдельные таблицы, схемы или всю БД. Очевидно, что нам сюда.
Как развивались события
Сервер с момента его настройки (около 10 лет назад) прирастал схемами и объемом постепенно. Поначалу процесс бекапа выглядел так: дампы делались при помощи утилиты expdp, затем скриптами сжимались и переносились на файловый сервер с бекапами. По мере роста объема, нагрузки и важности данных, в какой-то момент это решение стало проблемным в поддержке и требовало регулярного участия администраторов.
Затем мой коллега написал на Джаве утилиту, которая работала так:
запускалась на сервере бекапов;
подключалась к базе данных;
посредством Datapump API запускала экспорт схемы;
по окончании копировала дамп, читая его из базы как BFILE;
сжимала полученный дамп в zip.
И так для каждой нужной схемы по списку.
Это было очень надежное решение, которое много лет проработало и показало себя с самой лучшей стороны на радость всем администраторам. Но шло время, объемы росли. Вначале бекап перестал успевать выполниться за ночь и по утрам БД немного подтормаживала. Потом бекап стал длиться до обеда, пользователи стали жаловаться. Ситуация не была критической, но проблема уже стала вполне ощутима. Потом бекап длился уже весь рабочий день, заканчиваясь только для того, чтобы запустить следующий бекап — процесс бекапа шел почти непрерывно.
А потом бекап перестал укладываться в сутки и бекапы стали делаться через день. Нужно было что‑то делать, но никто не понимал что. Решения не было, сервер рос, длительность бекап росла и уже стала приближаться к двум суткам (46 часов). Проблема начала становиться критической.
Тут начинается история решения
В общем, поначалу стояло два вопроса: на чем писать и что именно писать? На первый вопрос ответ был очевиден, так как у меня уже был успешный опыт написания клиентских утилит для Oracle на С.
По поводу алгоритма и узких мест мысли были следующие: в существуюшем решении бекап каждой схемы состоял из трех крупных этапов:
Бекап схемы на стороне сервера (утилита тут просто ждет конца бекапа. Этот этап, по сути, можно ускорить только заменой дисков на сервере БД. Но на сервере уже стояли достаточно шустрые SSD).
Чтение дампа из БД и запись в файл на сервере бекапов (тут тоже могут помочь только замена дисков на сервере бекапов с более высокой скоростью записи и расширение канала между сервером бекапов и сервером БД).
Сжатие дампа в ZIP (тут все упирается в быстродействие дисков на сервере бекапов и частоту ЦП для сжатия данных).
Путь замены оборудования был, очевидно, мимо — затраты намечались приличные, прирост ожидался не сильно ощутимый.
Первая мысль была такая: совместить второй и третий этап — сжимать данные на лету, попутно сильно сократив дисковые операции (убираются этапы записи в файл и чтения того же файла для сжатия). Навскидку казалось, что это снизит длительность бекапов процентов на 30.
У нас остаются еще два этапа: бекап данных на стороне БД и получение (со сжатием на лету) данных на сервер бекапов. Пока идет первый этап, сервер бекапов ждет; на втором этапе сервер бекапов получает данные, ждет сервер БД.
Напрашивается распараллелить это дело в два потока: один подключается к базе и последовательно запускает бекапы схем, второй один за другим получает эти файлы и, сжимая на лету, складывает на сервер бекапов, что сократит время бекапа еще раза в два. Казалось, что решение верное, бекап сократится раза в 3. Ну, или в 2. Я, с нетерпением потирая руки, приступил к реализации.
Первые тесты на моем компе (я развернул на своем компьютере тестовую базу с двумя десятками схем) показывали отличные цифры, все просто летало. Я дождался выходных, запустил тестовый бекап другого сервера, меньшего объема, и... Бекап прошел быстрее... Но не в три раза. И на в два.
Для сравнения, утилита коллеги забекапила 30 схем на 88 ГБ за 161 минуту (общая скорость бекапа - 0,54 ГБ в минуту). А моя утилита забекапила 43 схемы на 168 ГБ за 208 минут (общая скорость 0.8 ГБ в минуту).
Хм. Я тут все соптимизировал, распараллелил и получил прирост примерно 1.5 раза... Неплохо, конечно, но мы ожидали чего-то побыстрее. Что ж, двигаемся дальше. Благо, теперь из логов утилиты на руках были все временные отметки и длительности всех операций. С понедельника начался первый разбор полетов.
А анализ логов показал следующее:
у потока снятия бекапа общее время экспорта - 1:33:02.
у потока получения дампов цифры такие: время ожидания - 0:06:00, передачи данных - 3:22:45.
То есть все время заняло получение данных!
Да, общее время мы уменьшили, только оказалось, что самым долгим этапом оказалось не снятие бекапа и не копирование дынных, а именно сжатие. На сервере бекапов стоял старенький серверный процессор с частотой 2,5 ГГц и HDD диски, на моем компе ЦП с частотой 3.0 ГГц и SSD диском, тем не менее разница производительности тестовой и реальной конфигурации не должна так сильно отличаться.
Разбор полетов показал — большую роль тут сыграло то, что на этом сервере средний размер схемы был на порядок выше, чем на остальных серверах (в том числе и на моем тестовом), а при большом размере файла время сжатия растет нелинейно, так же как нелинейно падает степень сжатия. Что ж, попытка была хороша, но результат, мягко говоря, не впечатляет.
Работа над ошибками
Направление работ было понятно — получение данных нужно распараллелить в несколько потоков. Чтобы максимально использовать процессорное время сервера бекапов, количество потоков получения данных должно быть равным количеству ядер. Логику организации потоков так же предстояло немного усложнить. Сказано — сделано. Снова запускаем тестовый бекап на том же сервере.
49 схем на 182 ГБ забекапились за 103 минуты. Общая скорость бекапа — 1,76 ГБ в минуту.
[exportThread_1] — Общее время экспорта — 1:43:02
[receiveThread_1] — Время ожидания — 0:40:25, передачи данных — 1:01:53
[receiveThread_2] — Время ожидания — 0:50:48, передачи данных — 0:50:03
[receiveThread_3] — Время ожидания — 0:44:15, передачи данных — 0:58:34
[receiveThread_4] — Время ожидания — 0:25:57, передачи данных — 1:17:20
Что ж, прирост скорости был более, чем в три раза, по сравнению с утилитой коллеги, что меня в общем‑то устраивало — бекап большого сервера БД, запускаясь вечером, к обеду должен был завершаться. При текущей ситуации это был вполне повод для радости.
Я дождался выходных, запустил бекап большого сервера, виновника торжества, и... бекап прошел быстрее на 5 часов (вместо 46 часов все уложилось в 41)! Это был удар... Что ж, идем в логи:
[exportThread_1] - Общее время экспорта - 40:54:02
[receiveThread_1] - Время ожидания - 8:19:18, передачи данных - 1:53:34
[receiveThread_2] - Время ожидания - 8:20:19, передачи данных - 1:55:46
[receiveThread_3] - Время ожидания - 8:26:28, передачи данных - 1:48:14
[receiveThread_4] - Время ожидания - 8:23:31, передачи данных - 1:50:32
Сюрприз‑сюрприз. На этом сервере, несмотря на большой суммарный объем данных и количества схем, средний размер схемы был значительно меньше (200–300 МБ), и при таком раскладе бутылочным горлышком стало не прием и сжатие данных, а сам бекап на стороне сервера. А это значит только одно — ...
Нужно больше потоков!
Процесс выгрузки данных в дампы нужно тоже параллелить. Нужно было придумать, по какому параметру определять количество потоков для задач экспорта.
Немного подробностей. Экспорт данных через Datapump — это серверный процесс. Запуская задачу экспорта, помимо прочих параметров, необходимо указать каталог (Directory) для экспорта, в котором БД создаст дамп. Объект базы данных Directory является логической ссылкой в базе данных на каталог файловой системы сервера, где установлена БД Oracle. Если же такой каталог не передать, используется каталог DATA_PUMP_DIR, который создается автоматически при создании БД.
Очевидно, что запускать слишком много задач экспорта в один каталог не стоит — диск будет просто проседать по записи, параллельные задачи будут тормозить друг друга. Значит нужно иметь несколько Directory, смотрящих на разные диски. Это и подтолкнуло к решению — нужно создавать на сервере каталоги с однотипными именами. Количество потоков экспорта будет определяться количеством таких каталогов и каждый поток экспорта будет делать экспорт в свой каталог. Шаблон имени каталогов взял по имени утилиты — PRODDBBACKUP_DIR_n (PRODDBBACKUP_DIR_1, PRODDBBACKUP_DIR_2...).
Тут уже требовалось сильно пересмотреть организацию очереди потоков и их согласованность между собой. В конце концов основной поток проверял количество ядер на сервере бекапов, количество каталогов на сервере БД и запускал соответственное количество потоков экспорта и получения данных. Для первого тестового запуска было создано четыре каталога на двух дисках. Дожидаться выходных я уже не стал, в конце рабочего дня запустил и пошел домой.
К утру бекап закончился, в логах было такое:
[exportThread_1] - Общее время экспорта - 10:12:55
[exportThread_2] - Общее время экспорта - 10:13:21
[exportThread_3] - Общее время экспорта - 10:15:33
[exportThread_4] - Общее время экспорта - 10:12:13
[receiveThread_1] - Время ожидания - 8:18:19, передачи данных - 1:54:51
[receiveThread_2] - Время ожидания - 8:22:00, передачи данных - 1:54:11
[receiveThread_3] - Время ожидания - 8:40:32, передачи данных - 1:31:57
[receiveThread_4] - Время ожидания - 8:18:37, передачи данных - 1:56:09
Наконец-то!
Заключение
С тех пор объем данных еще вырос, я добавил еще два каталога для экспорта на еще одном диске и на текущий момент за 1594 ГБ данных бекапятся за 9:36 (2,76 ГБ/минуту). Это скорость, которая вполне всех устраивает и которую можно увеличивать, добавляя каталоги (при необходимости диски) для экспорта на сервер БД. Скорость экспорта полностью зависят от организации БД и дисков на сервере БД, скорость получения зависит от канала и количества ядер на сервере бекапов. А мы получили достаточно универсальный инструмент, который неплохо себя показал за пару лет использования.
Репозиторий проекта на GitHub.
Если для кого‑то эта утилита окажется полезной, с радостью отвечу на возникшие вопросы.
UPD: основной вопрос в комментариях - зачем вообще это делать? Я совершенно осознанно не вдавался в детали и причины такого решения, чтобы не отвлекаться от основной темы. Тем не менее поясню - нам не нужен бекап базы данных, нам нужен бекап данных, не привязанных к конкретному инстансу БД. Поставленная задача была решена, все причастные довольны.