ClickHouse 20.4 в production: Materialized Views для предагрегации и реальное ускорение аналитики
Обновляем production-кластер клиента до ClickHouse 20.4: тестируем улучшенные Materialized Views для предагрегации метрик, Dictionary источники, замеряем результат.
Выпуск ClickHouse 20.4 (апрель 2020): улучшенные Materialized Views, расширенные Dictionary источники, оконные функции в активной разработке
В мае мы строили дашборды DataLens поверх ClickHouse и в процессе делали materialized views для предагрегации - в том посте это упоминалось вскользь. Сейчас пришла пора разобраться подробнее: один из клиентов попросил провести обновление production-кластера до ClickHouse 20.4 и заодно посмотреть, что там изменилось в части Materialized Views. Смотрели, обновляли, замеряли.
Контекст: клиент и его данные
Клиент - ритейл, несколько каналов продаж, онлайн и офлайн. ClickHouse стоит на двух серверах в репликации, туда прилетают события из нескольких источников: транзакции, клики, события склада. Основная проблема была прозаичная: аналитические запросы с агрегациями по большим диапазонам дат работали секунд по 10, иногда больше. Аналитики жаловались - и справедливо. Дашборд, который грузится дольше 2-3 секунд, практически не используется.
Версия была 20.1, в которой MV уже работали, но с некоторыми ограничениями. В 20.4 кое-что поменяли.
Что поменялось в Materialized Views в 20.4
Основное, что нас интересовало в релизе 20.4 - это изменения в работе Materialized Views. Если коротко, два момента оказались практически значимыми.
Поддержка JOIN в запросе MV. До 20.4 запрос, лежащий в основе материализованного представления, мог обращаться только к одной таблице - той, на которую навешен триггер. Теперь можно делать JOIN с другими таблицами прямо в определении MV. Для нас это было важно: часть метрик требовала обогащения данными из справочников прямо в момент записи, и раньше это делалось отдельным ETL-шагом.
Более предсказуемое поведение при ошибках в MV. В старых версиях ошибка в материализованном представлении могла, в зависимости от настроек, либо молча проглатываться, либо блокировать INSERT в основную таблицу. В 20.4 поведение стало чище: materialized_views_ignore_errors работает более предсказуемо, и в логах видно что именно пошло не так. Для production это важнее, чем звучит: дебажить "вставка прошла, но данные в MV не попали" - то ещё удовольствие.
Как строили предагрегацию
Структура у нас стандартная для таких задач. Есть сырая таблица с событиями на движке MergeTree, туда льются данные. Поверх неё - materialized view, которое при каждом INSERT считает агрегаты и пишет результат в отдельную таблицу на AggregatingMergeTree.
Упрощённо схема выглядит так:
-- Целевая таблица с агрегатами
CREATE TABLE sales_daily_agg (
date Date,
region_id UInt32,
category_id UInt32,
revenue_state AggregateFunction(sum, Decimal(18,2)),
orders_state AggregateFunction(count, UInt64)
) ENGINE = AggregatingMergeTree()
ORDER BY (date, region_id, category_id);
-- Материализованное представление
CREATE MATERIALIZED VIEW sales_daily_mv TO sales_daily_agg AS
SELECT
toDate(event_time) AS date,
region_id,
category_id,
sumState(amount) AS revenue_state,
countState() AS orders_state
FROM sales_events
GROUP BY date, region_id, category_id;
Читать из такой таблицы нужно через sumMerge / countMerge - ClickHouse сам доделывает частичные агрегаты при SELECT. Выглядит немного непривычно, зато работает быстро.
Ключевой момент - AggregatingMergeTree хранит не финальные значения, а промежуточные состояния агрегатных функций. Это позволяет корректно объединять данные при слиянии кусков, не теряя точность. Для sum это кажется излишним, но для uniq (уникальные пользователи) - принципиально: нельзя просто складывать количества уникальных из разных кусков.
Обновление кластера
Обновление делали поочерёдно по нодам, не останавливая кластер целиком. ClickHouse поддерживает репликацию между разными версиями в процессе роллинг-апдейта - это задокументировано и работает.
Несколько наблюдений по процессу:
Резервная копия перед обновлением - обязательно. Не потому что мы ожидали проблем, а потому что production. Сделали clickhouse-backup create перед стартом.
Порядок обновления реплик имеет значение. Обновляли сначала реплику, которая не является лидером по репликации для основных таблиц, убеждались что всё работает - потом вторую. Суммарно на кластер из двух нод ушло около двух часов с учётом проверок.
Совместимость MV между версиями. Старые Materialized Views заработали без каких-либо изменений после обновления. Новые фичи (JOIN в MV) - это опциональные возможности, их нужно явно использовать при создании новых представлений.
Что получили на выходе
После обновления и создания предагрегированных таблиц переключили дашборды читать из них вместо raw-данных.
Запросы, которые раньше занимали 8-12 секунд - типа «продажи по регионам за квартал с разбивкой по категориям» - начали отвечать за 200-400 миллисекунд. Разница ощутимая. Аналитики это заметили сами, без того чтобы мы им рассказывали - просто перестали жаловаться.
Нагрузка на кластер при чтении тоже упала: вместо полного скана нескольких сотен миллионов строк запросы теперь читают предагрегированные данные, объём которых на несколько порядков меньше.
Обратная сторона: предагрегация - это компромисс. Запросы, которые не укладываются в заданные измерения агрегата, всё равно идут в сырые данные. Аналитики иногда придумывают срезы, которых мы не предусмотрели - тогда 10 секунд возвращаются. С этим ничего не поделать, кроме как добавлять новые MV под новые паттерны запросов - что мы и планируем делать по мере накопления понимания о том, что реально используется.
Dictionary источники и немного про оконные функции
В 20.1 добавили поддержку Redis как источника для Dictionary, а в 20.4 расширили возможности HTTP-источников. Мы потестировали HTTP Dictionary для обогащения данных из внутреннего справочника клиента, который живёт на отдельном сервисе. Работает, но с оговоркой: кеширование словаря нужно настраивать аккуратно, иначе либо данные устаревают, либо сервис-источник получает избыточную нагрузку при рефреше.
Оконные функции - пока в активной разработке, в продуктивный код не ставили. Следим за развитием.
Где сейчас
Кластер работает на 20.4, предагрегация настроена для основных паттернов запросов, дашборды через dwh-bi отвечают быстро. Следующее, что хотим сделать - настроить мониторинг покрытия: какой процент реальных запросов попадает в MV, а какой идёт в сырые данные. Это позволит понять, где ещё стоит добавить предагрегацию, не гадая.