ClickHouse прощает многое — один сервер тянет миллиарды строк, — но у него свой набор эксплуатационных сложностей, не похожих ни на PostgreSQL, ни на Kafka. Разберём, что настраивают до прода и на что смотрят после.
Репликация: зачем и как устроена
Если ClickHouse стоит в одном экземпляре и сервер упадёт — данные недоступны. Дублировать их на несколько машин помогает репликация.
В ClickHouse репликация — на уровне таблицы, не сервера. Реплицируются только те таблицы, чей движок принадлежит семейству Replicated*MergeTree:
CREATE TABLE order_events ON CLUSTER main
(
event_time DateTime,
event_type LowCardinality(String),
order_id UUID,
amount Decimal(18, 2)
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/order_events', '{replica}')
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, event_time);
Координатор репликации — ClickHouse Keeper (встроенная замена ZooKeeper; для новых установок — только Keeper). Keeper хранит метаданные: какие куски данных существуют, очередь репликации, кто лидер по партиции. Сами данные ходят между репликами напрямую.
Важные свойства:
- Репликация асинхронная и мультимастерная — писать можно в любую реплику, остальные подтянут. Чтение с другой реплики сразу после записи может не увидеть данные — это нормально для аналитических сценариев, но не строить на этом строгую согласованность.
- Вставка одних и тех же данных дважды не задвоит строки: дедупликация вставок через Keeper включена для Replicated-таблиц по умолчанию.
Минимальная продуктивная топология: 2 реплики + 3 ноды Keeper (Keeper можно совмещать с ClickHouse-нодами на небольших кластерах). Одна нода без репликации допустима, если данные можно перезалить из Kafka или PostgreSQL; если ClickHouse — единственное место хранения событий, одна нода не подходит.
Шардинг: позже, чем кажется
Типичная ошибка — шардировать сразу. Один сервер ClickHouse обрабатывает терабайты; пока вертикальный рост (диск, память) и правильная схема справляются — шардинг не нужен. Он нужен, когда данные физически не помещаются на одной машине или когда один запрос упирается в CPU одного сервера.
Механика: данные разрезаются по шардам, поверх — таблица-маршрутизатор:
ENGINE = Distributed(cluster, db, local_table, sharding_key)
Запрос в Distributed разлетается по шардам и собирается на инициаторе. Вставка — либо через Distributed (он раскидает по ключу), либо напрямую в локальные таблицы шардов (надёжнее под нагрузкой, меньше промежуточной буферизации).
Выбор sharding_key: равномерность важнее смысла. cityHash64(order_id) распределит ровно; шардинг «по региону» даст горячий шард на крупном регионе.
TTL: когда старые данные дорого хранить
Аналитические данные стареют, и хранить сырые события вечно накладно. TTL решает это декларативно:
ALTER TABLE order_events
MODIFY TTL event_time + INTERVAL 12 MONTH DELETE;
Варианты политики:
DELETE— удалить старые строки (фоном, при слиянии кусков).TO DISK 'cold'/TO VOLUME 'cold'— многоуровневое хранение: свежие месяцы на быстрых дисках, старые — на дешёвых или S3. Это то, что в других системах называют горячий/тёплый/холодный уровень.GROUP BY ... SET— понижение точности: старше года хранить не события, а суточные агрегаты.
Типовая связка: сырые события с TTL 12 месяцев + материализованное представление с агрегатами навсегда. Дашборды живут на агрегатах, сырьё для точечного анализа есть за последний год, диск под контролем.
Удаление целыми партициями — самый дешёвый способ чистки:
ALTER TABLE order_events DROP PARTITION '202401';
Это ещё один аргумент за месячные партиции.
Бэкапы
Реплика — не бэкап: команда DROP TABLE реплицируется так же исправно, как INSERT. Для настоящего резервного копирования — встроенный механизм:
BACKUP TABLE analytics.order_events
TO S3('https://s3.../backups/order_events/2026-06', '...', '...');
RESTORE TABLE analytics.order_events
FROM S3('https://s3.../backups/order_events/2026-06', '...', '...');
Бэкапы инкрементальные: неизменяемость кусков (parts) позволяет копировать только новые. Целиться стоит в S3.
Дополнительная страховка аналитического контура — возможность перезалить данные из источника. Пока Kafka хранит события или PostgreSQL хранит историю, ClickHouse восстановим пересборкой. Эта «вторая линия» исчезает, как только срок хранения в Kafka короче глубины данных в ClickHouse.
Мониторинг через system-таблицы
ClickHouse рассказывает о себе SQL-запросами — вся диагностика в схеме system:
| Таблица | Что смотреть | Порог для алерта |
|---|---|---|
system.parts | Число активных кусков на таблицу-партицию | > 300 на партицию — вставка скоро встанет |
system.merges | Текущие слияния, прогресс | Слияния не успевают за вставкой |
system.mutations | Очередь ALTER UPDATE/DELETE | is_done = 0 несколько часов |
system.replication_queue | Отставание реплик | Очередь растёт и не разгребается |
system.query_log | Кто, что и сколько читал | Запросы с чтением > N ГБ или > N секунд |
system.disks | Свободное место | < 20% — слияниям нужен запас под копию кусков |
Экспорт метрик в Prometheus встроен — секция <prometheus> в конфиге сервера, дальше стандартные дашборды и алерты. Стартовый набор алертов: слишком много кусков (TOO_MANY_PARTS), отставание репликации, диск, доля упавших запросов, длительность фоновых изменений.
Типичные проблемы и их причины
TOO_MANY_PARTS — вставка отвергнута. Причина почти всегда в пишущем сервисе: мелкие частые INSERT-ы, слишком дробное партиционирование или каскад материализованных представлений, размножающий вставки. Решается группировкой вставок и пересмотром схемы, а не подкруткой лимита.
OOM на тяжёлом GROUP BY. Агрегация по высококардинальной колонке не влезла в max_memory_usage. Быстрое средство — max_bytes_before_external_group_by (выгрузка на диск: медленнее, но запрос доживёт); системное решение — предагрегация через материализованные представления.
Реплики разошлись. Смотреть в system.replication_queue и логи Keeper: чаще всего причина — сеть до Keeper или заполненный диск. Реплика восстанавливается командами:
SYSTEM RESTART REPLICA table_name;
SYSTEM RESTORE REPLICA table_name;
Данные дольёт репликация самостоятельно.
Изменения (мутации) стоят. Очередь ALTER ... DELETE (например, удаление данных по требованию регуляторов) блокируется нехваткой диска или ошибкой в одной мутации. Зависшую мутацию можно отменить через KILL MUTATION, после чего разбирать причину.
Запрос-убийца. SELECT * за два года без фильтра. Защита — профили пользователей: max_execution_time, max_memory_usage, max_rows_to_read для read-only пользователей, квоты на частоту. Эти ограничения ставят до инцидента.
Версии и обновление
ClickHouse выпускается ежемесячно; для продуктивных систем — LTS-релизы (два в год, год поддержки). Обновление кластера — пореплично, без остановки: реплика выводится, обновляется, догоняет очередь репликации, затем следующая. Перед крупным обновлением — прогон рабочих запросов на тестовом окружении: оптимизатор и поведение настроек между LTS-версиями меняются заметнее, чем в PostgreSQL.
Коротко
- Репликация — на уровне таблицы, движок
ReplicatedMergeTree; координирует Keeper. - Минимальная продуктивная топология: 2 реплики + 3 ноды Keeper.
- Шардинг нужен позже, чем кажется — один сервер тянет терабайты; добавляйте, когда вертикальный рост исчерпан.
- TTL управляет жизнью данных: удаление, перемещение на холодный диск или понижение точности.
- Реплика — не бэкап;
DROP TABLEреплицируется. Бэкап —BACKUP TO S3. - Вся диагностика — SQL через
system.*таблицы; экспорт метрик в Prometheus встроен. - Самая частая авария:
TOO_MANY_PARTS— решается группировкой вставок, не подкруткой лимита.
Что почитать дальше
- Fundamentals — куски и слияния, из которых растёт половина этого раздела.
- Интеграция с ClickHouse — пайплайн, который эти алерты охраняют.
- Elasticsearch: эксплуатация — схожие темы (тиеринг, снэпшоты, размер кластера) в соседнем хранилище.