В статье описываются реальный инцидент, связанный с проблемами репликации в кластере ClickHouse, и процесс восстановления работоспособности сервиса. Рассмотрим особенности работы материализованных представлений, их репликации и причины возникновения ошибок, а также разберём практические решения для устранения подобных проблем.
Инцидент
Однажды поздним воскресным вечером ничто не предвещало неприятностей, пока не поступило сообщение о проблемах с репликацией в продуктовом кластере ClickHouse. Кластер состоит из одного шарда и двух реплик.
Оперативно было выявлено, что причиной неработоспособности репликации по некоторым таблицам стало повреждение данных, хранящихся в ZooKeeper (причины повреждения этих данных выходят за рамки этой статьи). В целом ситуация некритична: создаём новый пустой кластер ZooKeeper, перенацеливаем на него все реплики Clickhouse и восстанавливаем данные, которые должны храниться в ZooKeeper.
Для этого в ClickHouse, начиная с версии 21.7, нет необходимости выполнять detach/attach
элементов хранения, так как есть удобная SYSTEM-команда. Пройдёмся в цикле по всем пользовательским таблицам семейства ReplicatedMergeTree* и выполним: SYSTEM RESTORE REPLICA db_name.table_name ON CLUSTER '{cluster}'
.
Готово! Данные в ZooKeeper восстановлены.
Очень странные дела
Но что-то идёт не так, как ожидалось. Сервис не работает. Приложения почему-то не могут писать в некоторые таблицы. Оказывается, более чем на десятке таблиц есть построенные Materialized Views, которые по-прежнему находятся в readonly, несмотря на выполненный SYSTEM RESTORE
:
DB::Exception: Table is in readonly mode since table metadata was not found in zookeeper (TABLE_IS_READ_ONLY)
Пример такого Materialized View:
CREATE MATERIALIZED VIEW db_name.table_name_mv ON CLUSTER '{cluster}'
(
`FieldA` String,
`FieldB` DateTime
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/{uuid}', '{replica}')
ORDER BY FieldB
SETTINGS index_granularity = 8192 AS
SELECT
FieldA,
FieldB
FROM db_name.table_name
Странно и непонятно. Зачем для Materialized View (MV) использовать движок семейства ReplicatedMergeTree*, если сами таблицы, по которым строятся эти View, уже реплицируются? Если же эти MV зачем-то реплицируются, то почему тогда они находятся в readonly, но их нет в выводе запроса SELECT * FROM system.replicas WHERE is_readonly=1
?
Здесь было бы оптимальным внимательно погрузиться в документацию ClickHouse этим поздним воскресным вечером и найти ответы на эти вопросы. Например, вот так:
Но, к сожалению, обстановка не располагает.
Восстановление работы сервиса
Восстанавливать работу сервиса надо срочно, поэтому решаем задачу «в лоб». Создаём новое материализованное представление с той же структурой, но с другим именем:
CREATE MATERIALIZED VIEW db_name.table_name_mv_new ON CLUSTER '{cluster}'
(
FieldA String,
FieldB DateTime
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/{uuid}', '{replica}')
ORDER BY FieldB
SETTINGS index_granularity = 8192 AS
SELECT
FieldA,
FieldB
FROM db_name.table_name
Переливаем данные:
INSERT INTO db_name.table_name_mv_new SELECT * FROM db_name.table_name_mv;
Удаляем старое материализованное представление:
DROP TABLE db_name.table_name_mv ON CLUSTER '{cluster}'
После этого запись в основную таблицу снова пошла. Осталось переименовывать новое материализованное представление:
RENAME TABLE db_name.table_name_mv_new TO db_name.table_name_mv ON CLUSTER '{cluster}';
И так пройтись по всем MVs. Проблема решена, работа сервиса восстановлена.
На самом деле
Необходимо сказать несколько слов о работе материализованных представлений в ClickHouse. Во-первых, о хранении данных в материализованных представлениях в ClickHouse, которые могут быть двух типов:
Incremental Materialized Views — данные обновляются в реальном времени по мере их поступления в основную таблицу, на которой построено MV.
Refreshable Materialized Views — перестраиваются по запросу, подобно материализованным представлениям в классических реляционных СУБД.
В нашем случае используются Incremental MVs. При этом они могут быть объявлены синтаксически двумя разными способами: с явным указанием таблицы-назначения (более прозрачно для понимания и управления) и с неявным заданием таблицы-назначения.
Пример явного указания:
CREATE TABLE db_name.mv_table ON CLUSTER '{cluster}'
(
FieldA String,
FieldB DateTime
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/{uuid}', '{replica}')
ORDER BY FieldB
SETTINGS index_granularity = 8192
CREATE MATERIALIZED VIEW db_name.table_name_mv ON CLUSTER '{cluster}' TO db_name.mv_table AS
SELECT
FieldA,
FieldB
FROM db_name.table_name
Пример с неявным заданием, то есть с указанием Engine вместо TO:
CREATE MATERIALIZED VIEW db_name.table_name_mv ON CLUSTER '{cluster}'
(
FieldA String,
FieldB DateTime
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{cluster}/{shard}/{uuid}', '{replica}')
ORDER BY FieldB
SETTINGS index_granularity = 8192 AS
SELECT
FieldA,
FieldB
FROM db_name.table_name
Однако во всех случаях MV хранит свои данные в обычной таблице. Если таблица, в которую необходимо складывать данные, явно не указана инструкцией TO, тогда ClickHouse создаст таблицу для хранения данных сам и присвоит ей имя вида .inner_id.UUID
, где UUID
— это UUID создаваемого MV.
Пример:
SHOW TABLES
┌─name───────────────────────────────────────────┐
1. │ .inner_id.0a341d8d-55f2-4465-b7c4-3a7bf4ade0d6 │
2. │ table_name │
3. │ table_name_mv │
└────────────────────────────────────────────────┘
Во-вторых, рассмотрим репликацию материализованных представлений. Казалось бы, зачем реплицировать MV, если мы уже реплицируем исходные таблицы, на которых оно строится?
Репликация таблиц в ClickHouse организована через обмен кусками данных конкретных таблиц (parts) между членами реплики, а не через передачу журналов предзаписи (WAL, binlog и т. д.), как в классических реляционных СУБД.
Необходимость репликации материализованных представлений обусловлена тем, что Incremental MV в ClickHouse — это по сути триггер на INSERT. Поскольку при поступлении новых данных в исходную таблицу с другого члена реплики INSERT не произойдёт, необходимо реплицировать не только исходную таблицу, но и таблицу — хранилище данных MV.
Выводы
Работоспособность сервиса была восстановлена без потери данных, однако не оптимальным путем. Для более быстрого восстановления не хватило буквально пары крупиц понимания работы механизма репликации MV в ClickHouse. Достаточно было бы сделать что-то вроде:
for i in $(clickhouse-client --password my_pass --verbose --progress --query "SELECT concat(database, '.\`', table, '\`') FROM system.replicas WHERE is_readonly=1"); do
echo "${i}:"
clickhouse-client --password my_pass --verbose --progress --query "SYSTEM RESTORE REPLICA ${i} ON CLUSTER '{cluster}'"
done
И не было бы дополнительного простоя, связанного с переливом данных.
Остаётся пожелать всем быть как котик:
P. S.
Читайте также в нашем блоге:
Alex-ok
Двумя словами вся проблема из-за отсутствия метаданных таблиц MV в ZooKeeper. Тоже наступали на эти грабли.