Меня зовут Леонид Блынский и я администратор баз данных в Лиге Цифровой Экономики. В этой небольшой статье расскажу, как я делаю резервное копирование кластера ClickHouse размером 20 ТБ.
Документация по резервному копированию довольно небольшая и содержит инструкции по созданию резервных копий отдельной инсталляции СУБД. К сожалению, информации о том, как создавать резервные копии кластера, практически нет. Как и нет промышленного решения для управления бэкапом.
Мы в команде изучили возможности, которые предоставляют инструменты сторонних разработчиков, и решили разработать собственный инструмент с визуализацией и мониторингом. Но перед этим предстояло научиться делать само резервное копирование.
Для стенда разработки мы сделали возможность создания локальных бэкапов. В файле настроек /etc/clickhouse-server/config.d/backup_disk.xml
потребовалось прописать разрешенный путь для бэкапа:
<clickhouse>
<storage_configuration>
<disks>
<backups>
<type>local</type>
<path>/var/lib/clickhouse/backups</path>
</backups>
</disks>
</storage_configuration>
<backups>
<allowed_disk>backups</allowed_disk>
<allowed_path>/var/lib/clickhouse/backups/</allowed_path>
</backups>
</clickhouse>
Разработчики и аналитики теперь могли сохранить данные перед какими-либо необратимыми действиями:
backup table main.dim_account TO Disk('backups', ' main.dim_account.zip')
Ключевое слово Disk следует писать с большой буквы:)
Или так, для бэкапа всей БД:
backup database main TO Disk('backups', ' main_dev.zip')
Мы проверили — восстановиться просто, все работает отлично:
restore table main.dim_account AS main.dim_account FROM Disk('backups', ' main.dim_account.zip')
Далее мы настроили хранилище S3 для хранения бэкапа нашей БД. Команда для запуска стала выглядеть так:
backup table main.dim_account to S3('http://10.10.10.10:9000/clickhouse/ main.dim_account.zip','bkp_user','bkp_pwd');
Далее мы попробовали вручную забэкапить все таблицы базы. Все шло хорошо, пока не пришла очередь самой большой таблицы размером около 3 ТБ. В какой-то момент стала выпадать ошибка:
<Fatal> BaseDaemon: Received signal Segmentation fault (11)
<Fatal> BaseDaemon: Address: 0x7f0ba8a00960 Access: write. Address not mapped to object.
<Fatal> BaseDaemon: Stack trace: 0x7f7ab25479e6 ………………….
Ошибка возникала случайным образом: таблица то бэкапилась, то нет. Поиск на форумах ни к чему не привел.
Так как с другими таблицами проблем не возникало, мы сделали вывод, что проблема именно в размере, потому и надо что-то придумать, чтобы уменьшить размер резервной копии в одной команде.
Оказалось, что таблица секционирована, а ClickHouse замечательно умеет делать резервные копии отдельных партиций.
В нашем кластере 6 узлов и несколько конфигураций баз данных: база allrp (имеет реплики на всех шести узлах), main (3 шарда, и каждый шард имеет две реплики).
Для бэкапа allrp достаточно снять копию только с одного узла. Для резервного копирования main требуется снять копию c трех узлов. То есть надо зайти на все хосты, где располагаются шарды, сгенерировать и выполнить команды бэкапа.
Немного вспомнив язык SQL, мы явили на свет скрипт генерации команд бэкапа по партициям:
select distinct 'backup table ' || database || '.' || table || ' partition ''' || partition || ''' to S3(''http://10.10.10.10:9000/clickhouse/' || formatDateTime(now(),
'%d-%m-%Y')|| '/main.' || table || '.' || partition || '-shard-' || q2.shard || '.zip'',''bkp_user'',''bkp_pwd'');' as backup_command
FROM
system.parts,
(
select
toString(shard_num) as shard
from
system.clusters
where
host_address = '127.0.0.1'
and cluster = 'main') q2
where active
and database = 'main'
and table in ('fct_bigtable')
group by
database,
table,
partition,s
q2.shard
Все скрипты бэкапа по партициям отрабатывают успешно, таблицу можно восстановить.
Так как остальные таблицы не вызывали проблем, то их бэкапим уже целиком:
select * from
(
select
distinct 'backup table ' || database || '.' || table || ' to S3(''http://10.10.10.10:9000/clickhouse/' || formatDateTime(now(),
'%d-%m-%Y')|| '/main.' || table || '-shard-' || q2.shard || '.zip'',''bkp_user'',''bkp_pwd'');'
from
system.parts,
(
select
toString(shard_num) as shard
from
system.clusters
where
host_address = '127.0.0.1'
and cluster = 'main') q2
where
active
and database = 'main'
and table not like 'fct_bigtable%'
) q1
Данные SQL надо выполнить на каждом узле с уникальными шардами (в нашем примере на трех) и на этих же узлах запустить сгенерированные команды. Для сохранения результата запроса в файл можно последней строчкой дописать:
INTO OUTFILE '/var/lib/clickhouse/backups/backup.sql' FORMAT TabSeparatedRaw;
Для бэкапа базы allrp генерируем команды, которые надо запустить на любом узле:
select
distinct 'backup table ' || database || '.' || table || ' to S3(''http://10.10.10.10:9000/clickhouse/' || formatDateTime(now(),
'%d-%m-%Y')|| '/allrp.' || table || '.zip'',''bkp_user'',''bkp_pwd'');'
from
system.parts
where
active
and database = 'allrp';
Наблюдать за работой процесса резервного копирования и его статусом можно так:
select * from system.backups_distributed
После того как нам удалось в ручном режиме проводить полноценный бэкап базы данных, начался этап автоматизации. Мы привлекли разработчика Python — он за пару дней реализовал задачи в Airflow, которые генерируют команды бэкапа, запускают их, отслеживают выполнение и в случае аварии отдельных команд ставят их в очередь снова. То есть бэкап стало возможным пускать по расписание или «по нажатию кнопки» в Airflow.
После успешного выполнения бэкапа инфоруется команда поддержки. Кроме того, на логи работы бэкапа мы прикрутили Grafana. В ней видим информацию о последних бэкапах, время выполнения, размер, а также случившиеся ошибки. С помощью распараллеливания команд резервного копирования нам удалось сократить время бэкапа с 40 до 18 часов.
Как результат — вполне стабильный инструмент с интерфейсом, визуализацией работы и мониторингом, который можно полностью контролировать.
Ранее мы рассказывали, как работать со словарями данных и оптимизировать запросы и распределенными таблицами в ClickHouse. Возможно, это тоже будет полезно.
Demosfen
А почему не clickhouse-backup? По каким причинам не подошел?