ClickHouse 24.6 и нативный JSON: мигрируем клиентский кластер с JSON-as-String
ClickHouse 24.6 выпустил нативный тип JSON, убрав флаг experimental. Переносим клиентский кластер с JSON-as-String и делимся тем, что получилось.
ClickHouse 24.6 выходит с нативным типом JSON, улучшенным параллелизмом запросов и расширенными возможностями интеграции с S3
В марте, когда вышел ClickHouse 24.3 LTS, мы смотрели на нативный JSON-тип с осторожностью - он был за флагом allow_experimental_json_type и явно не просился в продакшн. В 24.6 ситуация меняется: тип вышел из экспериментального статуса, и у нас на руках конкретный кластер с конкретной проблемой, на которую он отвечает.
Проблема типичная - event-таблица с полуструктурированными данными. Источник - система аналитики поведения пользователей, где каждое событие несёт произвольный набор атрибутов в зависимости от типа. Таблица росла, схема атрибутов менялась с каждым спринтом, и в итоге мы пришли к классическому компромиссу: хранить payload в колонке String, парсить через JSONExtract*-функции в запросах. Работает, но медленно - особенно когда нужно фильтровать по нескольким полям внутри JSON одновременно.
Что изменилось в 24.6 по части JSON
Нативный тип JSON в 24.6 - это не просто синтаксический сахар поверх String. Внутри ClickHouse хранит JSON-документ в колоночном виде: каждый путь (например, payload.user.region) становится отдельным sub-column с типизацией и собственной компрессией. Читать конкретные поля - значит читать только соответствующий sub-column, не разбирая весь документ в памяти.
Дополнительно в 24.6 подтянули:
- Параллелизм запросов. Улучшения Parallel Replicas продолжают серию 24.3 - теперь точнее работает эвристика по числу гранул, при которой подключение реплик даёт выигрыш. Запросы на небольших диапазонах перестали включать параллелизм там, где накладные расходы на координацию были дороже самого ответа.
- S3-интеграция. Расширили набор настроек для работы с S3-бэкендами, включая более гибкое управление retry-политикой и поддержку ролевой аутентификации через instance profile. Для нас это актуально: холодный tier клиентского кластера живёт на S3-совместимом хранилище.
Но основной интерес - JSON, и туда мы и пошли.
Как делали миграцию
Миграция в ClickHouse с изменением типа колонки - это не ALTER COLUMN. ClickHouse не умеет конвертировать существующие данные на месте для таких изменений. Схема работы: новая таблица с нужным типом, перекладка данных, переключение алиасов.
У нас таблица на ReplicatedMergeTree, несколько партиций по месяцу, суммарно несколько сотен гигабайт в партах.
Сначала включили тип на уровне сессии для проверки:
SET allow_experimental_json_type = 1; -- для старых установок, на 24.6 уже не нужен
CREATE TABLE events_v2
(
event_id UUID,
event_time DateTime,
event_type LowCardinality(String),
payload JSON
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events_v2', '{replica}')
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, event_time, event_id);
Перекладку данных делали по партициям, через INSERT INTO ... SELECT:
INSERT INTO events_v2
SELECT
event_id,
event_time,
event_type,
payload -- тип String, ClickHouse парсит при вставке
FROM events
WHERE toYYYYMM(event_time) = 202405;
ClickHouse при вставке из String в JSON-колонку парсит документ и раскладывает по sub-column'ам. На практике это дороже по CPU, чем обычная вставка - держите это в уме при планировании окна перекладки.
Что получилось
Несколько наблюдений после переключения тестовой нагрузки на новую таблицу.
Запросы с фильтрацией по JSON-полям заметно быстрее. Конкретно - запросы вида WHERE payload.utm_source = 'email' AND payload.country = 'RU'. Раньше это был JSONExtractString(payload, 'utm_source') с полным чтением String-колонки и разбором на лету. Теперь читается только sub-column utm_source - разница ощутимая, особенно на больших диапазонах дат.
Аггрегации по JSON-полям тоже выигрывают по той же причине. GROUP BY payload.event_category - ClickHouse тянет только нужный путь, не весь документ.
Нюанс с разреженными полями. У нас часть атрибутов присутствует только в 5-10% событий. ClickHouse хранит такие sub-column'ы эффективно - разреженность не бьёт по размеру хранилища сильнее, чем String с NULL-значениями. Но типовывод на вставке работает по первым попавшимся значениям в блоке - если поле редкое, тип может определиться не так, как вы ожидаете. На это потратили время: пришлось добавить явные type hints для нескольких полей, которые в 90% случаев отсутствуют, но когда появляются, должны быть числом, а не строкой.
Размер данных. После перекладки с хорошей компрессией JSON-представление оказалось чуть тяжелее String для документов с небольшим числом разнотипных полей - структурный overhead съедает часть выигрыша от sub-column'ного хранения. На документах с глубокой и повторяющейся структурой - наоборот, компактнее. В нашем случае вышло примерно паритетно, что нас устраивает: потеря нулевая, а скорость чтения лучше.
Где сейчас
На DWH-сопровождении этого клиента переложили две из пяти event-таблиц на нативный JSON. Остальные три - в очереди, но без спешки: хочется понаблюдать на боевой нагрузке ещё несколько недель, прежде чем переключать всё подряд. Сам 24.6 не LTS - следующий LTS по планам будет 24.8 или позже, так что для консервативных кластеров торопиться с обновлением незачем. Но JSON-тип теперь не experimental, и держать решение в режиме «подождём» больше особых оснований нет.
Если у вас есть JSON-as-String с активной фильтрацией внутри документов - смотреть стоит.