В статье описываются реальный инцидент, связанный с проблемами репликации в кластере 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, которые могут быть двух типов: 

  1. Incremental Materialized Views — данные обновляются в реальном времени по мере их поступления в основную таблицу, на которой построено MV.

  2. 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. 

Читайте также в нашем блоге:

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


  1. Alex-ok
    27.01.2025 05:59

    Двумя словами вся проблема из-за отсутствия метаданных таблиц MV в ZooKeeper. Тоже наступали на эти грабли.