Тема 08

Базы данных временных рядов: ClickHouse

Базы данных временных рядов: ClickHouse

Показания приборов учета, сетевая телеметрия, логи серверов и финансовые тики принадлежат к особому классу данных — временным рядам (Time-Series Data). Они обладают характерными свойствами:

  1. Каждая запись жестко привязана к метке времени (timestamp).
  2. Данные неизменяемы (Append-Only): показание температуры за прошлый четверг никогда не меняется задним числом.
  3. Огромный входящий поток: десятки и сотни тысяч записей в секунду.
  4. Характер запросов аналитический: расчет средних, квантилей, агрегатов по скользящим временным окнам и поиск аномалий среди миллиардов исторических строк 1.

Попытка использовать для таких задач традиционные строковые СУБД (PostgreSQL) приводит к исчерпанию дискового пространства и деградации производительности. Специализированной и наиболее быстрой колоночной аналитической СУБД реального времени стала система ClickHouse, разработанная в Яндексе для сервиса Яндекс.Метрика и открытая в 2016 году 2.

В этой теме мы подробно разберем архитектуру ClickHouse: семейство табличных движков MergeTree, разреженные индексы, специализированные кодеки сжатия временных рядов (Gorilla, DoubleDelta) и непрерывное предагрегирование с материализованными представлениями.

Семейство движков MergeTree

Базовым фундаментом ClickHouse является семейство табличных движков MergeTree (дерево слияния). Название отражает ключевой принцип работы: новые порции данных записываются на диск отдельными неизменяемыми кусками (parts), которые затем в фоновом режиме непрерывно объединяются и упорядочиваются алгоритмом слияния (merge), аналогично структуре LSM-дерева (Log-Structured Merge-Tree) 3.

Синтаксис создания базовой таблицы для телеметрии:

CREATE TABLE telemetry_readings (
    device_id UInt32,
    metric_type LowCardinality(String),
    recorded_at DateTime64(3, 'UTC'),
    val Float64,
    status UInt8
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(recorded_at)
PRIMARY KEY (metric_type, device_id)
ORDER BY (metric_type, device_id, recorded_at);

Ключевые параметры MergeTree

  1. PARTITION BY (партиционирование): Крупноблочное физическое разделение данных по отдельным каталогам на диске (чаще всего по месяцам: toYYYYMM(recorded_at)). Позволяет мгновенно удалять или архивировать старые данные командой ALTER TABLE DROP PARTITION без построчного удаления.
  2. ORDER BY (ключ сортировки): Главный параметр, определяющий физический порядок хранения строк на диске. Данные внутри каждого куска отсортированы строго по кортежу (metric_type, device_id, recorded_at).
  3. PRIMARY KEY (первичный ключ): В отличие от реляционных баз, в ClickHouse первичный ключ не обеспечивает уникальность и не строит плотное B-дерево для каждой строки! Он строит сверхкомпактный разреженный индекс (Sparse Index).

Разреженный индекс (Sparse Index)

В миллионной таблице создание B-дерева на каждую запись потребовало бы гигабайты оперативной памяти. ClickHouse использует разреженный индекс:

  • Таблица логически нарезается на гранулы (по умолчанию index_granularity = 8192 строки).
  • В оперативной памяти сохраняется только значение первичного ключа для первой строки каждой гранулы (индексная засечка):
Гранула 0: (metric="temp", id=101, time=10:00:00) --> Смещение в файле столбца 0
Гранула 1: (metric="temp", id=101, time=12:15:00) --> Смещение в файле столбца 8192
Гранула 2: (metric="temp", id=102, time=08:00:00) --> Смещение в файле столбца 16384

Весь индекс для таблицы на 1 миллиард строк занимает всего несколько десятков мегабайт и целиком помещается в RAM. При запросе с условием WHERE metric_type = 'temp' AND device_id = 101 ClickHouse по разреженному индексу за доли миллисекунды вычисляет номера гранул, в которых могут находиться нужные строки, и читает с диска только эти гранулы.

Специализированные алгоритмы сжатия временных рядов

Колоночное хранение позволяет ClickHouse применять цепочки кодирования и сжатия на уровне каждого отдельного столбца (CODEC(...)) 4:

CREATE TABLE compressed_telemetry (
    recorded_at DateTime CODEC(DoubleDelta, ZSTD),
    val Float64 CODEC(Gorilla, ZSTD)
) ENGINE = MergeTree() ORDER BY recorded_at;
  1. DoubleDelta (двойная дельта): Для строго упорядоченных меток времени сохраняются не сами даты, и даже не разности между ними, а разности разностей (вторая производная). Если данные поступают строго раз в 10 секунд, вторая производная равна 0. Цепочка нулей упаковывается битовым сжатием практически в ноль байт.
  2. Gorilla (алгоритм сжатия вещественных чисел от Facebook): Специализированный алгоритм для сжатия чисел с плавающей точкой Float32/Float64 5. Он вычисляет побитовое исключающее ИЛИ (XOR) между соседними вещественными значениями. Если показания датчика меняются слабо (например, температура колеблется между 21.4 и 21.5), старшие биты экспоненты и мантиссы совпадают, и XOR порождает длинные серии нулевых бит, сжимаемые префиксным кодированием.

В результате телеметрия сжимается в 10–20 раз по сравнению с исходным текстовым форматом.

Специализированные движки семейства MergeTree

  • ReplacingMergeTree: При фоновом слиянии кусков автоматически удаляет дубликаты записей по ключу сортировки, оставляя самую свежую версию по полю ревизии (используется для идемпотентной дозагрузки данных).
  • SummingMergeTree: При слиянии автоматически суммирует числовые столбцы для строк с одинаковым ключом сортировки (идеально для счетчиков трафика и расхода).
  • AggregatingMergeTree: Позволяет сохранять промежуточные состояния агрегатных функций (AggregateFunction), обеспечивая инкрементальный расчет квантилей, уникальных пользователей (uniqHLL12) и средних значений.

Материализованные представления (Materialized Views)

Одной из самых мощных возможностей ClickHouse для IoT являются материализованные представления (Materialized Views). В отличие от других СУБД, в ClickHouse Materialized View — это не снимок данных, а триггер на вставку (Insert Trigger):

-- Таблица агрегированных суточных показателей:
CREATE TABLE daily_metrics_mv (
    device_id UInt32,
    day Date,
    max_val Float64,
    total_samples UInt64
) ENGINE = SummingMergeTree()
ORDER BY (device_id, day);

-- Материализованное представление, наполняющееся на лету при каждой вставке:
CREATE MATERIALIZED VIEW mv_daily_telemetry TO daily_metrics_mv AS
SELECT device_id,
       toDate(recorded_at) AS day,
       max(val) AS max_val,
       count(*) AS total_samples
FROM telemetry_readings
GROUP BY device_id, day;

При поступлении сырых данных в telemetry_readings ClickHouse параллельно преобразует их и дописывает в агрегированную витрину daily_metrics_mv. Запросы дашбордов обращаются к предрассчитанной витрине, выполняясь мгновенно независимо от общего объема накопленной сырой телеметрии.

Итоги темы

ClickHouse воплотил в себе передовые идеи проектирования сверхскоростных аналитических хранилищ: колоночное хранение, семейство движков MergeTree с фоновым слиянием, компактные разреженные индексы и алгоритмы сжатия временных рядов (DoubleDelta, Gorilla). Использование материализованных представлений позволяет строить витрины непрерывной агрегации данных телеметрии в реальном времени с пропускной способностью в миллионы событий в секунду.

Литература

  1. Kleppmann M. Designing Data-Intensive Applications. — O'Reilly Media, 2017 1.
  2. ClickHouse Documentation. ClickHouse Architecture and Core Principles. — ClickHouse Inc., 2024. https://clickhouse.com/docs/ 2.
  3. O'Neil P., Cheng E., Gawlick D., O'Neil E. The Log-Structured Merge-Tree (LSM-Tree). — Acta Informatica, 1996, Vol. 33, No. 4, С. 351–385 3.
  4. ClickHouse Documentation. Column Compression Codecs: Gorilla and DoubleDelta. — ClickHouse Inc., 2024 4.
  5. Pelkonen T., Franklin S., Teller J., Cavallaro P., Huang Q., Meza J., Kaushik V. Gorilla: A Fast, Scalable, In-Memory Time Series Database. — Proceedings of the VLDB Endowment, 2015, Vol. 8, No. 12, С. 1816–1827 5.