Learning Platform
Глоссарий Troubleshooting
Урок 08.03 · 20 мин
Средний
Log AnalyticsELK ReplacementText IndexBloom FilterTTLMapLowCardinalityObservability

Схемы для лог-аналитики

Elasticsearch (ELK stack) и Grafana Loki — два доминирующих решения для лог-аналитики. Оба имеют существенные ограничения: ELK требует Java heap и инвертированные индексы на каждое поле (высокая стоимость хранения), Loki не индексирует содержимое логов (медленный полнотекстовый поиск).

ClickHouse предлагает третий путь: столбцовое хранение + text index + агрессивный TTL. Логи занимают в 5-10 раз меньше места, чем в Elasticsearch, при сопоставимой скорости поиска.


Архитектура log pipeline

Log pipeline: от ingestion до retention
Ingestion: Vector / Fluent Bit / HTTPIngestion: логи поступают из приложений через Vector, Fluent Bit, или напрямую через HTTP-интерфейс ClickHouse (порт 8123). Формат: JSONEachRow или Native. Рекомендуется batch insert (1000+ строк за INSERT) для минимизации количества parts.
Storage: MergeTree + text index + bloom_filterStorage: MergeTree с дневными партициями. Столбцовое сжатие: LowCardinality для повторяющихся полей (level, service, host), ZSTD для message. Text index для полнотекстового поиска по message. Bloom filter для точного поиска по trace_id.
Query: фильтрация + полнотекстовый поискQuery: ORDER BY (service, level, timestamp) обеспечивает быструю фильтрацию по сервису и уровню. Text index ускоряет LIKE/hasToken() по message. Bloom filter ускоряет точный поиск по trace_id без сканирования всех гранул.
TTL: автоматическое удаление по retention policyTTL: автоматическое удаление старых данных. Дневные партиции позволяют DROP PARTITION за миллисекунды. TTL ts + INTERVAL 30 DAY удаляет данные старше 30 дней при фоновом merge. Для compliance: разные TTL для разных уровней (ERROR: 90 дней, DEBUG: 7 дней).

Схема таблицы логов

CREATE TABLE logs (
    -- Временная метка с миллисекундной точностью
    timestamp DateTime64(3) CODEC(Delta, ZSTD(1)),

    -- Структурированные поля с LowCardinality
    level LowCardinality(String),       -- INFO, WARN, ERROR, DEBUG, TRACE
    service LowCardinality(String),     -- api-gateway, auth-service, payment
    host LowCardinality(String),        -- web-01, worker-03
    environment LowCardinality(String), -- production, staging

    -- Текстовые поля
    message String,

    -- Tracing
    trace_id String,
    span_id String,

    -- Переменные атрибуты (Kubernetes labels, HTTP headers)
    attributes Map(LowCardinality(String), String),

    -- Индексы
    INDEX idx_message message TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4,
    INDEX idx_trace trace_id TYPE bloom_filter(0.01) GRANULARITY 1
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (service, level, timestamp)
TTL timestamp + INTERVAL 30 DAY;

Разберём каждое решение в этой схеме.


LowCardinality для повторяющихся полей

Логи содержат множество столбцов с низкой кардинальностью: уровень (5 значений), сервис (десятки), хост (сотни), environment (2-3). LowCardinality заменяет строковые значения на целочисленные индексы словаря:

-- Без LowCardinality: каждая строка хранит полную строку "production"
environment String  -- 10 байт на строку

-- С LowCardinality: словарь ["production", "staging"] + индекс UInt8
environment LowCardinality(String)  -- 1 байт на строку + словарь

При миллиардах строк логов экономия — гигабайты. Фильтрация по LowCardinality-столбцу работает на целочисленных индексах, а не на строковом сравнении.

TIP

LowCardinality эффективен при кардинальности до ~10 000 уникальных значений. Для столбцов с миллионами уникальных значений (user_id, request_id) используйте обычный String или UInt64.


Text index для полнотекстового поиска

Начиная с ClickHouse 26.2, text index (inverted index) — GA-фича для полнотекстового поиска. Это замена legacy-индексов ngrambf_v1 и tokenbf_v1.

-- Text index (рекомендуется для ClickHouse 26.2+)
INDEX idx_message message TYPE full_text GRANULARITY 1

-- Legacy bloom-based index (для совместимости с более ранними версиями)
INDEX idx_message message TYPE tokenbf_v1(10240, 3, 0) GRANULARITY 4
NOTE

В схеме выше использован tokenbf_v1, который работает во всех версиях ClickHouse. Для 26.2+ рассмотрите замену на full_text index, обеспечивающий более точный поиск (без false positives bloom filter).

Text index ускоряет запросы с LIKE, hasToken(), multiSearchAny():

-- Поиск по ключевому слову в message
SELECT timestamp, service, level, message
FROM logs
WHERE service = 'payment'
  AND level = 'ERROR'
  AND message LIKE '%timeout%'
  AND timestamp >= now() - INTERVAL 1 HOUR
ORDER BY timestamp DESC
LIMIT 100;

Без индекса ClickHouse сканирует все гранулы. С text/bloom-индексом — только гранулы, которые могут содержать совпадение.


Bloom filter для trace_id

Trace ID — строка высокой кардинальности (миллионы уникальных значений). Full text index для неё избыточен. Bloom filter — компактная вероятностная структура данных, которая точно отвечает “этой гранулы точно НЕ содержит значение” и с малой вероятностью ошибки “эта гранула МОЖЕТ содержать значение”.

INDEX idx_trace trace_id TYPE bloom_filter(0.01) GRANULARITY 1
-- 0.01 = 1% false positive rate
-- GRANULARITY 1 = bloom filter на каждую гранулу (максимальная точность)

Типичный запрос по trace_id:

-- Найти все логи конкретного запроса
SELECT timestamp, service, level, message
FROM logs
WHERE trace_id = '4bf92f3577b34da6a3ce929d0e0e4736'
ORDER BY timestamp;

Bloom filter с 1% false positive rate и GRANULARITY 1 читает ~1% гранул вместо 100%.


Map для переменных атрибутов

Kubernetes labels, HTTP headers, custom metadata — набор ключей варьируется от записи к записи. Map(LowCardinality(String), String) хранит произвольные пары key-value без фиксации схемы:

attributes Map(LowCardinality(String), String)

Доступ к атрибутам:

-- Фильтрация по Kubernetes namespace
SELECT timestamp, message
FROM logs
WHERE attributes['k8s.namespace'] = 'production'
  AND timestamp >= now() - INTERVAL 1 HOUR;

-- Извлечение HTTP status code
SELECT
    attributes['http.status_code'] AS status,
    count() AS cnt
FROM logs
WHERE service = 'api-gateway'
GROUP BY status
ORDER BY cnt DESC;
WARNING

Map-столбцы не поддерживают skip indexes. Фильтрация по Map-ключу выполняется как полный scan гранулы. Для часто фильтруемых атрибутов (namespace, status_code) рассмотрите выделение в отдельный LowCardinality-столбец.


Дневные партиции и TTL

Логи — данные с коротким жизненным циклом. В отличие от метрик (месяцы/годы), логи обычно хранятся 7-90 дней. Дневные партиции обеспечивают:

  • Точный TTL: удаление данных с точностью до дня (не месяца)
  • Быстрый DROP PARTITION: моментальное удаление целого дня без фонового merge
  • Управляемый размер партиции: при 100 миллионах строк/день одна партиция ~ 1-5 ГБ в сжатом виде
PARTITION BY toYYYYMMDD(timestamp)
TTL timestamp + INTERVAL 30 DAY

Дифференцированный TTL по уровню:

-- Для разных retention policies используйте отдельные таблицы
-- или TTL с условием:
CREATE TABLE logs_tiered (
    timestamp DateTime64(3),
    level LowCardinality(String),
    message String,
    ...
) ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(timestamp)
ORDER BY (service, level, timestamp)
TTL
    timestamp + INTERVAL 7 DAY DELETE WHERE level = 'DEBUG',
    timestamp + INTERVAL 30 DAY DELETE WHERE level IN ('INFO', 'TRACE'),
    timestamp + INTERVAL 90 DAY DELETE WHERE level IN ('WARN', 'ERROR');

ORDER BY: service first для multi-tenant фильтрации

ORDER BY (service, level, timestamp)

Почему service первым?

В микросервисной архитектуре подавляющее большинство запросов к логам начинается с фильтра по сервису: “покажи ошибки payment-service за последний час”. Service первым в ORDER BY означает, что sparse index отсекает все гранулы других сервисов.

Level вторым: после выбора сервиса часто фильтруют по уровню (ERROR, WARN). Timestamp последним для хронологической сортировки внутри группы (service, level).


ClickHouse vs. ELK vs. Loki

АспектElasticsearchLokiClickHouse
Хранение (на 1 ТБ логов)1-1.5 ТБ (JSON + inverted index)200-300 ГБ (chunks + index)100-200 ГБ (columnar + codecs)
Полнотекстовый поискБыстрый (inverted index на всё)Медленный (brute-force)Быстрый (text index / bloom)
Structured queriesKQL / DSLLogQLSQL
Стоимость RAMВысокая (Java heap)НизкаяСредняя
TTL/RetentionIndex Lifecycle ManagementTable ManagerTTL + PARTITION
МасштабированиеГоризонтальное (шарды)ГоризонтальноеГоризонтальное (шарды)

Ключевые выводы

  1. LowCardinality — обязательно для level, service, host и других столбцов с низкой кардинальностью. Экономия хранения и ускорение фильтрации.
  2. Text index (GA 26.2) — современная замена ngrambf_v1/tokenbf_v1 для полнотекстового поиска по message.
  3. Bloom filter — для точного поиска по высококардинальным столбцам (trace_id, request_id).
  4. Map(LowCardinality(String), String) — для переменных атрибутов (Kubernetes labels, HTTP headers). Но без skip-index поддержки.
  5. Дневные партиции + TTL — для агрессивного retention. Дифференцированный TTL по уровню позволяет хранить ERROR дольше, чем DEBUG.
  6. ORDER BY (service, level, timestamp) — service первым для multi-tenant фильтрации в микросервисной архитектуре.
Apache Kafka: основы и архитектура Parquet: column statistics, min/max pruning и row group skipping

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Таблица logs содержит столбец message String с произвольным текстом логов. Нужен полнотекстовый поиск по ключевым словам (LIKE '%timeout%'). Какой тип индекса оптимален в ClickHouse 26.3?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7